dsh-subscriptions 0.2.0 → 0.2.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 (105) hide show
  1. package/README.md +220 -180
  2. package/lib/auth/claude-code-creds.js +31 -9
  3. package/lib/auth/device-flow.js +16 -9
  4. package/lib/auth/oauth-flow.js +13 -10
  5. package/lib/auth/rpc.d.ts +8 -4
  6. package/lib/auth/rpc.js +89 -25
  7. package/lib/auth/store.js +80 -31
  8. package/lib/client/CursorCard.d.ts +1 -1
  9. package/lib/client/CursorCard.js +50 -16
  10. package/lib/client/ExternalUsageCards.d.ts +1 -1
  11. package/lib/client/ExternalUsageCards.js +37 -16
  12. package/lib/client/ImageGallery.d.ts +2 -2
  13. package/lib/client/ImageGallery.js +92 -33
  14. package/lib/client/ImageGenerateToolview.js +50 -14
  15. package/lib/client/OllamaCard.d.ts +1 -1
  16. package/lib/client/OllamaCard.js +26 -8
  17. package/lib/client/OllamaSettingsManager.js +113 -30
  18. package/lib/client/ProviderAccountManager.d.ts +1 -1
  19. package/lib/client/ProviderAccountManager.js +164 -49
  20. package/lib/client/ProviderModelEditor.js +137 -34
  21. package/lib/client/SpeedSelect.d.ts +62 -14
  22. package/lib/client/SpeedSelect.js +261 -49
  23. package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
  24. package/lib/client/SubscriptionUsageBadge.js +300 -112
  25. package/lib/client/SubscriptionsSection.d.ts +2 -0
  26. package/lib/client/SubscriptionsSection.js +265 -85
  27. package/lib/client/UsageBadgeDisplaySetting.js +35 -12
  28. package/lib/client/UsageMeter.d.ts +1 -1
  29. package/lib/client/UsageMeter.js +62 -24
  30. package/lib/client/VideoGenerateToolview.js +69 -17
  31. package/lib/client/account-preferences.d.ts +8 -0
  32. package/lib/client/account-preferences.js +20 -5
  33. package/lib/client/host-contract.d.ts +11 -2
  34. package/lib/client/host-contract.js +20 -3
  35. package/lib/client/index.d.ts +5 -5
  36. package/lib/client/index.js +34 -12
  37. package/lib/client/locales.d.ts +22 -0
  38. package/lib/client/locales.js +73 -51
  39. package/lib/client/model-filter.d.ts +37 -0
  40. package/lib/client/model-filter.js +45 -0
  41. package/lib/client/ollama-draft.js +21 -12
  42. package/lib/client/reset-action.js +6 -4
  43. package/lib/client/reset-credits-view.d.ts +1 -1
  44. package/lib/client/reset-credits-view.js +66 -17
  45. package/lib/client/subscription-card-styles.js +94 -24
  46. package/lib/client/usage-color-preferences.js +4 -2
  47. package/lib/client/usage-pace.js +30 -12
  48. package/lib/client/usage-window-filter.d.ts +7 -0
  49. package/lib/client/usage-window-filter.js +12 -0
  50. package/lib/client.js +480 -136
  51. package/lib/client.js.map +1 -1
  52. package/lib/http.js +1 -1
  53. package/lib/index.d.ts +3 -3
  54. package/lib/index.js +167 -76
  55. package/lib/model-defaults.js +4 -2
  56. package/lib/provider-settings.d.ts +17 -0
  57. package/lib/provider-settings.js +79 -16
  58. package/lib/providers/account-preferences.js +51 -22
  59. package/lib/providers/accounts.js +15 -9
  60. package/lib/providers/antigravity-oauth-client.js +2 -2
  61. package/lib/providers/antigravity.js +141 -68
  62. package/lib/providers/catalog-store.js +120 -41
  63. package/lib/providers/claude-cli-version.js +3 -3
  64. package/lib/providers/claude.d.ts +9 -2
  65. package/lib/providers/claude.js +153 -90
  66. package/lib/providers/codex-client-version.js +3 -3
  67. package/lib/providers/codex-search.js +40 -13
  68. package/lib/providers/codex.js +196 -112
  69. package/lib/providers/common.js +71 -39
  70. package/lib/providers/copilot.js +143 -82
  71. package/lib/providers/cursor-adapter.js +42 -18
  72. package/lib/providers/cursor-auth.d.ts +1 -1
  73. package/lib/providers/cursor-auth.js +69 -24
  74. package/lib/providers/cursor-model-settings.js +14 -5
  75. package/lib/providers/cursor-usage.js +27 -13
  76. package/lib/providers/external-usage-controller.js +14 -5
  77. package/lib/providers/external-usage.js +59 -23
  78. package/lib/providers/grok.js +152 -89
  79. package/lib/providers/image-pool.js +29 -11
  80. package/lib/providers/minimax-usage.js +31 -11
  81. package/lib/providers/npm-cli-version.js +10 -3
  82. package/lib/providers/ollama-settings.js +24 -8
  83. package/lib/providers/ollama-web.js +29 -11
  84. package/lib/providers/ollama.js +129 -66
  85. package/lib/providers/pool-family.js +11 -3
  86. package/lib/providers/pool-health.js +28 -8
  87. package/lib/providers/pool-usage.js +16 -9
  88. package/lib/providers/pool.d.ts +8 -0
  89. package/lib/providers/pool.js +81 -42
  90. package/lib/providers/rate-limit.js +8 -2
  91. package/lib/providers/replay.js +40 -17
  92. package/lib/providers/reset-redemption.js +18 -6
  93. package/lib/tools/image-generate.js +123 -65
  94. package/lib/tools/registration.js +5 -2
  95. package/lib/tools/video-generate.js +51 -31
  96. package/lib/tools/x-search.js +50 -22
  97. package/lib/translate/anthropic.js +85 -37
  98. package/lib/translate/antigravity-schema.js +71 -28
  99. package/lib/translate/antigravity-thinking.js +22 -9
  100. package/lib/translate/antigravity.js +151 -52
  101. package/lib/translate/chat-completions.js +67 -23
  102. package/lib/translate/resolved.js +55 -21
  103. package/lib/translate/responses.js +69 -29
  104. package/lib/translate/sse.js +4 -1
  105. package/package.json +27 -4
