dsh-plugin-subscriptions 0.5.1 → 0.5.3

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 (50) hide show
  1. package/README.md +42 -1
  2. package/README.zh.md +42 -1
  3. package/lib/auth/device-flow.d.ts +0 -9
  4. package/lib/auth/device-flow.js +2 -1
  5. package/lib/auth/rpc.d.ts +44 -13
  6. package/lib/auth/rpc.js +127 -9
  7. package/lib/auth/store.d.ts +75 -17
  8. package/lib/auth/store.js +148 -27
  9. package/lib/client/SubscriptionsSection.d.ts +26 -3
  10. package/lib/client/SubscriptionsSection.js +263 -67
  11. package/lib/client/index.js +11 -0
  12. package/lib/client/locales.d.ts +82 -10
  13. package/lib/client/locales.js +82 -10
  14. package/lib/client.js +837 -223
  15. package/lib/client.js.map +1 -1
  16. package/lib/http.d.ts +114 -0
  17. package/lib/http.js +402 -0
  18. package/lib/index.d.ts +21 -0
  19. package/lib/index.js +1938 -208
  20. package/lib/providers/accounts.d.ts +102 -0
  21. package/lib/providers/accounts.js +123 -0
  22. package/lib/providers/antigravity.d.ts +90 -0
  23. package/lib/providers/antigravity.js +392 -0
  24. package/lib/providers/claude.d.ts +22 -4
  25. package/lib/providers/claude.js +97 -16
  26. package/lib/providers/codex.d.ts +24 -3
  27. package/lib/providers/codex.js +121 -21
  28. package/lib/providers/common.d.ts +17 -0
  29. package/lib/providers/common.js +67 -3
  30. package/lib/providers/copilot.d.ts +23 -4
  31. package/lib/providers/copilot.js +99 -19
  32. package/lib/providers/grok.d.ts +24 -4
  33. package/lib/providers/grok.js +106 -19
  34. package/lib/providers/pool-family.d.ts +56 -0
  35. package/lib/providers/pool-family.js +45 -0
  36. package/lib/providers/pool-health.d.ts +74 -0
  37. package/lib/providers/pool-health.js +148 -0
  38. package/lib/providers/pool-usage.d.ts +57 -0
  39. package/lib/providers/pool-usage.js +130 -0
  40. package/lib/providers/pool.d.ts +107 -0
  41. package/lib/providers/pool.js +371 -0
  42. package/lib/tools/image-generate.d.ts +3 -3
  43. package/lib/tools/image-generate.js +4 -2
  44. package/lib/tools/video-generate.d.ts +2 -2
  45. package/lib/tools/video-generate.js +4 -2
  46. package/lib/tools/x-search.d.ts +2 -2
  47. package/lib/tools/x-search.js +4 -2
  48. package/lib/translate/antigravity.d.ts +110 -0
  49. package/lib/translate/antigravity.js +303 -0
  50. package/package.json +14 -9
