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.
- package/README.md +42 -1
- package/README.zh.md +42 -1
- package/lib/auth/device-flow.d.ts +0 -9
- package/lib/auth/device-flow.js +2 -1
- package/lib/auth/rpc.d.ts +44 -13
- package/lib/auth/rpc.js +127 -9
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +26 -3
- package/lib/client/SubscriptionsSection.js +263 -67
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +82 -10
- package/lib/client/locales.js +82 -10
- package/lib/client.js +837 -223
- 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 +21 -0
- package/lib/index.js +1938 -208
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +97 -16
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +121 -21
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +23 -4
- package/lib/providers/copilot.js +99 -19
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +106 -19
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +148 -0
- package/lib/providers/pool-usage.d.ts +57 -0
- package/lib/providers/pool-usage.js +130 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +4 -2
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +4 -2
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +4 -2
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- 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 {
|
|
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?:
|
|
32
|
+
codexTokens?: AccountTokenManager<CodexSession>;
|
|
33
33
|
/** Grok session source; preferred when the call passes `provider: 'grok'`. */
|
|
34
|
-
grokTokens?:
|
|
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
|
|
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 ??
|
|
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 {
|
|
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:
|
|
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
|
|
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 ??
|
|
202
|
+
const fetchFn = options.fetchFn ?? proxiedFetch;
|
|
201
203
|
const headers = {
|
|
202
204
|
'authorization': `Bearer ${session.accessToken}`,
|
|
203
205
|
'accept': 'application/json',
|
package/lib/tools/x-search.d.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
18
|
+
tokens: AccountTokenManager<GrokSession>;
|
|
19
19
|
/** Fetch implementation (injectable for tests). */
|
|
20
20
|
fetchFn?: FetchFn;
|
|
21
21
|
}
|
package/lib/tools/x-search.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* output is `{ answer, citations }`.
|
|
6
6
|
*/
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
-
import { httpLlmError
|
|
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 ??
|
|
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[];
|