dsh-plugin-subscriptions 0.4.2 → 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.
- package/README.md +19 -6
- package/README.zh.md +18 -6
- package/lib/auth/device-flow.d.ts +64 -0
- package/lib/auth/device-flow.js +176 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +21 -2
- package/lib/auth/rpc.js +23 -3
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/ImageGallery.d.ts +54 -0
- package/lib/client/ImageGallery.js +112 -0
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/ImageGenerateToolview.js +2 -2
- package/lib/client/SpeedSelect.d.ts +48 -0
- package/lib/client/SpeedSelect.js +173 -0
- package/lib/client/SubscriptionsSection.d.ts +10 -1
- package/lib/client/SubscriptionsSection.js +48 -5
- package/lib/client/index.d.ts +1 -0
- package/lib/client/index.js +42 -0
- package/lib/client/locales.d.ts +22 -0
- package/lib/client/locales.js +22 -0
- package/lib/client.js +679 -77
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1868 -183
- package/lib/providers/catalog-store.js +19 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +58 -31
- package/lib/providers/codex.d.ts +27 -0
- package/lib/providers/codex.js +117 -27
- package/lib/providers/common.d.ts +34 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +786 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +46 -18
- package/lib/tools/image-generate.js +3 -10
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +363 -0
- package/lib/translate/responses.d.ts +49 -5
- package/lib/translate/responses.js +40 -7
- package/package.json +6 -3
package/lib/providers/grok.d.ts
CHANGED
|
@@ -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
|
|
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[]>;
|
package/lib/providers/grok.js
CHANGED
|
@@ -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
|
|
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?.(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -16,7 +16,6 @@ import { mkdir, writeFile } from 'node:fs/promises';
|
|
|
16
16
|
import { basename, join } from 'node:path';
|
|
17
17
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
18
18
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
19
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
20
19
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
21
20
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
22
21
|
/** Endpoint the codex generation request is posted to. */
|
|
@@ -334,15 +333,9 @@ export function createImageGenerateTool(options) {
|
|
|
334
333
|
...refs.length > 0 ? { images: refs } : {},
|
|
335
334
|
...revisedPrompt === undefined ? {} : { revisedPrompt },
|
|
336
335
|
};
|
|
337
|
-
// Nested (Code Mode) dispatches
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
if (exec.parent !== undefined && refs.length > 0) {
|
|
341
|
-
exec.deferContext(createUserMessage({
|
|
342
|
-
content: imageGenerateContent(value),
|
|
343
|
-
source: { kind: 'plugin', plugin: 'dsh-plugin-subscriptions' },
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
336
|
+
// Nested (Code Mode) dispatches need no defer here: the harness's code
|
|
337
|
+
// mode already defers any image-bearing sub-result as a user message, so
|
|
338
|
+
// deferring again would inject the same attachment twice.
|
|
346
339
|
return value;
|
|
347
340
|
},
|
|
348
341
|
});
|
|
@@ -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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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;
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
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: '
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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;
|
|
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
|
-
|
|
109
|
-
|
|
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
|
|
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>;
|