@@ -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 ? {} : { description: definition.description },
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
- + ` (${error instanceof Error ? error.message : String(error)}); excluding it`);
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.map(info => info.context?.contextWindow).filter(isNumber);
190
- const maxTokens = resolved.map(info => info.defaultMaxTokens).filter(isNumber);
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 ? {} : { description: definition.description },
198
- ...contextWindows.length > 0 ? { context: { contextWindow: Math.min(...contextWindows) } } : {},
199
- ...maxTokens.length > 0 ? { defaultMaxTokens: Math.min(...maxTokens) } : {},
200
- ...reasoning === undefined ? {} : { reasoning },
201
- ...modalities === undefined ? {} : { inputModalities: modalities },
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.exhausted(options.model, members);
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 || classification.reason === 'RATE_LIMIT') {
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
- + ` (${error instanceof Error ? error.message : String(error)}); trying the next member`);
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.exhausted(options.model, members, failures);
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
- && this.options.health.isMemberAvailable(member.provider, member.account, member.model));
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) === this.sticky.get(stickyKey(poolId, sessionId)));
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 || bestUrgency <= stickyUrgency * this.options.switchMargin) {
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 && retryAfterMs !== failure.failure.providerRetryAfterMs) {
382
+ if (retryAfterMs !== undefined &&
383
+ retryAfterMs !== failure.failure.providerRetryAfterMs) {
356
384
  Object.defineProperty(failure, 'failure', {
357
- value: Object.freeze({ ...failure.failure, providerRetryAfterMs: retryAfterMs }),
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 = [...attemptedCodes, ...coolingReasons.filter(code => !attemptedCodes.includes(code))];
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
- ?? coolingReasons.filter(code => !isQuotaFailure(code)).at(-1);
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 ? {} : { providerRetryAfterMs: retryAfterMs },
384
- ...cause === undefined ? {} : { cause },
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 ? 'member unavailable' : `${code}: ${description}`;
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) === true));
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
- && efforts.some(effort => effort.id === first.reasoning?.defaultEffort)
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 = { h: 3_600_000, m: 60_000, s: 1_000, ms: 1 };
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 && (earliest === undefined || candidate < earliest))
199
+ if (candidate !== undefined &&
200
+ (earliest === undefined || candidate < earliest))
195
201
  earliest = candidate;
196
202
  };
197
203
  if (Array.isArray(value)) {
@@ -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])).digest('hex');
7
- const messages = options.messages.map(message => {
8
- if (message.role !== 'assistant'
9
- || message.source?.replayState === undefined)
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 || typeof source.replayState !== 'object'
13
- || !('response' in source.replayState))
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 && typeof response === 'object'
18
- && 'kind' in response && response.kind === REPLAY_KIND) {
19
- if ('version' in response && response.version === 1
20
- && 'key' in response && response.key === key && 'state' in response) {
21
- return { ...message, source: {
22
- ...source, provider: options.provider, model,
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, model, messages,
52
+ ...options,
53
+ model,
54
+ messages,
41
55
  }, account)) {
42
56
  if (chunk.type === 'finish' && chunk.replayState !== undefined) {
43
- yield { ...chunk, replayState: { ...chunk.replayState, response: {
44
- kind: REPLAY_KIND, version: 1, key, state: chunk.replayState.response,
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 || !Number.isFinite(weekly.usedPercent) || weekly.usedPercent < 80
39
- || weekly.resetsAt === undefined || weekly.resetsAt <= Date.now()) {
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?.filter(c => c.id && (c.expiresAt === undefined || c.expiresAt > Date.now()))
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 { ticket, expiresAt, weeklyUsedPercent, ...(credit.expiresAt === undefined ? {} : { creditExpiresAt: credit.expiresAt }) };
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 && (c.expiresAt === undefined || c.expiresAt > Date.now())))
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);