dsh-subscriptions 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -180
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.js +74 -23
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +1 -1
- package/lib/client/OllamaCard.js +26 -8
- package/lib/client/OllamaSettingsManager.js +113 -30
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +107 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
- package/lib/client/SubscriptionUsageBadge.js +300 -112
- package/lib/client/SubscriptionsSection.js +258 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +62 -24
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +10 -0
- package/lib/client/locales.js +61 -51
- package/lib/client/ollama-draft.js +21 -12
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.js +30 -12
- package/lib/client.js +287 -47
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +118 -64
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +12 -0
- package/lib/provider-settings.js +58 -16
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +9 -2
- package/lib/providers/claude.js +153 -90
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.js +71 -39
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.js +14 -5
- package/lib/providers/external-usage.js +59 -23
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.js +24 -8
- package/lib/providers/ollama-web.js +29 -11
- package/lib/providers/ollama.js +129 -66
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +141 -50
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +26 -4
package/lib/providers/pool.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
|
|
11
11
|
import { poolKey } from './pool-family.js';
|
|
12
|
-
import { accountKey, classifyPoolFailure, memberKey, PoolHealthRegistry } from './pool-health.js';
|
|
12
|
+
import { accountKey, classifyPoolFailure, memberKey, PoolHealthRegistry, } from './pool-health.js';
|
|
13
13
|
import { streamAccountWithReplay } from './replay.js';
|
|
14
14
|
/** Bound on sticky-session memory; oldest entries evict past it. */
|
|
15
15
|
const STICKY_SESSION_LIMIT = 1000;
|
|
@@ -57,7 +57,7 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
57
57
|
usable(pools) {
|
|
58
58
|
const result = new Map(pools);
|
|
59
59
|
for (const [id, definition] of [...result]) {
|
|
60
|
-
const kept = definition.members.filter(member => this.options.adapters[member.provider] !== undefined);
|
|
60
|
+
const kept = definition.members.filter((member) => this.options.adapters[member.provider] !== undefined);
|
|
61
61
|
if (kept.length === 0)
|
|
62
62
|
result.delete(id);
|
|
63
63
|
else if (kept.length < definition.members.length)
|
|
@@ -118,7 +118,9 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
118
118
|
provider,
|
|
119
119
|
id,
|
|
120
120
|
name: definition.name ?? id,
|
|
121
|
-
...definition.description === undefined
|
|
121
|
+
...(definition.description === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { description: definition.description }),
|
|
122
124
|
});
|
|
123
125
|
}
|
|
124
126
|
return models;
|
|
@@ -139,7 +141,7 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
139
141
|
const seen = new Set();
|
|
140
142
|
const resolved = [];
|
|
141
143
|
for (const member of members) {
|
|
142
|
-
const requested = member.account ?? await this.options.defaultAccount(member.provider);
|
|
144
|
+
const requested = member.account ?? (await this.options.defaultAccount(member.provider));
|
|
143
145
|
if (requested === undefined)
|
|
144
146
|
continue;
|
|
145
147
|
const account = await (this.options.resolveAccount?.(member.provider, requested) ?? Promise.resolve(requested));
|
|
@@ -177,28 +179,38 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
177
179
|
}
|
|
178
180
|
catch (error) {
|
|
179
181
|
lastFailure = error;
|
|
180
|
-
this.warnOnce(`pool "${model}": member ${memberLabel(member)} failed to resolve`
|
|
181
|
-
|
|
182
|
+
this.warnOnce(`pool "${model}": member ${memberLabel(member)} failed to resolve` +
|
|
183
|
+
` (${error instanceof Error ? error.message : String(error)}); excluding it`);
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
if (resolved.length === 0) {
|
|
185
187
|
throw new LlmError(`pool "${model}" has no usable member`, 'NO_ADAPTER', {
|
|
186
|
-
...lastFailure === undefined ? {} : { cause: lastFailure },
|
|
188
|
+
...(lastFailure === undefined ? {} : { cause: lastFailure }),
|
|
187
189
|
});
|
|
188
190
|
}
|
|
189
|
-
const contextWindows = resolved
|
|
190
|
-
|
|
191
|
+
const contextWindows = resolved
|
|
192
|
+
.map((info) => info.context?.contextWindow)
|
|
193
|
+
.filter(isNumber);
|
|
194
|
+
const maxTokens = resolved
|
|
195
|
+
.map((info) => info.defaultMaxTokens)
|
|
196
|
+
.filter(isNumber);
|
|
191
197
|
const reasoning = intersectReasoning(resolved);
|
|
192
198
|
const modalities = intersectModalities(resolved);
|
|
193
199
|
return {
|
|
194
200
|
provider,
|
|
195
201
|
id: model,
|
|
196
202
|
name: definition.name ?? model,
|
|
197
|
-
...definition.description === undefined
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
...
|
|
201
|
-
|
|
203
|
+
...(definition.description === undefined
|
|
204
|
+
? {}
|
|
205
|
+
: { description: definition.description }),
|
|
206
|
+
...(contextWindows.length > 0
|
|
207
|
+
? { context: { contextWindow: Math.min(...contextWindows) } }
|
|
208
|
+
: {}),
|
|
209
|
+
...(maxTokens.length > 0
|
|
210
|
+
? { defaultMaxTokens: Math.min(...maxTokens) }
|
|
211
|
+
: {}),
|
|
212
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
213
|
+
...(modalities === undefined ? {} : { inputModalities: modalities }),
|
|
202
214
|
};
|
|
203
215
|
}
|
|
204
216
|
/**
|
|
@@ -214,7 +226,7 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
214
226
|
const members = await this.concrete(definition.members);
|
|
215
227
|
const candidates = await this.select(options.model, members, options.sessionId);
|
|
216
228
|
if (candidates.length === 0)
|
|
217
|
-
throw this.
|
|
229
|
+
throw this.exhaustedWithWarn(options.model, members);
|
|
218
230
|
const failures = [];
|
|
219
231
|
for (const member of candidates) {
|
|
220
232
|
const adapter = this.options.adapters[member.provider];
|
|
@@ -239,12 +251,13 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
239
251
|
// A quota failure invalidates the cached usage snapshot so the NEXT
|
|
240
252
|
// selection re-polls instead of trusting minutes-old percentages.
|
|
241
253
|
// Transient/auth failures say nothing about quota — keep the cache.
|
|
242
|
-
if (classification.reason === QUOTA_EXCEEDED_CODE ||
|
|
254
|
+
if (classification.reason === QUOTA_EXCEEDED_CODE ||
|
|
255
|
+
classification.reason === 'RATE_LIMIT') {
|
|
243
256
|
this.options.usage.invalidate(member.provider, member.account);
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
|
-
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output`
|
|
247
|
-
|
|
259
|
+
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output` +
|
|
260
|
+
` (${error instanceof Error ? error.message : String(error)}); trying the next member`);
|
|
248
261
|
failures.push(error);
|
|
249
262
|
continue;
|
|
250
263
|
}
|
|
@@ -271,7 +284,7 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
271
284
|
}
|
|
272
285
|
return;
|
|
273
286
|
}
|
|
274
|
-
throw this.
|
|
287
|
+
throw this.exhaustedWithWarn(options.model, members, failures);
|
|
275
288
|
}
|
|
276
289
|
/**
|
|
277
290
|
* Order the candidates for one request. Health filters both strategies;
|
|
@@ -282,27 +295,29 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
282
295
|
* `switchMargin`.
|
|
283
296
|
*/
|
|
284
297
|
async select(poolId, members, sessionId) {
|
|
285
|
-
const usable = members.filter(member => this.options.adapters[member.provider] !== undefined
|
|
286
|
-
|
|
298
|
+
const usable = members.filter((member) => this.options.adapters[member.provider] !== undefined &&
|
|
299
|
+
this.options.health.isMemberAvailable(member.provider, member.account, member.model));
|
|
287
300
|
if (usable.length === 0)
|
|
288
301
|
return [];
|
|
289
302
|
const stickyMember = sessionId === undefined
|
|
290
303
|
? undefined
|
|
291
|
-
: usable.find(member => memberKey(member.provider, member.account, member.model) ===
|
|
304
|
+
: usable.find((member) => memberKey(member.provider, member.account, member.model) ===
|
|
305
|
+
this.sticky.get(stickyKey(poolId, sessionId)));
|
|
292
306
|
if (this.options.strategy === 'priority') {
|
|
293
307
|
return stickyMember === undefined
|
|
294
308
|
? usable
|
|
295
|
-
: [stickyMember, ...usable.filter(member => member !== stickyMember)];
|
|
309
|
+
: [stickyMember, ...usable.filter((member) => member !== stickyMember)];
|
|
296
310
|
}
|
|
297
311
|
const quotas = new Map(await Promise.all(usable.map(async (member) => [member, await this.options.usage.quotaFor(member)])));
|
|
298
|
-
const scored = usable.filter(member => quotas.get(member)?.available === true);
|
|
299
|
-
const quotaFull = usable.filter(member => quotas.get(member)?.available === false);
|
|
312
|
+
const scored = usable.filter((member) => quotas.get(member)?.available === true);
|
|
313
|
+
const quotaFull = usable.filter((member) => quotas.get(member)?.available === false);
|
|
300
314
|
scored.sort((a, b) => (quotas.get(b)?.urgency ?? 0) - (quotas.get(a)?.urgency ?? 0));
|
|
301
315
|
if (stickyMember !== undefined && scored.includes(stickyMember)) {
|
|
302
316
|
const best = scored[0];
|
|
303
317
|
const stickyUrgency = quotas.get(stickyMember)?.urgency ?? 0;
|
|
304
318
|
const bestUrgency = quotas.get(best)?.urgency ?? 0;
|
|
305
|
-
if (best === stickyMember ||
|
|
319
|
+
if (best === stickyMember ||
|
|
320
|
+
bestUrgency <= stickyUrgency * this.options.switchMargin) {
|
|
306
321
|
// Sticky holds (no challenger beats it by the margin): lead with it.
|
|
307
322
|
scored.splice(scored.indexOf(stickyMember), 1);
|
|
308
323
|
scored.unshift(stickyMember);
|
|
@@ -323,6 +338,18 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
323
338
|
}
|
|
324
339
|
this.sticky.set(key, memberKey(member.provider, member.account, member.model));
|
|
325
340
|
}
|
|
341
|
+
/**
|
|
342
|
+
* Build the terminal exhaustion error and log it. Per-member failovers are
|
|
343
|
+
* already warned above; without this line dsh.log shows the failover chain
|
|
344
|
+
* but not the exhaustion that ends it. The message carries only the
|
|
345
|
+
* LlmError's own sanitized text and code — raw provider payloads never
|
|
346
|
+
* reach here (see the note in {@link exhausted}).
|
|
347
|
+
*/
|
|
348
|
+
exhaustedWithWarn(model, pool, failures = []) {
|
|
349
|
+
const error = this.exhausted(model, pool, failures);
|
|
350
|
+
this.options.onWarn(`pool "${model}": exhausted (${error.code}: ${error.message})`);
|
|
351
|
+
return error;
|
|
352
|
+
}
|
|
326
353
|
/** Report attempt failures or this pool's active cooldowns, never unrelated health records. */
|
|
327
354
|
exhausted(model, pool, failures = []) {
|
|
328
355
|
if (pool.length === 0) {
|
|
@@ -336,7 +363,7 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
336
363
|
const recovery = this.options.health.earliestRecovery(keys);
|
|
337
364
|
const retryAfterMs = recovery === undefined ? undefined : Math.max(recovery - Date.now(), 1);
|
|
338
365
|
const coolingReasons = this.options.health.unavailableReasons(keys);
|
|
339
|
-
const codes = [...failures.map(error => error.code), ...coolingReasons];
|
|
366
|
+
const codes = [...failures.map((error) => error.code), ...coolingReasons];
|
|
340
367
|
if (codes.length === 0)
|
|
341
368
|
return new LlmError(`pool "${model}" has no usable member`, 'NO_ADAPTER');
|
|
342
369
|
const quotaOnly = codes.every(isQuotaFailure);
|
|
@@ -352,15 +379,22 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
352
379
|
// Keep the error object itself. LlmError freezes its facts snapshot, so
|
|
353
380
|
// replace that snapshot to correct only providerRetryAfterMs for pool
|
|
354
381
|
// routing; this is not permission to rewrite other frozen failure facts.
|
|
355
|
-
if (retryAfterMs !== undefined &&
|
|
382
|
+
if (retryAfterMs !== undefined &&
|
|
383
|
+
retryAfterMs !== failure.failure.providerRetryAfterMs) {
|
|
356
384
|
Object.defineProperty(failure, 'failure', {
|
|
357
|
-
value: Object.freeze({
|
|
385
|
+
value: Object.freeze({
|
|
386
|
+
...failure.failure,
|
|
387
|
+
providerRetryAfterMs: retryAfterMs,
|
|
388
|
+
}),
|
|
358
389
|
});
|
|
359
390
|
}
|
|
360
391
|
return failure;
|
|
361
392
|
}
|
|
362
|
-
const attemptedCodes = failures.map(error => error.code);
|
|
363
|
-
const details = [
|
|
393
|
+
const attemptedCodes = failures.map((error) => error.code);
|
|
394
|
+
const details = [
|
|
395
|
+
...attemptedCodes,
|
|
396
|
+
...coolingReasons.filter((code) => !attemptedCodes.includes(code)),
|
|
397
|
+
];
|
|
364
398
|
const summary = failureSummary(details);
|
|
365
399
|
const cause = failures.length === 0
|
|
366
400
|
? undefined
|
|
@@ -377,11 +411,13 @@ export class PoolAdapter extends LlmAdapter {
|
|
|
377
411
|
// merely because the last member hit its quota. No new aggregate error code.
|
|
378
412
|
const code = quotaOnly
|
|
379
413
|
? 'RATE_LIMIT'
|
|
380
|
-
: attemptedCodes.filter(code => !isQuotaFailure(code)).at(-1)
|
|
381
|
-
|
|
414
|
+
: (attemptedCodes.filter((code) => !isQuotaFailure(code)).at(-1) ??
|
|
415
|
+
coolingReasons.filter((code) => !isQuotaFailure(code)).at(-1));
|
|
382
416
|
return new LlmError(message, code, {
|
|
383
|
-
...retryAfterMs === undefined
|
|
384
|
-
|
|
417
|
+
...(retryAfterMs === undefined
|
|
418
|
+
? {}
|
|
419
|
+
: { providerRetryAfterMs: retryAfterMs }),
|
|
420
|
+
...(cause === undefined ? {} : { cause }),
|
|
385
421
|
});
|
|
386
422
|
}
|
|
387
423
|
}
|
|
@@ -404,9 +440,11 @@ function failureSummary(codes) {
|
|
|
404
440
|
RATE_LIMIT: 'provider rate limit reached',
|
|
405
441
|
};
|
|
406
442
|
const limit = 5;
|
|
407
|
-
const shown = codes.slice(0, limit).map(code => {
|
|
443
|
+
const shown = codes.slice(0, limit).map((code) => {
|
|
408
444
|
const description = descriptions[code];
|
|
409
|
-
return description === undefined
|
|
445
|
+
return description === undefined
|
|
446
|
+
? 'member unavailable'
|
|
447
|
+
: `${code}: ${description}`;
|
|
410
448
|
});
|
|
411
449
|
if (codes.length > limit)
|
|
412
450
|
shown.push(`${codes.length - limit} more`);
|
|
@@ -423,21 +461,22 @@ function intersectReasoning(resolved) {
|
|
|
423
461
|
const [first, ...rest] = resolved;
|
|
424
462
|
if (first?.reasoning === undefined)
|
|
425
463
|
return undefined;
|
|
426
|
-
const efforts = first.reasoning.efforts.filter(effort => rest.every(info => info.reasoning?.efforts.some(other => other.id === effort.id) ===
|
|
464
|
+
const efforts = first.reasoning.efforts.filter((effort) => rest.every((info) => info.reasoning?.efforts.some((other) => other.id === effort.id) ===
|
|
465
|
+
true));
|
|
427
466
|
if (efforts.length === 0)
|
|
428
467
|
return undefined;
|
|
429
|
-
const defaultEffort = first.reasoning.defaultEffort !== undefined
|
|
430
|
-
|
|
468
|
+
const defaultEffort = first.reasoning.defaultEffort !== undefined &&
|
|
469
|
+
efforts.some((effort) => effort.id === first.reasoning?.defaultEffort)
|
|
431
470
|
? first.reasoning.defaultEffort
|
|
432
471
|
: undefined;
|
|
433
|
-
return { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } };
|
|
472
|
+
return { efforts, ...(defaultEffort === undefined ? {} : { defaultEffort }) };
|
|
434
473
|
}
|
|
435
474
|
/** Modalities all members accept; undefined when any member leaves it unknown. */
|
|
436
475
|
function intersectModalities(resolved) {
|
|
437
476
|
const [first, ...rest] = resolved;
|
|
438
477
|
if (first?.inputModalities === undefined)
|
|
439
478
|
return undefined;
|
|
440
|
-
const modalities = first.inputModalities.filter(modality => rest.every(info => info.inputModalities?.includes(modality) === true));
|
|
479
|
+
const modalities = first.inputModalities.filter((modality) => rest.every((info) => info.inputModalities?.includes(modality) === true));
|
|
441
480
|
// An empty intersection would declare negative capability ("accepts
|
|
442
481
|
// nothing"); report unknown instead — the serving member enforces its own
|
|
443
482
|
// limits at request time.
|
|
@@ -67,7 +67,12 @@ export function durationMs(text) {
|
|
|
67
67
|
// Sticky: every component must abut the previous one, so trailing or
|
|
68
68
|
// interleaved junk ("6m0s later") fails the length check below.
|
|
69
69
|
const pattern = /(\d+(?:\.\d+)?)(ms|h|m|s)/y;
|
|
70
|
-
const units = {
|
|
70
|
+
const units = {
|
|
71
|
+
h: 3_600_000,
|
|
72
|
+
m: 60_000,
|
|
73
|
+
s: 1_000,
|
|
74
|
+
ms: 1,
|
|
75
|
+
};
|
|
71
76
|
let total = 0;
|
|
72
77
|
let matched = false;
|
|
73
78
|
// A failed sticky exec resets `lastIndex` to zero, so the reached offset is
|
|
@@ -191,7 +196,8 @@ export function resetFromFields(value, keys, now, depth = MAX_BODY_DEPTH) {
|
|
|
191
196
|
return undefined;
|
|
192
197
|
let earliest;
|
|
193
198
|
const consider = (candidate) => {
|
|
194
|
-
if (candidate !== undefined &&
|
|
199
|
+
if (candidate !== undefined &&
|
|
200
|
+
(earliest === undefined || candidate < earliest))
|
|
195
201
|
earliest = candidate;
|
|
196
202
|
};
|
|
197
203
|
if (Array.isArray(value)) {
|
package/lib/providers/replay.js
CHANGED
|
@@ -3,25 +3,37 @@ const REPLAY_KIND = 'subscriptions-account-replay';
|
|
|
3
3
|
/** Bind opaque replay to the concrete account/model behind a route alias. */
|
|
4
4
|
export async function* streamAccountWithReplay(adapter, options, account, model = options.model) {
|
|
5
5
|
const key = createHash('sha256')
|
|
6
|
-
.update(JSON.stringify([options.provider, account, model]))
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
.update(JSON.stringify([options.provider, account, model]))
|
|
7
|
+
.digest('hex');
|
|
8
|
+
const messages = options.messages.map((message) => {
|
|
9
|
+
if (message.role !== 'assistant' ||
|
|
10
|
+
message.source?.replayState === undefined)
|
|
10
11
|
return message;
|
|
11
12
|
const source = message.source;
|
|
12
|
-
if (source.replayState === null ||
|
|
13
|
-
|
|
13
|
+
if (source.replayState === null ||
|
|
14
|
+
typeof source.replayState !== 'object' ||
|
|
15
|
+
!('response' in source.replayState))
|
|
14
16
|
return message;
|
|
15
17
|
const replay = source.replayState;
|
|
16
18
|
const response = replay.response;
|
|
17
|
-
if (response !== null &&
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (response !== null &&
|
|
20
|
+
typeof response === 'object' &&
|
|
21
|
+
'kind' in response &&
|
|
22
|
+
response.kind === REPLAY_KIND) {
|
|
23
|
+
if ('version' in response &&
|
|
24
|
+
response.version === 1 &&
|
|
25
|
+
'key' in response &&
|
|
26
|
+
response.key === key &&
|
|
27
|
+
'state' in response) {
|
|
28
|
+
return {
|
|
29
|
+
...message,
|
|
30
|
+
source: {
|
|
31
|
+
...source,
|
|
32
|
+
provider: options.provider,
|
|
33
|
+
model,
|
|
23
34
|
replayState: { ...replay, response: response.state },
|
|
24
|
-
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
25
37
|
}
|
|
26
38
|
const { replayState: _discarded, ...withoutReplay } = source;
|
|
27
39
|
return { ...message, source: withoutReplay };
|
|
@@ -37,12 +49,23 @@ export async function* streamAccountWithReplay(adapter, options, account, model
|
|
|
37
49
|
return { ...message, source: withoutReplay };
|
|
38
50
|
});
|
|
39
51
|
for await (const chunk of adapter.streamAccount({
|
|
40
|
-
...options,
|
|
52
|
+
...options,
|
|
53
|
+
model,
|
|
54
|
+
messages,
|
|
41
55
|
}, account)) {
|
|
42
56
|
if (chunk.type === 'finish' && chunk.replayState !== undefined) {
|
|
43
|
-
yield {
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
yield {
|
|
58
|
+
...chunk,
|
|
59
|
+
replayState: {
|
|
60
|
+
...chunk.replayState,
|
|
61
|
+
response: {
|
|
62
|
+
kind: REPLAY_KIND,
|
|
63
|
+
version: 1,
|
|
64
|
+
key,
|
|
65
|
+
state: chunk.replayState.response,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
46
69
|
}
|
|
47
70
|
else
|
|
48
71
|
yield chunk;
|
|
@@ -34,9 +34,12 @@ export class ResetRedemption {
|
|
|
34
34
|
this.canonical = canonical;
|
|
35
35
|
}
|
|
36
36
|
eligible(usage) {
|
|
37
|
-
const weekly = usage.windows?.find(w => w.kind === 'weekly' && w.scope === undefined);
|
|
38
|
-
if (!weekly ||
|
|
39
|
-
|
|
37
|
+
const weekly = usage.windows?.find((w) => w.kind === 'weekly' && w.scope === undefined);
|
|
38
|
+
if (!weekly ||
|
|
39
|
+
!Number.isFinite(weekly.usedPercent) ||
|
|
40
|
+
weekly.usedPercent < 80 ||
|
|
41
|
+
weekly.resetsAt === undefined ||
|
|
42
|
+
weekly.resetsAt <= Date.now()) {
|
|
40
43
|
throw new Error('Reset blocked: fresh weekly usage must be at least 80%.');
|
|
41
44
|
}
|
|
42
45
|
if (usage.resetCreditsError)
|
|
@@ -54,7 +57,8 @@ export class ResetRedemption {
|
|
|
54
57
|
if (this.busy.has(who) || this.uncertain.has(who))
|
|
55
58
|
throw new Error('Reset blocked: an operation is pending or its outcome is uncertain. Check Codex before retrying.');
|
|
56
59
|
const weeklyUsedPercent = this.eligible(usage);
|
|
57
|
-
const credit = usage.resetCredits
|
|
60
|
+
const credit = usage.resetCredits
|
|
61
|
+
?.filter((c) => c.id && (c.expiresAt === undefined || c.expiresAt > Date.now()))
|
|
58
62
|
.sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity))[0];
|
|
59
63
|
if (!credit?.id)
|
|
60
64
|
throw new Error('No available reset credit.');
|
|
@@ -64,7 +68,14 @@ export class ResetRedemption {
|
|
|
64
68
|
const ticket = randomUUID();
|
|
65
69
|
const expiresAt = Date.now() + 60_000;
|
|
66
70
|
this.tickets.set(ticket, { account: who, creditId: credit.id, expiresAt });
|
|
67
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
ticket,
|
|
73
|
+
expiresAt,
|
|
74
|
+
weeklyUsedPercent,
|
|
75
|
+
...(credit.expiresAt === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { creditExpiresAt: credit.expiresAt }),
|
|
78
|
+
};
|
|
68
79
|
}
|
|
69
80
|
async redeem(account, ticket, signal) {
|
|
70
81
|
const who = await this.canonical(account, signal);
|
|
@@ -82,7 +93,8 @@ export class ResetRedemption {
|
|
|
82
93
|
if (pending.expiresAt <= Date.now())
|
|
83
94
|
throw new Error('Confirmation expired. Start again.');
|
|
84
95
|
this.eligible(usage);
|
|
85
|
-
if (!usage.resetCredits?.some(c => c.id === pending.creditId &&
|
|
96
|
+
if (!usage.resetCredits?.some((c) => c.id === pending.creditId &&
|
|
97
|
+
(c.expiresAt === undefined || c.expiresAt > Date.now())))
|
|
86
98
|
throw new Error('The selected reset is no longer available.');
|
|
87
99
|
// Mark before submission: no blind retry after transport or response ambiguity.
|
|
88
100
|
this.uncertain.add(who);
|