dsh-plugin-subscriptions 0.5.0 → 0.5.2
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 +21 -6
- package/README.zh.md +20 -6
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +18 -2
- package/lib/auth/rpc.js +98 -3
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/SubscriptionsSection.d.ts +18 -1
- package/lib/client/SubscriptionsSection.js +216 -6
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +72 -0
- package/lib/client/locales.js +72 -0
- package/lib/client.js +725 -144
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2256 -226
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/catalog-store.js +15 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +51 -33
- package/lib/providers/codex.js +58 -13
- package/lib/providers/common.d.ts +32 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +787 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +53 -24
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.js +2 -1
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- 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 +11 -7
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,8 @@ 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
|
+
import { proxiedFetch } from '../http.js';
|
|
11
12
|
export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
12
13
|
export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
|
|
13
14
|
export const GROK_API_URL = 'https://api.x.ai/v1/responses';
|
|
@@ -40,7 +41,7 @@ let discoveryCache;
|
|
|
40
41
|
export async function grokDiscovery() {
|
|
41
42
|
if (discoveryCache !== undefined)
|
|
42
43
|
return discoveryCache;
|
|
43
|
-
const response = await
|
|
44
|
+
const response = await proxiedFetch(GROK_DISCOVERY_URL);
|
|
44
45
|
if (!response.ok)
|
|
45
46
|
throw await oauthEndpointError(response, 'grok OIDC discovery');
|
|
46
47
|
const document = await response.json();
|
|
@@ -147,7 +148,7 @@ function grokSession(tokens, tokenEndpoint, fallbackRefreshToken) {
|
|
|
147
148
|
*/
|
|
148
149
|
export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
149
150
|
const discovery = await grokDiscovery();
|
|
150
|
-
const response = await
|
|
151
|
+
const response = await proxiedFetch(discovery.tokenEndpoint, {
|
|
151
152
|
method: 'POST',
|
|
152
153
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
153
154
|
body: new URLSearchParams({
|
|
@@ -174,7 +175,7 @@ export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
|
174
175
|
* @returns the fresh session to store.
|
|
175
176
|
*/
|
|
176
177
|
export async function refreshGrok(session) {
|
|
177
|
-
const response = await
|
|
178
|
+
const response = await proxiedFetch(session.tokenEndpoint, {
|
|
178
179
|
method: 'POST',
|
|
179
180
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
180
181
|
body: new URLSearchParams({
|
|
@@ -223,7 +224,7 @@ function grokResetsAt(value) {
|
|
|
223
224
|
* @param signal - caller cancellation from the RPC transport.
|
|
224
225
|
* @returns the mapped usage snapshot.
|
|
225
226
|
*/
|
|
226
|
-
export async function fetchGrokUsage(session, fetchFn =
|
|
227
|
+
export async function fetchGrokUsage(session, fetchFn = proxiedFetch, signal) {
|
|
227
228
|
const response = await fetchFn(GROK_BILLING_URL, {
|
|
228
229
|
headers: {
|
|
229
230
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -313,7 +314,7 @@ function grokCliReasoning(entry) {
|
|
|
313
314
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
314
315
|
* @returns model id → contributed metadata.
|
|
315
316
|
*/
|
|
316
|
-
export async function fetchGrokCliCatalog(session, fetchFn =
|
|
317
|
+
export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch) {
|
|
317
318
|
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
318
319
|
headers: {
|
|
319
320
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -355,18 +356,40 @@ export async function fetchGrokCliCatalog(session, fetchFn = fetch) {
|
|
|
355
356
|
function isChatModel(id) {
|
|
356
357
|
return !/imagine|image-|video|embed/i.test(id);
|
|
357
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* CLI-contributed fields carried forward from a previously discovered model.
|
|
361
|
+
* @param prior - the last-known entry for this id, if any.
|
|
362
|
+
* @returns enrichment to apply when the live CLI catalog cannot contribute.
|
|
363
|
+
*/
|
|
364
|
+
function grokPriorMeta(prior) {
|
|
365
|
+
if (prior === undefined)
|
|
366
|
+
return {};
|
|
367
|
+
return {
|
|
368
|
+
...(prior.name.length > 0 ? { name: prior.name } : {}),
|
|
369
|
+
...(prior.description === undefined ? {} : { description: prior.description }),
|
|
370
|
+
...(prior.contextWindow === undefined ? {} : { contextWindow: prior.contextWindow }),
|
|
371
|
+
...(prior.reasoning === undefined ? {} : { reasoning: prior.reasoning }),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
358
374
|
/**
|
|
359
375
|
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
360
376
|
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
361
377
|
* list stays authoritative for which models exist; the CLI catalog is
|
|
362
378
|
* enrichment only, so its failure degrades to a plain list instead of taking
|
|
363
|
-
* discovery down
|
|
379
|
+
* discovery down. When enrichment is missing, last-known capability metadata
|
|
380
|
+
* is carried forward so a transient CLI outage cannot strip efforts a
|
|
381
|
+
* session already selected.
|
|
364
382
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
365
383
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
366
384
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
385
|
+
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
386
|
+
* catalog is down or omits a model.
|
|
367
387
|
* @returns discovered chat models in endpoint order.
|
|
368
388
|
*/
|
|
369
|
-
export async function fetchGrokModels(session, fetchFn =
|
|
389
|
+
export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous) {
|
|
390
|
+
const previousById = previous === undefined || previous.length === 0
|
|
391
|
+
? undefined
|
|
392
|
+
: new Map(previous.map(model => [model.id, model]));
|
|
370
393
|
const [response, cliCatalog] = await Promise.all([
|
|
371
394
|
fetchFn(GROK_MODELS_URL, {
|
|
372
395
|
headers: {
|
|
@@ -376,7 +399,9 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
|
376
399
|
},
|
|
377
400
|
}),
|
|
378
401
|
fetchGrokCliCatalog(session, fetchFn).catch((error) => {
|
|
379
|
-
onWarn?.(
|
|
402
|
+
onWarn?.(previousById === undefined
|
|
403
|
+
? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`
|
|
404
|
+
: `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
|
|
380
405
|
return undefined;
|
|
381
406
|
}),
|
|
382
407
|
]);
|
|
@@ -393,7 +418,12 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
|
|
|
393
418
|
if (!isChatModel(entry.id))
|
|
394
419
|
continue;
|
|
395
420
|
seen.add(entry.id);
|
|
396
|
-
|
|
421
|
+
const cli = cliCatalog?.get(entry.id);
|
|
422
|
+
discovered.push({
|
|
423
|
+
id: entry.id,
|
|
424
|
+
name: entry.id,
|
|
425
|
+
...(cli ?? grokPriorMeta(previousById?.get(entry.id))),
|
|
426
|
+
});
|
|
397
427
|
}
|
|
398
428
|
// An empty catalog from a 200 response is treated as a discovery failure so
|
|
399
429
|
// the adapter falls back to the static catalog instead of vanishing from
|
|
@@ -413,7 +443,16 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
413
443
|
}
|
|
414
444
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
415
445
|
async fetchCatalog() {
|
|
416
|
-
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn);
|
|
446
|
+
return fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn, this.catalog.lastKnown());
|
|
447
|
+
}
|
|
448
|
+
listed(provider, discovered) {
|
|
449
|
+
return discovered.map(model => ({
|
|
450
|
+
provider,
|
|
451
|
+
id: model.id,
|
|
452
|
+
name: model.name,
|
|
453
|
+
...model.description === undefined ? {} : { description: model.description },
|
|
454
|
+
inputModalities: grokModalities(model.id),
|
|
455
|
+
}));
|
|
417
456
|
}
|
|
418
457
|
providerInfo(provider) {
|
|
419
458
|
return { id: provider, name: 'Grok (Subscription)' };
|
|
@@ -437,23 +476,13 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
437
476
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
438
477
|
// through the refresh-aware path so an expired access token renews here
|
|
439
478
|
// 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
|
-
}));
|
|
479
|
+
return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog())));
|
|
448
480
|
}
|
|
449
481
|
catch (error) {
|
|
450
482
|
// A permanent refresh failure deletes the stored session: the provider
|
|
451
483
|
// 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'))
|
|
484
|
+
if (isMissingOrInvalidCredential(error))
|
|
454
485
|
return [];
|
|
455
|
-
if (error instanceof OAuthEndpointError && error.status === 401)
|
|
456
|
-
this.catalog.invalidate();
|
|
457
486
|
this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
458
487
|
return this.staticModels(provider);
|
|
459
488
|
}
|
|
@@ -534,7 +563,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
534
563
|
store: false,
|
|
535
564
|
stream: true,
|
|
536
565
|
};
|
|
537
|
-
return
|
|
566
|
+
return proxiedFetch(GROK_API_URL, {
|
|
538
567
|
method: 'POST',
|
|
539
568
|
headers: {
|
|
540
569
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -18,6 +18,7 @@ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
|
18
18
|
import { AttachmentId } from '@deepseek-ai/dsh-attachment';
|
|
19
19
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
20
20
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
21
|
+
import { proxiedFetch } from '../http.js';
|
|
21
22
|
/** Endpoint the codex generation request is posted to. */
|
|
22
23
|
export const IMAGE_GENERATE_URL = 'https://chatgpt.com/backend-api/codex/images/generations';
|
|
23
24
|
/** The image model the codex subscription endpoint serves. */
|
|
@@ -241,7 +242,7 @@ export function createImageGenerateTool(options) {
|
|
|
241
242
|
content: result.content.filter(block => block.type === 'text'),
|
|
242
243
|
}),
|
|
243
244
|
async execute(args, exec) {
|
|
244
|
-
const fetchFn = options.fetchFn ??
|
|
245
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
245
246
|
// Provider selection: the preferred provider (default gpt) when logged
|
|
246
247
|
// in, the other one as the fallback. A configured-but-logged-out manager
|
|
247
248
|
// still resolves through `session()` below so the standard log-in hint
|
|
@@ -13,6 +13,7 @@ import { basename, join } from 'node:path';
|
|
|
13
13
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
14
14
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
15
15
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
16
|
+
import { proxiedFetch } from '../http.js';
|
|
16
17
|
/** Endpoint the generation request is posted to. */
|
|
17
18
|
export const VIDEO_GENERATE_URL = 'https://api.x.ai/v1/videos/generations';
|
|
18
19
|
/** The video model the grok subscription endpoint serves. */
|
|
@@ -197,7 +198,7 @@ export function createVideoGenerateTool(options) {
|
|
|
197
198
|
async execute(args, exec) {
|
|
198
199
|
const body = buildVideoGenerateBody(args);
|
|
199
200
|
const session = await options.tokens.session();
|
|
200
|
-
const fetchFn = options.fetchFn ??
|
|
201
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
201
202
|
const headers = {
|
|
202
203
|
'authorization': `Bearer ${session.accessToken}`,
|
|
203
204
|
'accept': 'application/json',
|
package/lib/tools/x-search.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
8
|
import { httpLlmError, TokenManager } from '../providers/common.js';
|
|
9
|
+
import { proxiedFetch } from '../http.js';
|
|
9
10
|
/** Endpoint the search request is posted to. */
|
|
10
11
|
export const X_SEARCH_URL = 'https://api.x.ai/v1/responses';
|
|
11
12
|
/** Grok model the search runs on (a catalog model of the grok provider). */
|
|
@@ -172,7 +173,7 @@ export function createXSearchTool(options) {
|
|
|
172
173
|
async execute(args, exec) {
|
|
173
174
|
const request = buildXSearchRequest(args);
|
|
174
175
|
const session = await options.tokens.session();
|
|
175
|
-
const response = await (options.fetchFn ??
|
|
176
|
+
const response = await (options.fetchFn ?? proxiedFetch)(X_SEARCH_URL, {
|
|
176
177
|
method: 'POST',
|
|
177
178
|
headers: {
|
|
178
179
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -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,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness message/tool vocabulary to Antigravity's Gemini-shaped
|
|
3
|
+
* v1internal request envelope, plus response/SSE translation back to the
|
|
4
|
+
* harness streaming contract.
|
|
5
|
+
*/
|
|
6
|
+
import type { GenerateOptions, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm';
|
|
7
|
+
import type { TranslatableMessage } from './resolved.js';
|
|
8
|
+
/** Minimal Gemini part shape used by v1internal. */
|
|
9
|
+
export interface AntigravityPart {
|
|
10
|
+
text?: string;
|
|
11
|
+
thought?: boolean;
|
|
12
|
+
thoughtSignature?: string;
|
|
13
|
+
inlineData?: {
|
|
14
|
+
mimeType: string;
|
|
15
|
+
data: string;
|
|
16
|
+
};
|
|
17
|
+
functionCall?: {
|
|
18
|
+
id?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
args?: unknown;
|
|
21
|
+
};
|
|
22
|
+
functionResponse?: {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
response: unknown;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Full Antigravity request envelope. */
|
|
29
|
+
export interface AntigravityRequest {
|
|
30
|
+
project: string;
|
|
31
|
+
requestId: string;
|
|
32
|
+
model: string;
|
|
33
|
+
userAgent: 'antigravity';
|
|
34
|
+
requestType: 'agent';
|
|
35
|
+
request: {
|
|
36
|
+
contents: {
|
|
37
|
+
role: 'user' | 'model';
|
|
38
|
+
parts: AntigravityPart[];
|
|
39
|
+
}[];
|
|
40
|
+
sessionId: string;
|
|
41
|
+
systemInstruction?: {
|
|
42
|
+
parts: {
|
|
43
|
+
text: string;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
tools?: {
|
|
47
|
+
functionDeclarations: Record<string, unknown>[];
|
|
48
|
+
}[];
|
|
49
|
+
toolConfig?: {
|
|
50
|
+
functionCallingConfig: {
|
|
51
|
+
mode: 'VALIDATED';
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
generationConfig?: Record<string, unknown>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Map harness tool schemas to Gemini function declarations. */
|
|
58
|
+
export declare function toAntigravityTools(tools: readonly ToolSchema[]): {
|
|
59
|
+
functionDeclarations: Record<string, unknown>[];
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* Convert resolved harness messages into Gemini contents. Function response
|
|
63
|
+
* names are recovered from prior tool calls because DSH correlates results by
|
|
64
|
+
* id while the Gemini wire requires both id and name.
|
|
65
|
+
*/
|
|
66
|
+
export declare function toAntigravityContents(messages: readonly TranslatableMessage[]): {
|
|
67
|
+
role: 'user' | 'model';
|
|
68
|
+
parts: AntigravityPart[];
|
|
69
|
+
}[];
|
|
70
|
+
/** Build one v1internal generateContent/streamGenerateContent request. */
|
|
71
|
+
export declare function toAntigravityRequest(options: GenerateOptions, messages: readonly TranslatableMessage[], projectId: string): AntigravityRequest;
|
|
72
|
+
/** Antigravity SSE/non-stream response subset. */
|
|
73
|
+
export interface AntigravityResponseEvent {
|
|
74
|
+
response?: {
|
|
75
|
+
candidates?: {
|
|
76
|
+
content?: {
|
|
77
|
+
parts?: AntigravityPart[];
|
|
78
|
+
};
|
|
79
|
+
finishReason?: string;
|
|
80
|
+
}[];
|
|
81
|
+
usageMetadata?: {
|
|
82
|
+
promptTokenCount?: number;
|
|
83
|
+
candidatesTokenCount?: number;
|
|
84
|
+
thoughtsTokenCount?: number;
|
|
85
|
+
totalTokenCount?: number;
|
|
86
|
+
cachedContentTokenCount?: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Map Gemini usage metadata to the harness's disjoint counters. */
|
|
91
|
+
export declare function mapAntigravityUsage(metadata: NonNullable<NonNullable<AntigravityResponseEvent['response']>['usageMetadata']>): TokenUsage;
|
|
92
|
+
/** Push translator for both parsed SSE events and one non-stream response. */
|
|
93
|
+
export declare class AntigravityStreamTranslator {
|
|
94
|
+
private blocks;
|
|
95
|
+
private closed;
|
|
96
|
+
private nextIndex;
|
|
97
|
+
private sawContent;
|
|
98
|
+
private sawToolCall;
|
|
99
|
+
terminated: boolean;
|
|
100
|
+
private open;
|
|
101
|
+
private close;
|
|
102
|
+
private closeAll;
|
|
103
|
+
private finish;
|
|
104
|
+
/** Process one decoded Antigravity response frame. */
|
|
105
|
+
push(event: AntigravityResponseEvent): StreamChunk[];
|
|
106
|
+
}
|
|
107
|
+
/** Consume Antigravity's SSE response into the DSH streaming contract. */
|
|
108
|
+
export declare function streamAntigravity(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
|
|
109
|
+
/** Translate a non-stream generateContent response using the same state machine. */
|
|
110
|
+
export declare function parseAntigravityResponse(event: AntigravityResponseEvent): StreamChunk[];
|