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.
Files changed (44) hide show
  1. package/README.md +21 -6
  2. package/README.zh.md +20 -6
  3. package/lib/auth/device-flow.d.ts +55 -0
  4. package/lib/auth/device-flow.js +177 -0
  5. package/lib/auth/oauth-flow.js +1 -1
  6. package/lib/auth/rpc.d.ts +18 -2
  7. package/lib/auth/rpc.js +98 -3
  8. package/lib/auth/store.d.ts +20 -2
  9. package/lib/auth/store.js +45 -9
  10. package/lib/client/SubscriptionsSection.d.ts +18 -1
  11. package/lib/client/SubscriptionsSection.js +216 -6
  12. package/lib/client/index.js +11 -0
  13. package/lib/client/locales.d.ts +72 -0
  14. package/lib/client/locales.js +72 -0
  15. package/lib/client.js +725 -144
  16. package/lib/client.js.map +1 -1
  17. package/lib/http.d.ts +114 -0
  18. package/lib/http.js +402 -0
  19. package/lib/index.d.ts +3 -2
  20. package/lib/index.js +2256 -226
  21. package/lib/providers/antigravity.d.ts +90 -0
  22. package/lib/providers/antigravity.js +392 -0
  23. package/lib/providers/catalog-store.js +15 -0
  24. package/lib/providers/claude.d.ts +20 -1
  25. package/lib/providers/claude.js +51 -33
  26. package/lib/providers/codex.js +58 -13
  27. package/lib/providers/common.d.ts +32 -1
  28. package/lib/providers/common.js +48 -1
  29. package/lib/providers/copilot.d.ts +315 -0
  30. package/lib/providers/copilot.js +787 -0
  31. package/lib/providers/grok.d.ts +7 -2
  32. package/lib/providers/grok.js +53 -24
  33. package/lib/tools/image-generate.js +2 -1
  34. package/lib/tools/video-generate.js +2 -1
  35. package/lib/tools/x-search.js +2 -1
  36. package/lib/translate/anthropic.d.ts +47 -6
  37. package/lib/translate/anthropic.js +135 -20
  38. package/lib/translate/antigravity.d.ts +110 -0
  39. package/lib/translate/antigravity.js +303 -0
  40. package/lib/translate/chat-completions.d.ts +120 -0
  41. package/lib/translate/chat-completions.js +363 -0
  42. package/lib/translate/responses.d.ts +49 -5
  43. package/lib/translate/responses.js +40 -7
  44. package/package.json +11 -7
@@ -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,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 fetch(GROK_DISCOVERY_URL);
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 fetch(discovery.tokenEndpoint, {
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 fetch(session.tokenEndpoint, {
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 = fetch, signal) {
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 = fetch) {
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 models it does not cover simply expose no efforts.
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 = fetch, onWarn) {
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?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
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
- discovered.push({ id: entry.id, name: entry.id, ...cliCatalog?.get(entry.id) });
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
- 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
- }));
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 instanceof LlmError
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 fetch(GROK_API_URL, {
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 ?? fetch;
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 ?? fetch;
201
+ const fetchFn = options.fetchFn ?? proxiedFetch;
201
202
  const headers = {
202
203
  'authorization': `Bearer ${session.accessToken}`,
203
204
  'accept': 'application/json',
@@ -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 ?? fetch)(X_SEARCH_URL, {
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; 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,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[];