dsh-plugin-subscriptions 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,13 +101,17 @@ export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: Fetc
101
101
  * metadata (display name, context window, reasoning efforts). The api.x.ai
102
102
  * list stays authoritative for which models exist; the CLI catalog is
103
103
  * enrichment only, so its failure degrades to a plain list instead of taking
104
- * discovery down models it does not cover simply expose no efforts.
104
+ * discovery down. When enrichment is missing, last-known capability metadata
105
+ * is carried forward so a transient CLI outage cannot strip efforts a
106
+ * session already selected.
105
107
  * @param session - the stored session (used as-is; never refreshed here).
106
108
  * @param fetchFn - fetch implementation (injectable for tests).
107
109
  * @param onWarn - warning sink for a failed CLI catalog fetch.
110
+ * @param previous - last-known catalog used to keep enrichment when the CLI
111
+ * catalog is down or omits a model.
108
112
  * @returns discovered chat models in endpoint order.
109
113
  */
110
- export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void): Promise<DiscoveredModel[]>;
114
+ export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void, previous?: readonly DiscoveredModel[]): Promise<DiscoveredModel[]>;
111
115
  /** Constructor dependencies for {@link GrokAdapter}. */
112
116
  export interface GrokAdapterOptions {
113
117
  models: readonly ModelEntry[];
@@ -131,6 +135,7 @@ export declare class GrokAdapter extends LlmAdapter {
131
135
  constructor(options: GrokAdapterOptions);
132
136
  /** Discovery fetcher: resolves the session through the refresh-aware path. */
133
137
  private fetchCatalog;
138
+ private listed;
134
139
  providerInfo(provider: string): LlmProviderInfo;
135
140
  private staticModels;
136
141
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
@@ -7,7 +7,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
7
7
  import { decodeJwtPayload } from '../auth/jwt.js';
8
8
  import { resolveImages } from '../translate/resolved.js';
9
9
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
10
- import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
11
11
  export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
12
12
  export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
13
13
  export const GROK_API_URL = 'https://api.x.ai/v1/responses';
@@ -355,18 +355,40 @@ export async function fetchGrokCliCatalog(session, fetchFn = fetch) {
355
355
  function isChatModel(id) {
356
356
  return !/imagine|image-|video|embed/i.test(id);
357
357
  }
358
+ /**
359
+ * CLI-contributed fields carried forward from a previously discovered model.
360
+ * @param prior - the last-known entry for this id, if any.
361
+ * @returns enrichment to apply when the live CLI catalog cannot contribute.
362
+ */
363
+ function grokPriorMeta(prior) {
364
+ if (prior === undefined)
365
+ return {};
366
+ return {
367
+ ...(prior.name.length > 0 ? { name: prior.name } : {}),
368
+ ...(prior.description === undefined ? {} : { description: prior.description }),
369
+ ...(prior.contextWindow === undefined ? {} : { contextWindow: prior.contextWindow }),
370
+ ...(prior.reasoning === undefined ? {} : { reasoning: prior.reasoning }),
371
+ };
372
+ }
358
373
  /**
359
374
  * Fetch the live grok model list, enriched with the CLI catalog's per-model
360
375
  * metadata (display name, context window, reasoning efforts). The api.x.ai
361
376
  * list stays authoritative for which models exist; the CLI catalog is
362
377
  * enrichment only, so its failure degrades to a plain list instead of taking
363
- * discovery down models it does not cover simply expose no efforts.
378
+ * discovery down. When enrichment is missing, last-known capability metadata
379
+ * is carried forward so a transient CLI outage cannot strip efforts a
380
+ * session already selected.
364
381
  * @param session - the stored session (used as-is; never refreshed here).
365
382
  * @param fetchFn - fetch implementation (injectable for tests).
366
383
  * @param onWarn - warning sink for a failed CLI catalog fetch.
384
+ * @param previous - last-known catalog used to keep enrichment when the CLI
385
+ * catalog is down or omits a model.
367
386
  * @returns discovered chat models in endpoint order.
368
387
  */
369
- export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
388
+ export async function fetchGrokModels(session, fetchFn = fetch, onWarn, previous) {
389
+ const previousById = previous === undefined || previous.length === 0
390
+ ? undefined
391
+ : new Map(previous.map(model => [model.id, model]));
370
392
  const [response, cliCatalog] = await Promise.all([
371
393
  fetchFn(GROK_MODELS_URL, {
372
394
  headers: {
@@ -376,7 +398,9 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
376
398
  },
377
399
  }),
378
400
  fetchGrokCliCatalog(session, fetchFn).catch((error) => {
379
- onWarn?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
401
+ onWarn?.(previousById === undefined
402
+ ? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`
403
+ : `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
380
404
  return undefined;
381
405
  }),
382
406
  ]);
@@ -393,7 +417,12 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
393
417
  if (!isChatModel(entry.id))
394
418
  continue;
395
419
  seen.add(entry.id);
396
- discovered.push({ id: entry.id, name: entry.id, ...cliCatalog?.get(entry.id) });
420
+ const cli = cliCatalog?.get(entry.id);
421
+ discovered.push({
422
+ id: entry.id,
423
+ name: entry.id,
424
+ ...(cli ?? grokPriorMeta(previousById?.get(entry.id))),
425
+ });
397
426
  }
398
427
  // An empty catalog from a 200 response is treated as a discovery failure so
399
428
  // the adapter falls back to the static catalog instead of vanishing from
@@ -413,7 +442,16 @@ export class GrokAdapter extends LlmAdapter {
413
442
  }
414
443
  /** Discovery fetcher: resolves the session through the refresh-aware path. */
415
444
  async fetchCatalog() {
416
- return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
445
+ return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn, this.catalog.lastKnown());
446
+ }
447
+ listed(provider, discovered) {
448
+ return discovered.map(model => ({
449
+ provider,
450
+ id: model.id,
451
+ name: model.name,
452
+ ...model.description === undefined ? {} : { description: model.description },
453
+ inputModalities: grokModalities(model.id),
454
+ }));
417
455
  }
418
456
  providerInfo(provider) {
419
457
  return { id: provider, name: 'Grok (Subscription)' };
@@ -437,23 +475,13 @@ export class GrokAdapter extends LlmAdapter {
437
475
  // The fetcher runs only on a cache miss, and resolves the session
438
476
  // through the refresh-aware path so an expired access token renews here
439
477
  // instead of failing discovery into the static fallback.
440
- const discovered = await this.catalog.get(() => this.fetchCatalog());
441
- return discovered.map(model => ({
442
- provider,
443
- id: model.id,
444
- name: model.name,
445
- ...model.description === undefined ? {} : { description: model.description },
446
- inputModalities: grokModalities(model.id),
447
- }));
478
+ return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog())));
448
479
  }
449
480
  catch (error) {
450
481
  // A permanent refresh failure deletes the stored session: the provider
451
482
  // is logged out, so hide it instead of showing a stale static catalog.
452
- if (error instanceof LlmError
453
- && (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
483
+ if (isMissingOrInvalidCredential(error))
454
484
  return [];
455
- if (error instanceof OAuthEndpointError && error.status === 401)
456
- this.catalog.invalidate();
457
485
  this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
458
486
  return this.staticModels(provider);
459
487
  }
@@ -13,6 +13,25 @@ import type { TranslatableMessage } from './resolved.js';
13
13
  * system entry on every request.
14
14
  */
15
15
  export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
16
+ /** Tags wrapping a mid-conversation system message where it sits in the history. */
17
+ export declare const SYSTEM_REMINDER_OPEN = "<system-reminder>";
18
+ export declare const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
19
+ /**
20
+ * How far apart consecutive message breakpoints sit, in content blocks.
21
+ *
22
+ * A breakpoint looks back at most 20 blocks for an entry an earlier request
23
+ * wrote, so marks must stay closer than that: one agentic turn can append a
24
+ * dozen tool_use/tool_result blocks at once, and a single trailing mark would
25
+ * silently fall out of range and rebuild the whole prefix.
26
+ */
27
+ export declare const CACHE_BLOCK_STRIDE = 15;
28
+ /**
29
+ * Message breakpoints per request. Anthropic allows four in total and the
30
+ * last `system` block takes the fourth, so three are left for the history —
31
+ * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
32
+ * blocks before a read is lost.
33
+ */
34
+ export declare const MESSAGE_CACHE_BREAKPOINTS = 3;
16
35
  /** One Anthropic request message. */
17
36
  export interface AnthropicMessage {
18
37
  role: 'user' | 'assistant';
@@ -21,27 +40,49 @@ export interface AnthropicMessage {
21
40
  /**
22
41
  * Convert harness messages into Anthropic messages. Consecutive same-role
23
42
  * messages merge into one message with multiple content blocks; tool results
24
- * arrive as user messages with `tool_result` blocks; system-role messages are
25
- * handled by {@link toAnthropicSystem} and skipped here. Reasoning blocks are
26
- * not replayed (v1). Images must arrive pre-resolved
43
+ * arrive as user messages with `tool_result` blocks, which a merged user
44
+ * message keeps in one leading run ({@link leadWithToolResults}); system-role
45
+ * messages before the conversation starts are handled by
46
+ * {@link toAnthropicSystem} and skipped here, while a later one rides in
47
+ * place as a user-role `<system-reminder>` block.
48
+ * Reasoning blocks are not replayed (v1). Images must arrive pre-resolved
27
49
  * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
28
50
  * its bytes are unreachable here.
29
51
  * @param messages - ordered conversation messages with resolved images.
30
52
  * @returns Anthropic messages in conversation order.
31
53
  */
32
54
  export declare function toAnthropicMessages(messages: readonly TranslatableMessage[]): AnthropicMessage[];
55
+ /**
56
+ * Mark the conversation's cache breakpoints in place: the last content block,
57
+ * then one every {@link CACHE_BLOCK_STRIDE} blocks backwards, {@link
58
+ * MESSAGE_CACHE_BREAKPOINTS} in total.
59
+ *
60
+ * The history is append-only, so the block one request marks last is
61
+ * byte-identical in the next — that entry is what the next request reads.
62
+ * Marks are counted across the flattened block sequence, not per message,
63
+ * because the lookback window Anthropic walks counts blocks the same way.
64
+ * @param messages - assembled Anthropic messages, marked in place.
65
+ */
66
+ export declare function markMessageCache(messages: readonly AnthropicMessage[]): void;
33
67
  /**
34
68
  * Build the Anthropic `system` array: the mandatory Claude Code identity
35
69
  * block, then the explicit system prompt, then any system-role messages.
36
70
  * @param system - explicit system prompt, when set.
37
- * @param messages - conversation messages; their system-role text is appended.
71
+ * @param messages - conversation messages; the system-role text preceding the
72
+ * conversation is appended, and a later one is left to {@link toAnthropicMessages}.
38
73
  * @returns the system content blocks.
39
74
  */
40
75
  export declare function toAnthropicSystem(system?: string, messages?: readonly TranslatableMessage[]): Record<string, unknown>[];
41
76
  /**
42
- * Map harness tool schemas to Anthropic tools.
77
+ * Map harness tool schemas to Anthropic tools, in name order.
78
+ *
79
+ * `tools` renders at position 0 of the cached prefix, so any reordering
80
+ * invalidates every cache entry behind it — `system` and the whole
81
+ * conversation included. Registration order belongs to the caller and plugin
82
+ * load order can differ between processes, so the wire order is fixed here
83
+ * instead. Anthropic selects a tool by name; the array order carries nothing.
43
84
  * @param tools - tool schemas from the request.
44
- * @returns Anthropic `tools` array entries.
85
+ * @returns Anthropic `tools` array entries, ordered by tool name.
45
86
  */
46
87
  export declare function toAnthropicTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
47
88
  /** The subset of Anthropic SSE event shapes this translator reads. */
@@ -12,6 +12,25 @@ import { parseSse } from './sse.js';
12
12
  * system entry on every request.
13
13
  */
14
14
  export const CLAUDE_CODE_IDENTITY = 'You are Claude Code, Anthropic\'s official CLI for Claude.';
15
+ /** Tags wrapping a mid-conversation system message where it sits in the history. */
16
+ export const SYSTEM_REMINDER_OPEN = '<system-reminder>';
17
+ export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
18
+ /**
19
+ * How far apart consecutive message breakpoints sit, in content blocks.
20
+ *
21
+ * A breakpoint looks back at most 20 blocks for an entry an earlier request
22
+ * wrote, so marks must stay closer than that: one agentic turn can append a
23
+ * dozen tool_use/tool_result blocks at once, and a single trailing mark would
24
+ * silently fall out of range and rebuild the whole prefix.
25
+ */
26
+ export const CACHE_BLOCK_STRIDE = 15;
27
+ /**
28
+ * Message breakpoints per request. Anthropic allows four in total and the
29
+ * last `system` block takes the fourth, so three are left for the history —
30
+ * enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
31
+ * blocks before a read is lost.
32
+ */
33
+ export const MESSAGE_CACHE_BREAKPOINTS = 3;
15
34
  /** Flatten a tool result's content to plain text for `tool_result`. */
16
35
  function toolResultText(block) {
17
36
  return block.content.map(part => (part.type === 'text' ? part.text : '')).join('');
@@ -30,12 +49,55 @@ function parseToolInput(raw) {
30
49
  return {};
31
50
  }
32
51
  }
52
+ /**
53
+ * Move a user message's `tool_result` blocks into one contiguous run at the
54
+ * front, preserving the relative order of both groups.
55
+ *
56
+ * Anthropic answers every `tool_use` against the blocks that *lead* the next
57
+ * message, so a block of any other kind before or between the results reads
58
+ * as a call left unanswered and the request is rejected. The harness merges
59
+ * everything queued for one user turn into a single message, and a parallel
60
+ * tool batch arrives as one result message per call, so any context spliced
61
+ * mid-batch lands between two results. Restoring the run here keeps that
62
+ * independent of delivery order. Order *among* the results does not matter.
63
+ * @param message - one assembled user message, reordered in place.
64
+ */
65
+ function leadWithToolResults(message) {
66
+ const firstOther = message.content.findIndex(block => block.type !== 'tool_result');
67
+ if (firstOther === -1)
68
+ return;
69
+ if (!message.content.slice(firstOther).some(block => block.type === 'tool_result'))
70
+ return;
71
+ message.content = [
72
+ ...message.content.filter(block => block.type === 'tool_result'),
73
+ ...message.content.filter(block => block.type !== 'tool_result'),
74
+ ];
75
+ }
76
+ /**
77
+ * Index of the first non-system message; `messages.length` when every message
78
+ * is a system one.
79
+ *
80
+ * A system message before the conversation starts is the operator's opening
81
+ * instruction and belongs in the `system` slot. One that arrives later is
82
+ * mid-conversation context, and hoisting it into `system` would move bytes in
83
+ * front of the whole history — invalidating every cached turn behind it — so
84
+ * it stays where it is, as a reminder block in `messages`.
85
+ * @param messages - ordered conversation messages.
86
+ * @returns the boundary index separating the two.
87
+ */
88
+ function conversationStart(messages) {
89
+ const index = messages.findIndex(message => message.role !== 'system');
90
+ return index === -1 ? messages.length : index;
91
+ }
33
92
  /**
34
93
  * Convert harness messages into Anthropic messages. Consecutive same-role
35
94
  * messages merge into one message with multiple content blocks; tool results
36
- * arrive as user messages with `tool_result` blocks; system-role messages are
37
- * handled by {@link toAnthropicSystem} and skipped here. Reasoning blocks are
38
- * not replayed (v1). Images must arrive pre-resolved
95
+ * arrive as user messages with `tool_result` blocks, which a merged user
96
+ * message keeps in one leading run ({@link leadWithToolResults}); system-role
97
+ * messages before the conversation starts are handled by
98
+ * {@link toAnthropicSystem} and skipped here, while a later one rides in
99
+ * place as a user-role `<system-reminder>` block.
100
+ * Reasoning blocks are not replayed (v1). Images must arrive pre-resolved
39
101
  * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
40
102
  * its bytes are unreachable here.
41
103
  * @param messages - ordered conversation messages with resolved images.
@@ -43,24 +105,41 @@ function parseToolInput(raw) {
43
105
  */
44
106
  export function toAnthropicMessages(messages) {
45
107
  const out = [];
46
- for (const message of messages) {
47
- if (message.role === 'system')
108
+ const start = conversationStart(messages);
109
+ for (const [index, message] of messages.entries()) {
110
+ // A leading system message is an opening instruction; toAnthropicSystem
111
+ // owns those. A later one rides here so the cached prefix ahead of it
112
+ // stays byte-identical.
113
+ if (message.role === 'system' && index < start)
48
114
  continue;
49
- const role = message.role;
115
+ const role = message.role === 'system' ? 'user' : message.role;
50
116
  const blocks = [];
51
117
  for (const block of message.content) {
52
118
  switch (block.type) {
53
119
  case 'text':
54
- blocks.push({ type: 'text', text: block.text });
55
- break;
56
- case 'tool-call':
57
120
  blocks.push({
58
- type: 'tool_use',
59
- id: String(block.id),
60
- name: block.name,
61
- input: parseToolInput(block.arguments),
121
+ type: 'text',
122
+ text: message.role === 'system'
123
+ ? `${SYSTEM_REMINDER_OPEN}${block.text}${SYSTEM_REMINDER_CLOSE}`
124
+ : block.text,
62
125
  });
63
126
  break;
127
+ case 'tool-call':
128
+ // Anthropic accepts `tool_use` only in assistant messages, and only
129
+ // when a matching `tool_result` follows. A tool call in any other
130
+ // role is replayed narrative — a settled subagent's closing message
131
+ // spliced into the parent as a user-role notice carries the calls it
132
+ // died holding, which no result will ever answer — so it rides as
133
+ // descriptive text instead of a call the API would reject.
134
+ blocks.push(role === 'assistant'
135
+ ? {
136
+ type: 'tool_use',
137
+ id: String(block.id),
138
+ name: block.name,
139
+ input: parseToolInput(block.arguments),
140
+ }
141
+ : { type: 'text', text: `[tool call ${block.name}: ${block.arguments}]` });
142
+ break;
64
143
  case 'tool-result':
65
144
  blocks.push({
66
145
  type: 'tool_result',
@@ -92,36 +171,72 @@ export function toAnthropicMessages(messages) {
92
171
  else
93
172
  out.push({ role, content: blocks });
94
173
  }
174
+ for (const message of out) {
175
+ if (message.role === 'user')
176
+ leadWithToolResults(message);
177
+ }
95
178
  return out;
96
179
  }
180
+ /**
181
+ * Mark the conversation's cache breakpoints in place: the last content block,
182
+ * then one every {@link CACHE_BLOCK_STRIDE} blocks backwards, {@link
183
+ * MESSAGE_CACHE_BREAKPOINTS} in total.
184
+ *
185
+ * The history is append-only, so the block one request marks last is
186
+ * byte-identical in the next — that entry is what the next request reads.
187
+ * Marks are counted across the flattened block sequence, not per message,
188
+ * because the lookback window Anthropic walks counts blocks the same way.
189
+ * @param messages - assembled Anthropic messages, marked in place.
190
+ */
191
+ export function markMessageCache(messages) {
192
+ const blocks = messages.flatMap(message => message.content);
193
+ for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
194
+ const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
195
+ if (at < 0)
196
+ return;
197
+ blocks[at].cache_control = { type: 'ephemeral' };
198
+ }
199
+ }
97
200
  /**
98
201
  * Build the Anthropic `system` array: the mandatory Claude Code identity
99
202
  * block, then the explicit system prompt, then any system-role messages.
100
203
  * @param system - explicit system prompt, when set.
101
- * @param messages - conversation messages; their system-role text is appended.
204
+ * @param messages - conversation messages; the system-role text preceding the
205
+ * conversation is appended, and a later one is left to {@link toAnthropicMessages}.
102
206
  * @returns the system content blocks.
103
207
  */
104
208
  export function toAnthropicSystem(system, messages) {
105
209
  const blocks = [{ type: 'text', text: CLAUDE_CODE_IDENTITY }];
106
210
  if (system !== undefined && system.length > 0)
107
211
  blocks.push({ type: 'text', text: system });
108
- for (const message of messages ?? []) {
109
- if (message.role !== 'system')
110
- continue;
212
+ const history = messages ?? [];
213
+ for (const message of history.slice(0, conversationStart(history))) {
111
214
  for (const block of message.content) {
112
215
  if (block.type === 'text')
113
216
  blocks.push({ type: 'text', text: block.text });
114
217
  }
115
218
  }
219
+ // `tools` renders ahead of `system`, so this one marker caches both. It is
220
+ // deliberately separate from the message marks: a tool_choice or thinking
221
+ // change invalidates the messages tier only, and this entry survives it.
222
+ blocks[blocks.length - 1].cache_control = { type: 'ephemeral' };
116
223
  return blocks;
117
224
  }
118
225
  /**
119
- * Map harness tool schemas to Anthropic tools.
226
+ * Map harness tool schemas to Anthropic tools, in name order.
227
+ *
228
+ * `tools` renders at position 0 of the cached prefix, so any reordering
229
+ * invalidates every cache entry behind it — `system` and the whole
230
+ * conversation included. Registration order belongs to the caller and plugin
231
+ * load order can differ between processes, so the wire order is fixed here
232
+ * instead. Anthropic selects a tool by name; the array order carries nothing.
120
233
  * @param tools - tool schemas from the request.
121
- * @returns Anthropic `tools` array entries.
234
+ * @returns Anthropic `tools` array entries, ordered by tool name.
122
235
  */
123
236
  export function toAnthropicTools(tools) {
124
- return tools.map(tool => ({
237
+ return [...tools]
238
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
239
+ .map(tool => ({
125
240
  name: tool.name,
126
241
  description: tool.description,
127
242
  input_schema: tool.parameters,
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Translate between the harness message vocabulary and the OpenAI chat
3
+ * completions wire format the Copilot provider speaks: request message/tool
4
+ * assembly and a push-model SSE-chunk → StreamChunk state machine
5
+ * ({@link ChatCompletionsStreamTranslator}) mirroring the Responses
6
+ * translator, so tests need no streams.
7
+ */
8
+ import type { StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm';
9
+ import type { TranslatableMessage } from './resolved.js';
10
+ /**
11
+ * Convert harness messages into chat completions `messages`. System-role
12
+ * messages become one leading `system` message; an explicit `system` argument
13
+ * wins over them when both exist. Reasoning blocks are not replayed (matching
14
+ * the Responses translator). Images must arrive pre-resolved; an unresolved
15
+ * ImageBlock is skipped because its bytes are unreachable here. A user message
16
+ * carrying only text collapses to a plain string body (some endpoints still
17
+ * reject content-part arrays); tool results become separate `tool` messages.
18
+ * @param messages - ordered conversation messages with resolved images.
19
+ * @param system - explicit system prompt, which takes precedence.
20
+ * @returns the wire `messages` array.
21
+ */
22
+ export declare function toChatMessages(messages: readonly TranslatableMessage[], system?: string): Record<string, unknown>[];
23
+ /**
24
+ * Map harness tool schemas to chat completions function tools.
25
+ * @param tools - tool schemas from the request.
26
+ * @returns the wire `tools` array.
27
+ */
28
+ export declare function toChatTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
29
+ /** The subset of chat-completion chunk shapes this translator reads. */
30
+ export interface ChatCompletionsStreamEvent {
31
+ choices?: {
32
+ index?: number;
33
+ delta?: {
34
+ content?: string | null;
35
+ role?: string;
36
+ reasoning_content?: string | null;
37
+ /** Copilot's Gemini models stream thinking as `reasoning_text`. */
38
+ reasoning_text?: string | null;
39
+ tool_calls?: {
40
+ index?: number;
41
+ id?: string;
42
+ function?: {
43
+ name?: string;
44
+ arguments?: string;
45
+ };
46
+ }[];
47
+ };
48
+ finish_reason?: string | null;
49
+ }[];
50
+ usage?: ChatCompletionsUsage | null;
51
+ }
52
+ /** Chat completions `usage` object shape. */
53
+ export interface ChatCompletionsUsage {
54
+ prompt_tokens: number;
55
+ completion_tokens: number;
56
+ prompt_tokens_details?: {
57
+ cached_tokens?: number;
58
+ };
59
+ completion_tokens_details?: {
60
+ reasoning_tokens?: number;
61
+ };
62
+ }
63
+ /**
64
+ * Map chat completions usage to disjoint harness counts (cached input is
65
+ * subtracted out of `inputTokens` and reported as `cacheReadTokens`).
66
+ * @param usage - wire usage from the terminal chunk.
67
+ * @returns harness token usage.
68
+ */
69
+ export declare function mapChatCompletionsUsage(usage: ChatCompletionsUsage): TokenUsage;
70
+ /**
71
+ * Push-model chat completions SSE translator: feed each parsed chunk object
72
+ * to {@link push} and collect the emitted harness StreamChunks. The terminal
73
+ * `finish_reason` chunk closes every block but only ARMS the finish chunk —
74
+ * usage must precede the terminal finish, and where usage lives differs by
75
+ * upstream: OpenAI-style streams send a trailing usage-only chunk
76
+ * (stream_options.include_usage), while Copilot's Gemini models attach a
77
+ * (zero) usage object to EVERY chunk and fold the real usage into the
78
+ * finish chunk itself. A chunk therefore never early-returns on `usage`
79
+ * alone: its deltas are always processed, and the terminal pair is drained
80
+ * when the finish is armed and usage arrived (or when a usage-only chunk
81
+ * follows an armed finish). `flush()` emits whatever remains when the
82
+ * stream's `[DONE]` (or EOF) arrives.
83
+ */
84
+ export declare class ChatCompletionsStreamTranslator {
85
+ /** Text/reasoning blocks keyed by kind; tool calls keyed by their wire index. */
86
+ private blocks;
87
+ private order;
88
+ private nextIndex;
89
+ private sawToolCall;
90
+ private pendingUsage;
91
+ private armedFinish;
92
+ /** Set once the terminal finish chunk was emitted. */
93
+ terminated: boolean;
94
+ private open;
95
+ private close;
96
+ private closeAll;
97
+ /** Build the terminal finish chunk for one wire finish reason. */
98
+ private finishChunk;
99
+ /** Usage, then the armed finish: the only order the harness accepts. */
100
+ private drainTerminal;
101
+ /**
102
+ * Process one parsed chat-completion chunk.
103
+ * @param event - the parsed chunk object.
104
+ * @returns the StreamChunks this event produced (possibly none).
105
+ */
106
+ push(event: ChatCompletionsStreamEvent): StreamChunk[];
107
+ /**
108
+ * Emit whatever the stream left pending (`[DONE]` or EOF without a final
109
+ * usage chunk). Safe to call repeatedly.
110
+ * @returns the remaining terminal chunks.
111
+ */
112
+ flush(): StreamChunk[];
113
+ }
114
+ /**
115
+ * Consume a chat completions SSE byte stream and yield harness StreamChunks.
116
+ * @param stream - raw response body.
117
+ * @param onActivity - transport-activity callback for the idle watchdog.
118
+ * @returns the chunk stream; throws when the stream ends before any finish chunk.
119
+ */
120
+ export declare function streamChatCompletions(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;