@@ -0,0 +1,371 @@
1
+ /**
2
+ * The pool adapter: same-subscription account routing, plus optional
3
+ * configured tier extras. The picker is the union of every account's
4
+ * catalog. A model listed by several accounts failovers; a model listed by
5
+ * one account is pinned to it. Tiers are extra picker rows. Member
6
+ * selection is sticky per session (so prompt caches survive) and optionally
7
+ * quota-aware; failures fail over to the next member as long as no stream
8
+ * chunk has been emitted.
9
+ */
10
+ import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
11
+ import { poolKey } from './pool-family.js';
12
+ import { accountKey, classifyPoolFailure, memberKey, PoolHealthRegistry } from './pool-health.js';
13
+ /** Bound on sticky-session memory; oldest entries evict past it. */
14
+ const STICKY_SESSION_LIMIT = 1000;
15
+ /** Display form of one member (account shown when pinned). */
16
+ function memberLabel(member) {
17
+ return member.account === undefined
18
+ ? `${member.provider}/${member.model}`
19
+ : `${member.provider}/${member.account}/${member.model}`;
20
+ }
21
+ /** How long a pools snapshot is trusted (auth changes invalidate immediately). */
22
+ const POOLS_CACHE_TTL_MS = 5_000;
23
+ export class PoolAdapter extends LlmAdapter {
24
+ options;
25
+ /** sessionId|poolId → member key of the last member that served a chunk. */
26
+ sticky = new Map();
27
+ /** Messages already warned about — configuration diagnostics repeat every request otherwise. */
28
+ warned = new Set();
29
+ /**
30
+ * Short-lived pools snapshot. `owns()` runs on every resolveModel — the
31
+ * model picker issues one per entry — and pool assembly touches every
32
+ * provider's catalog and account store, so recompute at most this often.
33
+ * Auth changes bump {@link generation} so a stale snapshot cannot land.
34
+ */
35
+ poolsCache;
36
+ poolsInflight;
37
+ generation = 0;
38
+ constructor(options) {
39
+ super();
40
+ this.options = options;
41
+ }
42
+ /** Drop the pools snapshot so the next read reflects the current accounts. */
43
+ invalidate() {
44
+ this.generation += 1;
45
+ this.poolsCache = undefined;
46
+ this.poolsInflight = undefined;
47
+ }
48
+ /** Warn once per distinct message (pools() runs on every request). */
49
+ warnOnce(message) {
50
+ if (this.warned.has(message))
51
+ return;
52
+ this.warned.add(message);
53
+ this.options.onWarn(message);
54
+ }
55
+ /** Drop members whose adapter is not registered (copy — caller state is shared). */
56
+ usable(pools) {
57
+ const result = new Map(pools);
58
+ for (const [id, definition] of [...result]) {
59
+ const kept = definition.members.filter(member => this.options.adapters[member.provider] !== undefined);
60
+ if (kept.length === 0)
61
+ result.delete(id);
62
+ else if (kept.length < definition.members.length)
63
+ result.set(id, { ...definition, members: kept });
64
+ }
65
+ return result;
66
+ }
67
+ /** Account pools (auto-aggregated plus config overrides) with usable members. */
68
+ async familyPools() {
69
+ return this.usable(new Map(await this.options.families()));
70
+ }
71
+ /** All pools (account pools merged with extra tiers) with usable members. */
72
+ async pools() {
73
+ const cached = this.poolsCache;
74
+ if (cached !== undefined && Date.now() - cached.at < POOLS_CACHE_TTL_MS)
75
+ return cached.pools;
76
+ const gen = this.generation;
77
+ this.poolsInflight ??= this.assemblePools()
78
+ .then((pools) => {
79
+ if (this.generation === gen)
80
+ this.poolsCache = { at: Date.now(), pools };
81
+ return pools;
82
+ })
83
+ .finally(() => {
84
+ this.poolsInflight = undefined;
85
+ });
86
+ return this.poolsInflight;
87
+ }
88
+ /** Recompute the pools snapshot (account pools merged with extra tiers). */
89
+ async assemblePools() {
90
+ const pools = await this.familyPools();
91
+ for (const [id, members] of Object.entries(this.options.tiers)) {
92
+ if (members.length === 0)
93
+ continue;
94
+ const owner = members[0].provider;
95
+ const key = poolKey(owner, id);
96
+ if (pools.has(key))
97
+ this.warnOnce(`tier pool "${id}" overrides the account pool of the same id under ${owner}`);
98
+ pools.set(key, { members, extra: true });
99
+ }
100
+ return this.usable(pools);
101
+ }
102
+ /**
103
+ * Extra picker rows one provider lists (configured tiers). Account pools
104
+ * reuse the catalog entry of the same wire id, so they are not listed
105
+ * again — the picker stays one row per model in ChatGPT / Claude / ….
106
+ */
107
+ async modelsForProvider(provider) {
108
+ const pools = await this.pools();
109
+ const models = [];
110
+ for (const [key, definition] of pools) {
111
+ if (definition.extra !== true)
112
+ continue;
113
+ if (!key.startsWith(`${provider}/`))
114
+ continue;
115
+ const id = key.slice(provider.length + 1);
116
+ models.push({
117
+ provider,
118
+ id,
119
+ name: definition.name ?? id,
120
+ ...definition.description === undefined ? {} : { description: definition.description },
121
+ });
122
+ }
123
+ return models;
124
+ }
125
+ /**
126
+ * Whether `model` on `provider`'s route is served here (several accounts
127
+ * fail over, one account is pinned, or a configured tier).
128
+ */
129
+ async owns(provider, model) {
130
+ return (await this.pools()).has(poolKey(provider, model));
131
+ }
132
+ /**
133
+ * Resolve every member's account (config members may omit it to mean "the
134
+ * default account") and drop members with no resolvable login. Duplicates
135
+ * collapse — an explicitly pinned account and the default may coincide.
136
+ */
137
+ async concrete(members) {
138
+ const seen = new Set();
139
+ const resolved = [];
140
+ for (const member of members) {
141
+ const account = member.account ?? await this.options.defaultAccount(member.provider);
142
+ if (account === undefined)
143
+ continue;
144
+ const key = memberKey(member.provider, account, member.model);
145
+ if (seen.has(key))
146
+ continue;
147
+ seen.add(key);
148
+ resolved.push({ provider: member.provider, account, model: member.model });
149
+ }
150
+ return resolved;
151
+ }
152
+ /**
153
+ * Resolve a pool model to the conservative INTERSECTION of its members'
154
+ * capabilities: the smallest context window and output cap, the reasoning
155
+ * efforts every member supports, and the modalities all of them accept —
156
+ * so a request valid for the pool stays valid after a failover. Capability
157
+ * metadata is provider-level, so each provider resolves once regardless of
158
+ * how many accounts it pools.
159
+ */
160
+ async resolveModel(provider, model) {
161
+ const definition = (await this.pools()).get(poolKey(provider, model));
162
+ if (definition === undefined)
163
+ throw new LlmError(`unknown pool model "${model}"`, 'NO_ADAPTER');
164
+ const resolved = [];
165
+ let lastFailure;
166
+ const seenProviders = new Set();
167
+ for (const member of definition.members) {
168
+ if (seenProviders.has(member.provider))
169
+ continue;
170
+ seenProviders.add(member.provider);
171
+ const adapter = this.options.adapters[member.provider];
172
+ if (adapter === undefined)
173
+ continue;
174
+ // Tolerate per-member failures (a misconfigured tier member, a
175
+ // logged-out provider throwing AUTH): the pool serves as long as ONE
176
+ // member resolves, mirroring stream()'s failover semantics.
177
+ try {
178
+ resolved.push(await adapter.resolveOwnModel(member.provider, member.model));
179
+ }
180
+ catch (error) {
181
+ lastFailure = error;
182
+ this.warnOnce(`pool "${model}": member ${memberLabel(member)} failed to resolve`
183
+ + ` (${error instanceof Error ? error.message : String(error)}); excluding it`);
184
+ }
185
+ }
186
+ if (resolved.length === 0) {
187
+ throw new LlmError(`pool "${model}" has no usable member`, 'NO_ADAPTER', {
188
+ ...lastFailure === undefined ? {} : { cause: lastFailure },
189
+ });
190
+ }
191
+ const contextWindows = resolved.map(info => info.context?.contextWindow).filter(isNumber);
192
+ const maxTokens = resolved.map(info => info.defaultMaxTokens).filter(isNumber);
193
+ const reasoning = intersectReasoning(resolved);
194
+ const modalities = intersectModalities(resolved);
195
+ return {
196
+ provider,
197
+ id: model,
198
+ name: definition.name ?? model,
199
+ ...definition.description === undefined ? {} : { description: definition.description },
200
+ ...contextWindows.length > 0 ? { context: { contextWindow: Math.min(...contextWindows) } } : {},
201
+ ...maxTokens.length > 0 ? { defaultMaxTokens: Math.min(...maxTokens) } : {},
202
+ ...reasoning === undefined ? {} : { reasoning },
203
+ ...modalities === undefined ? {} : { inputModalities: modalities },
204
+ };
205
+ }
206
+ async *stream(options) {
207
+ const definition = (await this.pools()).get(poolKey(options.provider, options.model));
208
+ if (definition === undefined)
209
+ throw new LlmError(`unknown pool model "${options.model}"`, 'NO_ADAPTER');
210
+ const members = await this.concrete(definition.members);
211
+ const candidates = await this.select(options.model, members, options.sessionId);
212
+ if (candidates.length === 0)
213
+ throw this.exhausted(options.model, members);
214
+ let lastError;
215
+ for (const member of candidates) {
216
+ const adapter = this.options.adapters[member.provider];
217
+ if (adapter === undefined)
218
+ continue;
219
+ const iterator = adapter.streamAccount({ ...options, provider: member.provider, model: member.model }, member.account)[Symbol.asyncIterator]();
220
+ let first;
221
+ try {
222
+ first = await iterator.next();
223
+ if (first.done === true) {
224
+ throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
225
+ }
226
+ }
227
+ catch (error) {
228
+ const classification = classifyPoolFailure(error, member.provider);
229
+ if (classification.action === 'throw')
230
+ throw error;
231
+ if ('cooldownMs' in classification) {
232
+ this.options.health.markUnavailable(classification.scope === 'account'
233
+ ? accountKey(member.provider, member.account)
234
+ : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
235
+ // A quota failure invalidates the cached usage snapshot so the NEXT
236
+ // selection re-polls instead of trusting minutes-old percentages.
237
+ // Transient/auth failures say nothing about quota — keep the cache.
238
+ if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === 'RATE_LIMIT') {
239
+ this.options.usage.invalidate(member.provider, member.account);
240
+ }
241
+ }
242
+ this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output`
243
+ + ` (${error instanceof Error ? error.message : String(error)}); trying the next member`);
244
+ lastError = error;
245
+ continue;
246
+ }
247
+ this.remember(options.model, options.sessionId, member);
248
+ // Past the first chunk there is no clean attempt boundary: whatever
249
+ // the member does next (including failing) reaches the caller as-is.
250
+ // The finally closes the member stream when the CALLER walks away
251
+ // early (break / .return()) — manual iteration does not propagate
252
+ // closure the way `yield*` would, and a half-consumed member stream
253
+ // must not linger holding its connection.
254
+ try {
255
+ yield first.value;
256
+ for (let next = await iterator.next(); next.done !== true; next = await iterator.next()) {
257
+ yield next.value;
258
+ }
259
+ }
260
+ finally {
261
+ try {
262
+ await iterator.return?.();
263
+ }
264
+ catch {
265
+ // Closing a half-consumed member stream must not mask the outcome.
266
+ }
267
+ }
268
+ return;
269
+ }
270
+ throw this.exhausted(options.model, members, lastError);
271
+ }
272
+ /**
273
+ * Order the candidates for one request. Health filters both strategies;
274
+ * `quota_aware` then ranks by urgency (members without telemetry, e.g.
275
+ * copilot, score zero and sink to the bottom of their class), while
276
+ * quota-exhausted members stay as a last-resort tail in pool order. The
277
+ * sticky member keeps its lead unless a challenger out-scores it by
278
+ * `switchMargin`.
279
+ */
280
+ async select(poolId, members, sessionId) {
281
+ const usable = members.filter(member => this.options.adapters[member.provider] !== undefined
282
+ && this.options.health.isMemberAvailable(member.provider, member.account, member.model));
283
+ if (usable.length === 0)
284
+ return [];
285
+ const stickyMember = sessionId === undefined
286
+ ? undefined
287
+ : usable.find(member => memberKey(member.provider, member.account, member.model) === this.sticky.get(stickyKey(poolId, sessionId)));
288
+ if (this.options.strategy === 'priority') {
289
+ return stickyMember === undefined
290
+ ? usable
291
+ : [stickyMember, ...usable.filter(member => member !== stickyMember)];
292
+ }
293
+ const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
294
+ const scored = usable.filter(member => quotas.get(member)?.available === true);
295
+ const quotaFull = usable.filter(member => quotas.get(member)?.available === false);
296
+ scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
297
+ if (stickyMember !== undefined && scored.includes(stickyMember)) {
298
+ const best = scored[0];
299
+ const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
300
+ const bestUrgency = quotas.get(best)?.urgency ?? 0;
301
+ if (best === stickyMember || bestUrgency <= stickyUrgency * this.options.switchMargin) {
302
+ // Sticky holds (no challenger beats it by the margin): lead with it.
303
+ scored.splice(scored.indexOf(stickyMember), 1);
304
+ scored.unshift(stickyMember);
305
+ }
306
+ }
307
+ return [...scored, ...quotaFull];
308
+ }
309
+ /** Pin the serving member to the session (with bounded memory). */
310
+ remember(poolId, sessionId, member) {
311
+ if (sessionId === undefined)
312
+ return;
313
+ const key = stickyKey(poolId, sessionId);
314
+ this.sticky.delete(key);
315
+ if (this.sticky.size >= STICKY_SESSION_LIMIT) {
316
+ const oldest = this.sticky.keys().next();
317
+ if (oldest.done !== true)
318
+ this.sticky.delete(oldest.value);
319
+ }
320
+ this.sticky.set(key, memberKey(member.provider, member.account, member.model));
321
+ }
322
+ /**
323
+ * The error for an exhausted pool, carrying the earliest recovery hint of
324
+ * THIS pool's members (the health registry is shared across pools, so the
325
+ * hint is scoped to the keys this pool can actually recover through).
326
+ */
327
+ exhausted(model, pool, cause) {
328
+ const keys = new Set();
329
+ for (const member of pool) {
330
+ keys.add(memberKey(member.provider, member.account, member.model));
331
+ keys.add(accountKey(member.provider, member.account));
332
+ }
333
+ const recovery = this.options.health.earliestRecovery(keys);
334
+ const retryAfterMs = recovery === undefined ? undefined : Math.max(recovery - Date.now(), 1);
335
+ return new LlmError(`pool "${model}" exhausted: every member is unavailable or failed`, 'RATE_LIMIT', {
336
+ ...retryAfterMs === undefined ? {} : { providerRetryAfterMs: retryAfterMs },
337
+ ...cause === undefined ? {} : { cause },
338
+ });
339
+ }
340
+ }
341
+ function stickyKey(poolId, sessionId) {
342
+ return `${String(sessionId)}|${poolId}`;
343
+ }
344
+ function isNumber(value) {
345
+ return value !== undefined;
346
+ }
347
+ /** Reasoning efforts every member supports (id intersection, first member's order). */
348
+ function intersectReasoning(resolved) {
349
+ const [first, ...rest] = resolved;
350
+ if (first?.reasoning === undefined)
351
+ return undefined;
352
+ const efforts = first.reasoning.efforts.filter(effort => rest.every(info => info.reasoning?.efforts.some(other => other.id === effort.id) === true));
353
+ if (efforts.length === 0)
354
+ return undefined;
355
+ const defaultEffort = first.reasoning.defaultEffort !== undefined
356
+ && efforts.some(effort => effort.id === first.reasoning?.defaultEffort)
357
+ ? first.reasoning.defaultEffort
358
+ : undefined;
359
+ return { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } };
360
+ }
361
+ /** Modalities all members accept; undefined when any member leaves it unknown. */
362
+ function intersectModalities(resolved) {
363
+ const [first, ...rest] = resolved;
364
+ if (first?.inputModalities === undefined)
365
+ return undefined;
366
+ const modalities = first.inputModalities.filter(modality => rest.every(info => info.inputModalities?.includes(modality) === true));
367
+ // An empty intersection would declare negative capability ("accepts
368
+ // nothing"); report unknown instead — the serving member enforces its own
369
+ // limits at request time.
370
+ return modalities.length === 0 ? undefined : modalities;
371
+ }
@@ -16,7 +16,7 @@ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
16
16
  import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
17
17
  import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
18
18
  import type { CodexSession, GrokSession } from '../auth/store.js';
19
- import { TokenManager } from '../providers/common.js';
19
+ import { AccountTokenManager } from '../providers/accounts.js';
20
20
  import type { FetchFn } from '../providers/common.js';
21
21
  /** Endpoint the codex generation request is posted to. */
22
22
  export declare const IMAGE_GENERATE_URL = "https://chatgpt.com/backend-api/codex/images/generations";
@@ -29,9 +29,9 @@ export declare const GROK_IMAGE_GENERATE_MODEL = "grok-imagine-image-2.0";
29
29
  /** Dependencies of the `image_generate` tool. */
30
30
  export interface ImageGenerateToolOptions {
31
31
  /** Codex session source; the default preferred provider (`provider: 'gpt'`). */
32
- codexTokens?: TokenManager<CodexSession>;
32
+ codexTokens?: AccountTokenManager<CodexSession>;
33
33
  /** Grok session source; preferred when the call passes `provider: 'grok'`. */
34
- grokTokens?: TokenManager<GrokSession>;
34
+ grokTokens?: AccountTokenManager<GrokSession>;
35
35
  /** Fetch implementation (injectable for tests). */
36
36
  fetchFn?: FetchFn;
37
37
  /** Directory override for saved images (defaults under the harness home). */
@@ -17,7 +17,9 @@ 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
19
  import { defineTool } from '@deepseek-ai/dsh-tools';
20
- import { httpLlmError, TokenManager } from '../providers/common.js';
20
+ import { httpLlmError } from '../providers/common.js';
21
+ import { AccountTokenManager } from '../providers/accounts.js';
22
+ import { proxiedFetch } from '../http.js';
21
23
  /** Endpoint the codex generation request is posted to. */
22
24
  export const IMAGE_GENERATE_URL = 'https://chatgpt.com/backend-api/codex/images/generations';
23
25
  /** The image model the codex subscription endpoint serves. */
@@ -241,7 +243,7 @@ export function createImageGenerateTool(options) {
241
243
  content: result.content.filter(block => block.type === 'text'),
242
244
  }),
243
245
  async execute(args, exec) {
244
- const fetchFn = options.fetchFn ?? fetch;
246
+ const fetchFn = options.fetchFn ?? proxiedFetch;
245
247
  // Provider selection: the preferred provider (default gpt) when logged
246
248
  // in, the other one as the fallback. A configured-but-logged-out manager
247
249
  // still resolves through `session()` below so the standard log-in hint
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
12
12
  import type { GrokSession } from '../auth/store.js';
13
- import { TokenManager } from '../providers/common.js';
13
+ import { AccountTokenManager } from '../providers/accounts.js';
14
14
  import type { FetchFn } from '../providers/common.js';
15
15
  /** Endpoint the generation request is posted to. */
16
16
  export declare const VIDEO_GENERATE_URL = "https://api.x.ai/v1/videos/generations";
@@ -25,7 +25,7 @@ export declare const DEFAULT_MAX_WAIT_MS: number;
25
25
  /** Dependencies of the `video_generate` tool. */
26
26
  export interface VideoGenerateToolOptions {
27
27
  /** Grok session source; a missing session throws the log-in hint. */
28
- tokens: TokenManager<GrokSession>;
28
+ tokens: AccountTokenManager<GrokSession>;
29
29
  /** Fetch implementation (injectable for tests). */
30
30
  fetchFn?: FetchFn;
31
31
  /** Directory override for saved videos (defaults under the harness home). */
@@ -12,7 +12,9 @@ import { mkdir, writeFile } from 'node:fs/promises';
12
12
  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
- import { httpLlmError, TokenManager } from '../providers/common.js';
15
+ import { httpLlmError } from '../providers/common.js';
16
+ import { AccountTokenManager } from '../providers/accounts.js';
17
+ import { proxiedFetch } from '../http.js';
16
18
  /** Endpoint the generation request is posted to. */
17
19
  export const VIDEO_GENERATE_URL = 'https://api.x.ai/v1/videos/generations';
18
20
  /** The video model the grok subscription endpoint serves. */
@@ -197,7 +199,7 @@ export function createVideoGenerateTool(options) {
197
199
  async execute(args, exec) {
198
200
  const body = buildVideoGenerateBody(args);
199
201
  const session = await options.tokens.session();
200
- const fetchFn = options.fetchFn ?? fetch;
202
+ const fetchFn = options.fetchFn ?? proxiedFetch;
201
203
  const headers = {
202
204
  'authorization': `Bearer ${session.accessToken}`,
203
205
  'accept': 'application/json',
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
8
8
  import type { GrokSession } from '../auth/store.js';
9
- import { TokenManager } from '../providers/common.js';
9
+ import { AccountTokenManager } from '../providers/accounts.js';
10
10
  import type { FetchFn } from '../providers/common.js';
11
11
  /** Endpoint the search request is posted to. */
12
12
  export declare const X_SEARCH_URL = "https://api.x.ai/v1/responses";
@@ -15,7 +15,7 @@ export declare const X_SEARCH_MODEL = "grok-4";
15
15
  /** Dependencies of the `x_search` tool. */
16
16
  export interface XSearchToolOptions {
17
17
  /** Grok session source; a missing session throws the log-in hint. */
18
- tokens: TokenManager<GrokSession>;
18
+ tokens: AccountTokenManager<GrokSession>;
19
19
  /** Fetch implementation (injectable for tests). */
20
20
  fetchFn?: FetchFn;
21
21
  }
@@ -5,7 +5,9 @@
5
5
  * output is `{ answer, citations }`.
6
6
  */
7
7
  import { defineTool } from '@deepseek-ai/dsh-tools';
8
- import { httpLlmError, TokenManager } from '../providers/common.js';
8
+ import { httpLlmError } from '../providers/common.js';
9
+ import { AccountTokenManager } from '../providers/accounts.js';
10
+ import { proxiedFetch } from '../http.js';
9
11
  /** Endpoint the search request is posted to. */
10
12
  export const X_SEARCH_URL = 'https://api.x.ai/v1/responses';
11
13
  /** Grok model the search runs on (a catalog model of the grok provider). */
@@ -172,7 +174,7 @@ export function createXSearchTool(options) {
172
174
  async execute(args, exec) {
173
175
  const request = buildXSearchRequest(args);
174
176
  const session = await options.tokens.session();
175
- const response = await (options.fetchFn ?? fetch)(X_SEARCH_URL, {
177
+ const response = await (options.fetchFn ?? proxiedFetch)(X_SEARCH_URL, {
176
178
  method: 'POST',
177
179
  headers: {
178
180
  'authorization': `Bearer ${session.accessToken}`,
@@ -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[];