dsh-plugin-subscriptions 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +45 -6
  2. package/README.zh.md +43 -4
  3. package/lib/auth/rpc.d.ts +36 -2
  4. package/lib/auth/rpc.js +47 -5
  5. package/lib/client/ImageGenerateToolview.d.ts +1 -1
  6. package/lib/client/SpeedSelect.d.ts +25 -2
  7. package/lib/client/SpeedSelect.js +10 -6
  8. package/lib/client/SubscriptionsSection.d.ts +74 -0
  9. package/lib/client/SubscriptionsSection.js +325 -4
  10. package/lib/client/VideoGenerateToolview.d.ts +1 -1
  11. package/lib/client/index.d.ts +1 -9
  12. package/lib/client/index.js +7 -4
  13. package/lib/client/locales.d.ts +28 -0
  14. package/lib/client/locales.js +28 -0
  15. package/lib/client.js +458 -10
  16. package/lib/client.js.map +1 -1
  17. package/lib/compat.d.ts +36 -0
  18. package/lib/compat.js +20 -0
  19. package/lib/index.d.ts +5 -1
  20. package/lib/index.js +865 -111
  21. package/lib/model-defaults.d.ts +23 -0
  22. package/lib/model-defaults.js +237 -0
  23. package/lib/providers/claude.d.ts +24 -3
  24. package/lib/providers/claude.js +35 -24
  25. package/lib/providers/codex.d.ts +21 -0
  26. package/lib/providers/codex.js +37 -10
  27. package/lib/providers/common.d.ts +70 -6
  28. package/lib/providers/common.js +118 -19
  29. package/lib/providers/copilot.d.ts +10 -0
  30. package/lib/providers/copilot.js +21 -8
  31. package/lib/providers/grok.d.ts +21 -0
  32. package/lib/providers/grok.js +37 -7
  33. package/lib/providers/pool-usage.d.ts +23 -2
  34. package/lib/providers/pool-usage.js +70 -15
  35. package/lib/providers/rate-limit.d.ts +192 -0
  36. package/lib/providers/rate-limit.js +338 -0
  37. package/lib/translate/anthropic.js +5 -4
  38. package/lib/translate/chat-completions.js +5 -4
  39. package/lib/translate/responses.js +5 -4
  40. package/package.json +21 -21
  41. package/lib/providers/antigravity.d.ts +0 -90
  42. package/lib/providers/antigravity.js +0 -392
  43. package/lib/translate/antigravity.d.ts +0 -110
  44. package/lib/translate/antigravity.js +0 -303
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { CONTEXT_WINDOW_EXCEEDED_CODE, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
2
+ import * as llm from "@deepseek-ai/dsh-llm";
3
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
4
  import { createServer } from "node:http";
4
5
  import { createHash, randomBytes, randomUUID } from "node:crypto";
5
6
  import { ProxyAgent, fetch as fetch$1 } from "undici";
@@ -271,13 +272,13 @@ const DISABLED = {
271
272
  bypass: []
272
273
  };
273
274
  /** Current config; updated by every load/apply/save. */
274
- let current = DISABLED;
275
+ let current$1 = DISABLED;
275
276
  /** The live dispatcher, or undefined when proxies are off/errored. */
276
277
  let agent;
277
278
  /** Last load/apply failure, surfaced by the config view. */
278
279
  let configError;
279
280
  /** One lazy load of the on-disk config (module-import cheap; file read once). */
280
- let ready;
281
+ let ready$1;
281
282
  /** Absolute path of the proxy config file. */
282
283
  function proxyFilePath() {
283
284
  return dshHomePath("plugins", "subscriptions", "proxy.json");
@@ -392,7 +393,7 @@ async function applyConfig(cfg) {
392
393
  withError(error);
393
394
  next = void 0;
394
395
  }
395
- current = cfg;
396
+ current$1 = cfg;
396
397
  }
397
398
  const previous = agent;
398
399
  agent = next;
@@ -432,16 +433,16 @@ async function loadConfigFile(path) {
432
433
  });
433
434
  }
434
435
  /** Resolve the module state once from disk; failures disable the proxy. */
435
- async function ensureReady() {
436
- ready ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
436
+ async function ensureReady$1() {
437
+ ready$1 ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
437
438
  await applyConfig(cfg);
438
- return current;
439
+ return current$1;
439
440
  }, async (error) => {
440
441
  withError(error);
441
442
  await applyConfig(void 0);
442
- return current;
443
+ return current$1;
443
444
  });
444
- return ready;
445
+ return ready$1;
445
446
  }
446
447
  /** Persist a config atomically with owner-only permissions, then apply it. */
447
448
  async function persistConfig(cfg, path) {
@@ -462,13 +463,13 @@ async function persistConfig(cfg, path) {
462
463
  * load/apply failure when the stored config is unusable.
463
464
  */
464
465
  async function proxyGetConfig() {
465
- await ensureReady();
466
+ await ensureReady$1();
466
467
  return {
467
- enabled: current.enabled,
468
- url: current.url,
469
- ...current.username === void 0 ? {} : { username: current.username },
470
- passwordSet: current.password !== void 0 && current.password !== "",
471
- bypass: [...current.bypass],
468
+ enabled: current$1.enabled,
469
+ url: current$1.url,
470
+ ...current$1.username === void 0 ? {} : { username: current$1.username },
471
+ passwordSet: current$1.password !== void 0 && current$1.password !== "",
472
+ bypass: [...current$1.bypass],
472
473
  ...configError === void 0 ? {} : { error: configError }
473
474
  };
474
475
  }
@@ -479,14 +480,14 @@ async function proxyGetConfig() {
479
480
  * @returns the resulting view (secrets omitted).
480
481
  */
481
482
  async function proxySetConfig(input) {
482
- await ensureReady();
483
- const password = input.password === void 0 ? current.password : input.password === null || input.password === "" ? void 0 : input.password;
483
+ await ensureReady$1();
484
+ const password = input.password === void 0 ? current$1.password : input.password === null || input.password === "" ? void 0 : input.password;
484
485
  const next = normalizeConfig({
485
486
  enabled: input.enabled,
486
487
  url: input.url,
487
488
  ...input.username === void 0 ? {} : { username: input.username },
488
489
  ...password === void 0 ? {} : { password },
489
- bypass: input.bypass ?? current.bypass
490
+ bypass: input.bypass ?? current$1.bypass
490
491
  });
491
492
  await persistConfig(next, proxyFilePath());
492
493
  await applyConfig(next);
@@ -502,16 +503,16 @@ async function proxySetConfig(input) {
502
503
  * host's global fetch.
503
504
  */
504
505
  async function proxiedFetch(input, init = {}) {
505
- await ensureReady();
506
+ await ensureReady$1();
506
507
  let dispatcher;
507
- if (current.enabled && agent !== void 0) {
508
+ if (current$1.enabled && agent !== void 0) {
508
509
  let hostname = "";
509
510
  try {
510
511
  hostname = (typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url)).hostname;
511
512
  } catch {
512
513
  hostname = "";
513
514
  }
514
- if (!matchesBypass(hostname, current.bypass)) dispatcher = agent;
515
+ if (!matchesBypass(hostname, current$1.bypass)) dispatcher = agent;
515
516
  }
516
517
  if (dispatcher === void 0) return fetch(input, init);
517
518
  return dispatchFetch(input, {
@@ -544,7 +545,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
544
545
  error: errorMessage(error)
545
546
  };
546
547
  }
547
- await ensureReady();
548
+ await ensureReady$1();
548
549
  let probeAgent;
549
550
  let viaProxy;
550
551
  let closeProbe = false;
@@ -566,7 +567,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
566
567
  };
567
568
  }
568
569
  else {
569
- viaProxy = current.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current.bypass);
570
+ viaProxy = current$1.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current$1.bypass);
570
571
  probeAgent = viaProxy ? agent : void 0;
571
572
  }
572
573
  const started = Date.now();
@@ -1227,6 +1228,22 @@ function readString(payload, field) {
1227
1228
  if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
1228
1229
  return value;
1229
1230
  }
1231
+ /** Validate the `setModelDefault` endpoint's payload. */
1232
+ function readModelDefaultInput(payload) {
1233
+ const provider = readProvider(payload);
1234
+ const model = readString(payload, "model");
1235
+ const record = payload;
1236
+ let effort;
1237
+ if (record.effort !== void 0) {
1238
+ if (typeof record.effort !== "string" || record.effort.length === 0) throw new BadRequest("payload.effort must be a non-empty string when present");
1239
+ effort = record.effort;
1240
+ }
1241
+ return {
1242
+ provider,
1243
+ model,
1244
+ ...effort === void 0 ? {} : { effort }
1245
+ };
1246
+ }
1230
1247
  /** Validate the optional Claude login method. */
1231
1248
  function readLoginMethod(payload, provider) {
1232
1249
  const method = payload.method;
@@ -1279,6 +1296,14 @@ function readVideoName(payload) {
1279
1296
  if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
1280
1297
  return name$1;
1281
1298
  }
1299
+ /** Validate the `usage` endpoint's optional force flag. */
1300
+ function readForce(payload) {
1301
+ if (typeof payload !== "object" || payload === null) return false;
1302
+ const force = payload.force;
1303
+ if (force === void 0) return false;
1304
+ if (typeof force !== "boolean") throw new BadRequest("payload.force must be a boolean when present");
1305
+ return force;
1306
+ }
1282
1307
  /** Validate the session id both speed endpoints carry. */
1283
1308
  function readSessionId(payload) {
1284
1309
  if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
@@ -1346,7 +1371,7 @@ function readProxyTestPayload(payload) {
1346
1371
  ...proxy === void 0 ? {} : { proxy }
1347
1372
  };
1348
1373
  }
1349
- async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
1374
+ async function dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal) {
1350
1375
  switch (endpoint) {
1351
1376
  case "status": {
1352
1377
  const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
@@ -1376,7 +1401,7 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
1376
1401
  }
1377
1402
  case "usage": {
1378
1403
  const provider = readProvider(payload);
1379
- return ok(await controller.usage(provider, readString(payload, "account"), signal));
1404
+ return ok(await controller.usage(provider, readString(payload, "account"), signal, readForce(payload)));
1380
1405
  }
1381
1406
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
1382
1407
  case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
@@ -1393,6 +1418,16 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
1393
1418
  case "proxyTest":
1394
1419
  if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
1395
1420
  return ok(await proxy.test(readProxyTestPayload(payload)));
1421
+ case "modelDefaults":
1422
+ if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
1423
+ return ok(await modelDefaults.catalog());
1424
+ case "setModelDefault":
1425
+ if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
1426
+ {
1427
+ const input = readModelDefaultInput(payload);
1428
+ await modelDefaults.set(input.provider, input.model, input.effort);
1429
+ }
1430
+ return ok({ ok: true });
1396
1431
  default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
1397
1432
  }
1398
1433
  }
@@ -1402,13 +1437,14 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
1402
1437
  * @param controller - the auth operations backing the endpoints.
1403
1438
  * @param speed - the per-session speed-tier state backing the Speed toggle.
1404
1439
  * @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
1440
+ * @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
1405
1441
  */
1406
- function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
1442
+ function registerAuthRpc(ctx, controller, speed, proxy = void 0, modelDefaults = void 0) {
1407
1443
  ctx.inject(["connection"], (ctx$1) => {
1408
1444
  const connection = ctx$1.get("connection");
1409
1445
  ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
1410
1446
  try {
1411
- return await dispatch(controller, speed, proxy, endpoint, payload, signal);
1447
+ return await dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal);
1412
1448
  } catch (error) {
1413
1449
  return failure(error);
1414
1450
  }
@@ -1416,6 +1452,442 @@ function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
1416
1452
  });
1417
1453
  }
1418
1454
 
1455
+ //#endregion
1456
+ //#region src/model-defaults.ts
1457
+ /** Absolute path of the defaults file. */
1458
+ function modelDefaultsFilePath() {
1459
+ return dshHomePath("plugins", "subscriptions", "model-defaults.json");
1460
+ }
1461
+ const EMPTY = Object.freeze({});
1462
+ /** In-memory snapshot read by every consumer (adapters, RPC). */
1463
+ let current = EMPTY;
1464
+ /** One lazy load of the on-disk file (read once per process). */
1465
+ let ready;
1466
+ /** Last load failure, surfaced to callers that care; defaults stay empty. */
1467
+ let loadError;
1468
+ /**
1469
+ * Serialises every write: the read-modify-write sequence must not interleave,
1470
+ * or a fast second save would compute its snapshot from the stale `current`
1471
+ * and silently drop the first update (the UI disables only the row being
1472
+ * saved, so overlaps are reachable).
1473
+ */
1474
+ let writeChain = Promise.resolve();
1475
+ /**
1476
+ * Validate one persisted provider section: a string→string map, or undefined.
1477
+ * Malformed *entries* are skipped, not the whole section: one bad value (a
1478
+ * hand edit losing its quotes) must not silently un-configure every model in
1479
+ * that provider. What was dropped is reported so the caller can surface it
1480
+ * instead of the loss disappearing.
1481
+ */
1482
+ function sanitizeProvider(value, dropped) {
1483
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1484
+ const entries = {};
1485
+ for (const [model, effort] of Object.entries(value)) {
1486
+ if (typeof effort !== "string" || effort.length === 0) {
1487
+ dropped.push(model);
1488
+ continue;
1489
+ }
1490
+ entries[model] = effort;
1491
+ }
1492
+ if (Object.keys(entries).length === 0) return void 0;
1493
+ return Object.freeze(entries);
1494
+ }
1495
+ /** Validate the raw document: only known providers, malformed sections dropped. */
1496
+ function sanitizeDefaults(value) {
1497
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {
1498
+ defaults: EMPTY,
1499
+ dropped: []
1500
+ };
1501
+ const record = value;
1502
+ const result = {};
1503
+ const dropped = [];
1504
+ for (const provider of PROVIDER_IDS) {
1505
+ const section = sanitizeProvider(record[provider], dropped);
1506
+ if (section !== void 0) result[provider] = section;
1507
+ }
1508
+ return {
1509
+ defaults: Object.freeze(result),
1510
+ dropped
1511
+ };
1512
+ }
1513
+ /** Read and validate the on-disk file; a missing file reads as empty. */
1514
+ async function loadFile(path) {
1515
+ let text;
1516
+ try {
1517
+ text = await readFile(path, "utf8");
1518
+ } catch (error) {
1519
+ if (error.code === "ENOENT") return EMPTY;
1520
+ throw error;
1521
+ }
1522
+ try {
1523
+ const { defaults, dropped } = sanitizeDefaults(JSON.parse(text));
1524
+ if (dropped.length > 0) loadError = /* @__PURE__ */ new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? "y" : "ies"} skipped (${dropped.join(", ")}); fix or delete the file`);
1525
+ return defaults;
1526
+ } catch {
1527
+ throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
1528
+ }
1529
+ }
1530
+ /** Resolve the module state once from disk; failures leave the defaults empty. */
1531
+ async function ensureReady() {
1532
+ ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
1533
+ current = loaded;
1534
+ }, (error) => {
1535
+ loadError = error;
1536
+ current = EMPTY;
1537
+ });
1538
+ return ready;
1539
+ }
1540
+ /** Persist a snapshot atomically with owner-only permissions. */
1541
+ async function atomicPersist(defaults, path) {
1542
+ await mkdir(dirname(path), { recursive: true });
1543
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
1544
+ try {
1545
+ await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 384 });
1546
+ await chmod(tmp, 384);
1547
+ await rename(tmp, path);
1548
+ } catch (error) {
1549
+ await rm(tmp, { force: true });
1550
+ throw error;
1551
+ }
1552
+ }
1553
+ let persistDefaults = atomicPersist;
1554
+ /**
1555
+ * Clone one provider section, or undefined when nothing is configured for it.
1556
+ * The clone is prototype-less: model ids are provider-supplied catalog data
1557
+ * used as object keys, and consumers index the section directly (the RPC
1558
+ * catalog in index.ts does), so an id like `toString` would otherwise yield an
1559
+ * inherited *function* where a string is declared.
1560
+ */
1561
+ function sectionOf(defaults, provider) {
1562
+ const section = defaults[provider];
1563
+ if (section === void 0) return void 0;
1564
+ return Object.assign(Object.create(null), section);
1565
+ }
1566
+ /**
1567
+ * Ready the defaults store.
1568
+ * @internal Exported for tests; index.ts calls it at apply time so every
1569
+ * later synchronous read sees the persisted state.
1570
+ */
1571
+ async function loadModelDefaults() {
1572
+ await ensureReady();
1573
+ }
1574
+ /**
1575
+ * The configured default effort for one model, or undefined when none (the
1576
+ * picker then follows the provider's own default).
1577
+ * @internal Exported for the adapters' `defaultEffortOf` options.
1578
+ */
1579
+ function defaultEffortOf(provider, model) {
1580
+ const section = current[provider];
1581
+ if (section === void 0) return void 0;
1582
+ return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : void 0;
1583
+ }
1584
+ /**
1585
+ * Set or clear one model's configured default effort, then persist. The
1586
+ * memory snapshot updates only after the atomic write succeeds, so a failed
1587
+ * write never leaves the live state ahead of the file.
1588
+ * @param provider - the subscription provider route.
1589
+ * @param model - the wire model id.
1590
+ * @param effort - the effort id, or undefined to clear the override.
1591
+ */
1592
+ function setDefaultEffort(provider, model, effort) {
1593
+ const run = writeChain.then(async () => {
1594
+ await ensureReady();
1595
+ const section = { ...sectionOf(current, provider) ?? {} };
1596
+ if (effort === void 0) delete section[model];
1597
+ else section[model] = effort;
1598
+ const next = { ...current };
1599
+ if (Object.keys(section).length === 0) delete next[provider];
1600
+ else next[provider] = Object.freeze(section);
1601
+ const frozen = Object.freeze(next);
1602
+ await persistDefaults(frozen, modelDefaultsFilePath());
1603
+ current = frozen;
1604
+ });
1605
+ writeChain = run.catch(() => void 0);
1606
+ return run;
1607
+ }
1608
+
1609
+ //#endregion
1610
+ //#region src/providers/rate-limit.ts
1611
+ /**
1612
+ * Extra time added to every provider-disclosed wait. Absorbs clock skew
1613
+ * between the harness and the provider, so a retry does not land a moment
1614
+ * before the window actually reopens and burn an attempt on a second 429.
1615
+ */
1616
+ const RESET_GRACE_MS = 2e3;
1617
+ /** Shortest wait ever scheduled, including for a reset instant already in the past. */
1618
+ const MIN_WAIT_MS = 1e3;
1619
+ /** Below this a bare number is a delay in seconds rather than an epoch stamp. */
1620
+ const EPOCH_SECONDS_FLOOR = 1e9;
1621
+ /** At or above this a bare epoch stamp is already in milliseconds. */
1622
+ const EPOCH_MILLIS_FLOOR = 0xe8d4a51000;
1623
+ /** Node's maximum timer delay; a longer wait cannot be scheduled at all. */
1624
+ const MAX_TIMER_DELAY_MS = 2147483647;
1625
+ /** Default ceiling on a rate-limit wait: six hours covers a five-hour session window with slack. */
1626
+ const DEFAULT_RATE_LIMIT_MAX_WAIT_MS = 360 * 60 * 1e3;
1627
+ /**
1628
+ * Interpret a bare numeric rate-limit value, which providers write in three
1629
+ * shapes: epoch milliseconds, epoch seconds, or a delay in seconds. The
1630
+ * magnitude separates them unambiguously for any plausible value — an epoch in
1631
+ * seconds is ~1.8e9 today, while a delay of even a full week is ~6e5.
1632
+ * @param value - the raw numeric value.
1633
+ * @param now - the current epoch milliseconds.
1634
+ * @returns epoch milliseconds of the reset, or undefined when the value is unusable.
1635
+ */
1636
+ function resetInstantFromNumber(value, now) {
1637
+ if (!Number.isFinite(value) || value <= 0) return void 0;
1638
+ if (value >= EPOCH_MILLIS_FLOOR) return value;
1639
+ if (value >= EPOCH_SECONDS_FLOOR) return value * 1e3;
1640
+ return now + value * 1e3;
1641
+ }
1642
+ /**
1643
+ * Parse a Go-style duration (`6m0s`, `1h2m3.5s`, `150ms`) into milliseconds —
1644
+ * the form OpenAI-compatible `x-ratelimit-reset-*` headers use.
1645
+ * @param text - the raw header value.
1646
+ * @returns the duration in milliseconds, or undefined when the text is not one.
1647
+ */
1648
+ function durationMs(text) {
1649
+ const trimmed = text.trim();
1650
+ if (trimmed.length === 0) return void 0;
1651
+ const pattern = /(\d+(?:\.\d+)?)(ms|h|m|s)/y;
1652
+ const units = {
1653
+ h: 36e5,
1654
+ m: 6e4,
1655
+ s: 1e3,
1656
+ ms: 1
1657
+ };
1658
+ let total = 0;
1659
+ let matched = false;
1660
+ let index = 0;
1661
+ let previousUnit = Number.POSITIVE_INFINITY;
1662
+ for (;;) {
1663
+ pattern.lastIndex = index;
1664
+ const match = pattern.exec(trimmed);
1665
+ if (match === null) break;
1666
+ const unit = units[match[2]];
1667
+ if (unit >= previousUnit) return void 0;
1668
+ previousUnit = unit;
1669
+ total += Number(match[1]) * unit;
1670
+ index = pattern.lastIndex;
1671
+ matched = true;
1672
+ }
1673
+ if (!matched || index !== trimmed.length) return void 0;
1674
+ return total > 0 ? total : void 0;
1675
+ }
1676
+ /**
1677
+ * Interpret any single rate-limit value — a number, a numeric string, a
1678
+ * duration (`6m0s`), or a date — as the instant a window reopens. One reader
1679
+ * for every shape, so a provider that changes the encoding of a field it
1680
+ * already sends does not need a code change here.
1681
+ * @param value - the raw header value or JSON field.
1682
+ * @param now - the current epoch milliseconds.
1683
+ * @returns epoch milliseconds of the reset, or undefined when the value is unusable.
1684
+ */
1685
+ function resetInstantFromValue(value, now) {
1686
+ if (typeof value === "number") return resetInstantFromNumber(value, now);
1687
+ if (typeof value !== "string") return void 0;
1688
+ const trimmed = value.trim();
1689
+ if (trimmed.length === 0) return void 0;
1690
+ const numeric = Number(trimmed);
1691
+ if (Number.isFinite(numeric)) return resetInstantFromNumber(numeric, now);
1692
+ const duration = durationMs(trimmed);
1693
+ if (duration !== void 0) return now + duration;
1694
+ const parsed = Date.parse(trimmed);
1695
+ return Number.isFinite(parsed) ? parsed : void 0;
1696
+ }
1697
+ /**
1698
+ * Read a header carrying any of the {@link resetInstantFromValue} shapes.
1699
+ * @param response - the failed response.
1700
+ * @param name - the header to read.
1701
+ * @param now - the current epoch milliseconds.
1702
+ * @returns epoch milliseconds of the reset, or undefined when absent or unusable.
1703
+ */
1704
+ function resetInstantFromHeader(response, name$1, now) {
1705
+ return resetInstantFromValue(response.headers.get(name$1), now);
1706
+ }
1707
+ /**
1708
+ * Read the RFC 7231 `retry-after` header in both its forms: a delay in seconds
1709
+ * (never an epoch stamp, whatever its magnitude) or an HTTP-date.
1710
+ * @param response - the failed response.
1711
+ * @param now - the current epoch milliseconds.
1712
+ * @returns epoch milliseconds of the reset, or undefined when absent or unusable.
1713
+ */
1714
+ function retryAfterInstant(response, now) {
1715
+ const raw = response.headers.get("retry-after");
1716
+ if (raw === null) return void 0;
1717
+ const trimmed = raw.trim();
1718
+ if (trimmed.length === 0) return void 0;
1719
+ const seconds = Number(trimmed);
1720
+ if (Number.isFinite(seconds)) return seconds > 0 ? now + seconds * 1e3 : void 0;
1721
+ const parsed = Date.parse(trimmed);
1722
+ return Number.isFinite(parsed) ? parsed : void 0;
1723
+ }
1724
+ /**
1725
+ * Parse a response body as JSON without throwing on the non-JSON bodies
1726
+ * providers occasionally return under load (an HTML gateway page, say).
1727
+ * @param body - the complete response body.
1728
+ * @returns the parsed value, or undefined when the body is not JSON.
1729
+ */
1730
+ function jsonBody(body) {
1731
+ if (body.length === 0) return void 0;
1732
+ try {
1733
+ return JSON.parse(body);
1734
+ } catch {
1735
+ return;
1736
+ }
1737
+ }
1738
+ /** How deep {@link resetFromFields} walks; every observed payload nests one or two levels. */
1739
+ const MAX_BODY_DEPTH = 4;
1740
+ /**
1741
+ * Find a reset instant under any of the named keys, anywhere in a parsed body.
1742
+ *
1743
+ * The search is by key rather than by path on purpose: providers move the same
1744
+ * field between containers (`detail`, `error`, top level) across endpoints and
1745
+ * versions, and a path-shaped reader silently stops working when they do. Only
1746
+ * the key list is provider-specific.
1747
+ * @param value - the parsed body, or any nested value.
1748
+ * @param keys - field names this provider uses for a reset or delay.
1749
+ * @param now - the current epoch milliseconds.
1750
+ * @param depth - remaining recursion depth.
1751
+ * @returns the earliest instant found, or undefined when no key matched.
1752
+ */
1753
+ function resetFromFields(value, keys, now, depth = MAX_BODY_DEPTH) {
1754
+ if (depth <= 0 || value === null || typeof value !== "object") return void 0;
1755
+ let earliest;
1756
+ const consider = (candidate) => {
1757
+ if (candidate !== void 0 && (earliest === void 0 || candidate < earliest)) earliest = candidate;
1758
+ };
1759
+ if (Array.isArray(value)) {
1760
+ for (const item of value) consider(resetFromFields(item, keys, now, depth - 1));
1761
+ return earliest;
1762
+ }
1763
+ for (const [key, nested] of Object.entries(value)) if (keys.includes(key)) consider(resetInstantFromValue(nested, now));
1764
+ else consider(resetFromFields(nested, keys, now, depth - 1));
1765
+ return earliest;
1766
+ }
1767
+ /**
1768
+ * The earliest of several candidate reset instants, ignoring absent ones. The
1769
+ * earliest is the one that matters: it is the first moment any of the reported
1770
+ * limits allows a request again.
1771
+ * @param candidates - reset instants in no particular order.
1772
+ * @returns the earliest instant, or undefined when every candidate is absent.
1773
+ */
1774
+ function earliestReset(...candidates) {
1775
+ let earliest;
1776
+ for (const candidate of candidates) {
1777
+ if (candidate === void 0) continue;
1778
+ if (earliest === void 0 || candidate < earliest) earliest = candidate;
1779
+ }
1780
+ return earliest;
1781
+ }
1782
+ /**
1783
+ * Turn a reset instant into the wait to report as `providerRetryAfterMs`.
1784
+ *
1785
+ * Deliberately not capped: a reset beyond the policy's `maxDelayMs` makes the
1786
+ * retry plugin delegate immediately, failing the turn at once with the real
1787
+ * reset in the message, rather than clamping the wait down and burning the
1788
+ * retry budget against a window that is still closed.
1789
+ * @param instant - epoch milliseconds the window reopens.
1790
+ * @param now - the current epoch milliseconds.
1791
+ * @returns the wait in milliseconds, never below {@link MIN_WAIT_MS}.
1792
+ */
1793
+ function waitFromReset(instant, now) {
1794
+ return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
1795
+ }
1796
+ /** Header names worth showing when a 429 disclosed no reset this code recognizes. */
1797
+ const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
1798
+ /**
1799
+ * Render the rate-limit-shaped headers and the head of the body of a 429 whose
1800
+ * reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
1801
+ * how an unrecognized provider field gets named from live traffic instead of
1802
+ * being guessed at.
1803
+ *
1804
+ * It is also where the per-bucket rollover snapshots land by design — no reader
1805
+ * parks a turn on one, because on a 429 they cannot say which bucket refused —
1806
+ * so the operator still sees what the provider disclosed.
1807
+ * @param response - the failed response.
1808
+ * @param body - the complete response body.
1809
+ * @returns a one-line diagnostic.
1810
+ */
1811
+ function rateLimitDiagnostics(response, body) {
1812
+ const headers = [];
1813
+ response.headers.forEach((value, key) => {
1814
+ if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
1815
+ });
1816
+ headers.sort();
1817
+ const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
1818
+ const head = body.slice(0, 200);
1819
+ return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
1820
+ }
1821
+ /**
1822
+ * The retry shape every subscription route starts from: Claude Code's own SDK
1823
+ * numbers — ten retries after the first attempt, exponential backoff from 1s
1824
+ * doubling per attempt, capped at 60s, plus 20% jitter.
1825
+ *
1826
+ * Shared across all four routes rather than kept to claude, because what these
1827
+ * numbers are tuned for is the shape of a subscription endpoint — a consumer
1828
+ * plan behind a session window, which sheds load in bursts and rewards an
1829
+ * attempt that outlasts them — and that is the same on all four. The dsh-llm
1830
+ * defaults (5 retries from 500ms to 10s) give up after about fifteen seconds,
1831
+ * which is short for that.
1832
+ *
1833
+ * The 60s cap governs local backoff only: a disclosed rate-limit reset is
1834
+ * accepted up to the configured wait ceiling instead.
1835
+ */
1836
+ const DEFAULT_RETRY = Object.freeze({
1837
+ maxRetries: 10,
1838
+ initialDelayMs: 1e3,
1839
+ maxDelayMs: 6e4,
1840
+ jitterRatio: .2
1841
+ });
1842
+ /** Waiting behavior a route falls back to when the plugin passed none (waiting on, six-hour ceiling). */
1843
+ const DEFAULT_RATE_LIMIT_WAIT = Object.freeze({
1844
+ wait: true,
1845
+ maxWaitMs: DEFAULT_RATE_LIMIT_MAX_WAIT_MS
1846
+ });
1847
+ /**
1848
+ * Validate and default the rate-limit waiting config.
1849
+ * @param config - the raw plugin config section, when present.
1850
+ * @param path - diagnostic path naming the config that owns the value.
1851
+ * @returns the resolved, immutable behavior.
1852
+ */
1853
+ function resolveRateLimitWait(config, path) {
1854
+ const wait = config?.wait ?? true;
1855
+ const maxWaitMs = config?.maxWaitMs ?? DEFAULT_RATE_LIMIT_MAX_WAIT_MS;
1856
+ if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0) throw new Error(`${path}.maxWaitMs must be a positive finite number of milliseconds`);
1857
+ if (maxWaitMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.maxWaitMs must be no greater than ${String(MAX_TIMER_DELAY_MS)} (the maximum schedulable delay)`);
1858
+ return Object.freeze({
1859
+ wait,
1860
+ maxWaitMs
1861
+ });
1862
+ }
1863
+ /**
1864
+ * Resolve one route's retry policy, widening the delay ceiling to the
1865
+ * configured wait so a disclosed reset hours out is accepted rather than
1866
+ * refused.
1867
+ *
1868
+ * The ceiling is shared with local exponential backoff, so widening it also
1869
+ * raises how long an unrelated transient failure may back off for. That stays
1870
+ * bounded by the finite retry budget — the claude route's ten retries reach
1871
+ * 512 s per attempt at most — and it only governs when the provider disclosed
1872
+ * nothing, which is exactly the case where a longer wait is the safer guess.
1873
+ * @param defaults - the route's retry shape.
1874
+ * @param rateLimit - resolved waiting behavior.
1875
+ * @param path - diagnostic path naming the provider route.
1876
+ * @returns the policy to report from `providerRetryPolicy`.
1877
+ */
1878
+ function subscriptionRetryPolicy(defaults, rateLimit, path) {
1879
+ const maxDelayMs = rateLimit.wait ? Math.max(defaults.maxDelayMs, rateLimit.maxWaitMs) : defaults.maxDelayMs;
1880
+ return resolveRetryPolicy({
1881
+ mode: "normal",
1882
+ maxRetries: defaults.maxRetries,
1883
+ backoff: {
1884
+ initialDelayMs: defaults.initialDelayMs,
1885
+ maxDelayMs,
1886
+ jitterRatio: defaults.jitterRatio
1887
+ }
1888
+ }, path);
1889
+ }
1890
+
1419
1891
  //#endregion
1420
1892
  //#region src/providers/common.ts
1421
1893
  /**
@@ -1446,38 +1918,56 @@ function validateModels(models, label) {
1446
1918
  });
1447
1919
  }
1448
1920
  /**
1449
- * Build an LlmError from a non-2xx provider response, reading and truncating
1450
- * the body for the message and mapping the status to a stable code.
1921
+ * Build an LlmError from a non-2xx provider response, mapping the status to a
1922
+ * stable code and, for a rate-limited request, the disclosed reset instant to
1923
+ * the `providerRetryAfterMs` the retry plugin waits out.
1924
+ *
1925
+ * A 429 classifies as `RATE_LIMIT` on the strength of the status alone, ahead
1926
+ * of the quota-wording check. On these routes there is no terminal quota to
1927
+ * distinguish: a subscription has no balance to top up, only a window that
1928
+ * reopens, and providers announce an exhausted window with wording
1929
+ * (`usage_limit_reached`) the shared classifier reads as permanent.
1451
1930
  * @param response - the failed response.
1452
1931
  * @param label - diagnostic prefix naming the provider API.
1932
+ * @param options - the calling provider's rate-limit reader and warning sink.
1453
1933
  * @returns the classified error.
1454
1934
  */
1455
- async function httpLlmError(response, label) {
1935
+ async function httpLlmError(response, label, options = {}) {
1456
1936
  let body = "";
1457
1937
  try {
1458
- body = (await response.text()).slice(0, 500);
1938
+ body = await response.text();
1459
1939
  } catch {}
1460
- const message = body.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${body}` : `${label} error (HTTP ${String(response.status)})`;
1940
+ const shown = body.slice(0, 500);
1941
+ const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
1461
1942
  let code;
1462
1943
  if (response.status === 401 || response.status === 403) code = "AUTH";
1463
- else if (isQuotaExceededError(body)) code = QUOTA_EXCEEDED_CODE;
1464
1944
  else if (response.status === 429) code = "RATE_LIMIT";
1465
- else if (response.status === 400 && isContextWindowExceededError(body)) code = CONTEXT_WINDOW_EXCEEDED_CODE;
1945
+ else if (isQuotaExceededError(shown)) code = QUOTA_EXCEEDED_CODE;
1946
+ else if (response.status === 400 && isContextWindowExceededError(shown)) code = CONTEXT_WINDOW_EXCEEDED_CODE;
1466
1947
  else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
1467
1948
  else if (response.status >= 500) code = "SERVER";
1468
1949
  else code = `HTTP_${String(response.status)}`;
1469
- const retryAfter = response.headers.get("retry-after");
1470
- let providerRetryAfterMs;
1471
- if (retryAfter !== null) {
1472
- const seconds = Number(retryAfter);
1473
- if (Number.isFinite(seconds) && seconds > 0) providerRetryAfterMs = seconds * 1e3;
1474
- }
1950
+ const now = Date.now();
1951
+ const rateLimited = response.status === 429;
1952
+ const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
1953
+ if (reset === void 0 && rateLimited) options.onWarn?.(`${label}: ${rateLimitDiagnostics(response, body)}`);
1475
1954
  return new LlmError(message, code, {
1476
1955
  status: response.status,
1477
- ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs }
1956
+ ...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
1478
1957
  });
1479
1958
  }
1480
1959
  /**
1960
+ * Parse a response's `retry-after` header (seconds) into milliseconds.
1961
+ * @param response - the failed response.
1962
+ * @returns the delay in ms, or undefined when absent/unusable.
1963
+ */
1964
+ function parseRetryAfterMs(response) {
1965
+ const retryAfter = response.headers.get("retry-after");
1966
+ if (retryAfter === null) return void 0;
1967
+ const seconds = Number(retryAfter);
1968
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
1969
+ }
1970
+ /**
1481
1971
  * Create an idle watchdog chained to the caller's signal.
1482
1972
  * @param caller - the request's own abort signal, when present.
1483
1973
  * @param timeoutMs - maximum idle interval while a stream read is outstanding.
@@ -1531,11 +2021,20 @@ var OAuthEndpointError = class extends Error {
1531
2021
  status;
1532
2022
  /** The provider's OAuth `error` code (e.g. `invalid_grant`), when present. */
1533
2023
  oauthCode;
1534
- constructor(message, status, oauthCode) {
2024
+ /**
2025
+ * The endpoint's `retry-after`, in ms, when it sent one. Usage/models
2026
+ * endpoints reuse this error type and can rate-limit progressively (each
2027
+ * hit within the window extends the next one), so a caller retrying on a
2028
+ * fixed schedule instead of honoring this can keep an account locked out
2029
+ * indefinitely.
2030
+ */
2031
+ retryAfterMs;
2032
+ constructor(message, status, oauthCode, retryAfterMs$1) {
1535
2033
  super(message);
1536
2034
  this.name = "OAuthEndpointError";
1537
2035
  this.status = status;
1538
2036
  this.oauthCode = oauthCode;
2037
+ this.retryAfterMs = retryAfterMs$1;
1539
2038
  }
1540
2039
  };
1541
2040
  /**
@@ -1552,7 +2051,7 @@ async function oauthEndpointError(response, label) {
1552
2051
  oauthCode = typeof parsed.error === "string" ? parsed.error : void 0;
1553
2052
  detail = typeof parsed.error_description === "string" ? parsed.error_description : oauthCode ?? "";
1554
2053
  } catch {}
1555
- return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode);
2054
+ return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode, parseRetryAfterMs(response));
1556
2055
  }
1557
2056
  /**
1558
2057
  * Per-provider session freshness: loads the stored session, refreshes
@@ -1609,9 +2108,9 @@ var TokenManager = class {
1609
2108
  }
1610
2109
  }
1611
2110
  async doRefresh(session) {
1612
- const current$1 = await this.options.load();
1613
- if (current$1 !== void 0 && current$1.accessToken !== session.accessToken && current$1.expiresAt - Date.now() > this.options.preemptMs) return current$1;
1614
- const next = await this.options.refresh(current$1 ?? session);
2111
+ const current$2 = await this.options.load();
2112
+ if (current$2 !== void 0 && current$2.accessToken !== session.accessToken && current$2.expiresAt - Date.now() > this.options.preemptMs) return current$2;
2113
+ const next = await this.options.refresh(current$2 ?? session);
1615
2114
  await this.options.save(next);
1616
2115
  return next;
1617
2116
  }
@@ -1633,6 +2132,59 @@ function withTimeout(work, timeoutMs) {
1633
2132
  throw error;
1634
2133
  }), aborted]);
1635
2134
  }
2135
+ /** Display name for a wire reasoning-effort identifier. */
2136
+ function effortDisplayName(effort) {
2137
+ return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
2138
+ }
2139
+ /**
2140
+ * Fold a configured per-model default effort into a reasoning block, keeping
2141
+ * the DSH runtime invariant `defaultEffort ∈ efforts` (the runtime rejects an
2142
+ * unknown default with `INVALID_MODEL_REASONING`).
2143
+ *
2144
+ * A configured level the base set does not advertise is *dropped*, not
2145
+ * appended: for claude/grok/copilot the base is the provider's live catalog,
2146
+ * i.e. the truth about what the model accepts, so honouring a stale override
2147
+ * would put an unsupported effort on every single request instead of letting
2148
+ * the harness reject it before provider I/O. The override then simply falls
2149
+ * back to the provider's own default until the user picks a level the catalog
2150
+ * still lists.
2151
+ *
2152
+ * `extendable` opts into the opposite rule for a base that is a *built-in
2153
+ * fallback* rather than discovered truth (codex, whose static effort list is
2154
+ * known to trail the backend): there, appending the configured level is how a
2155
+ * newly shipped tier becomes selectable at all.
2156
+ * @param configuredDefault - the user-configured default effort id, or undefined.
2157
+ * @param base - the discovered/built-in reasoning block, or undefined.
2158
+ * @param options - `extendable` marks the base as a fallback that may be extended.
2159
+ * @returns the merged block, or undefined when neither side contributes one.
2160
+ */
2161
+ function mergeReasoning(configuredDefault, base, options) {
2162
+ const detached = base === void 0 ? void 0 : {
2163
+ efforts: [...base.efforts],
2164
+ ...base.defaultEffort === void 0 ? {} : { defaultEffort: base.defaultEffort }
2165
+ };
2166
+ if (configuredDefault === void 0) return detached;
2167
+ const effort = ReasoningEffortId(configuredDefault);
2168
+ if (base === void 0) return options?.extendable === true ? {
2169
+ efforts: [{
2170
+ id: effort,
2171
+ name: effortDisplayName(effort)
2172
+ }],
2173
+ defaultEffort: effort
2174
+ } : void 0;
2175
+ if (base.efforts.some((entry) => entry.id === effort)) return {
2176
+ efforts: [...base.efforts],
2177
+ defaultEffort: effort
2178
+ };
2179
+ if (options?.extendable !== true) return detached;
2180
+ return {
2181
+ efforts: [...base.efforts, {
2182
+ id: effort,
2183
+ name: effortDisplayName(effort)
2184
+ }],
2185
+ defaultEffort: effort
2186
+ };
2187
+ }
1636
2188
  /**
1637
2189
  * First account catalog that lists `model` (callers pass default-first).
1638
2190
  * One failing lookup sits that account out so a sibling's metadata still
@@ -2557,7 +3109,8 @@ var PoolUsageTracker = class {
2557
3109
  /**
2558
3110
  * The quota view of one member. A cold cache awaits the first fetch; a
2559
3111
  * stale one answers immediately while the refresh serves the NEXT call
2560
- * (member selection must never block on the network mid-conversation).
3112
+ * (member selection must never block on the network mid-conversation). A
3113
+ * failure still cooling down degrades immediately with no network call.
2561
3114
  * @param member - the pool member to score (account resolved).
2562
3115
  * @returns availability plus the urgency score.
2563
3116
  */
@@ -2570,10 +3123,13 @@ var PoolUsageTracker = class {
2570
3123
  fetchedAt: 0
2571
3124
  };
2572
3125
  const entry = this.entries.get(key);
2573
- if (entry !== void 0 && Date.now() - entry.at < this.ttlMs) return this.score(member, entry);
2574
3126
  if (entry !== void 0) {
2575
- this.refresh(key, fetcher).catch(() => void 0);
2576
- return this.score(member, entry);
3127
+ const fresh = Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs);
3128
+ if (entry.snapshot !== void 0) {
3129
+ if (!fresh) this.refresh(key, fetcher).catch(() => void 0);
3130
+ return this.score(member, entry);
3131
+ }
3132
+ if (fresh) return degradedQuota(entry.error);
2577
3133
  }
2578
3134
  try {
2579
3135
  const snapshot = await this.refresh(key, fetcher);
@@ -2582,17 +3138,31 @@ var PoolUsageTracker = class {
2582
3138
  at: Date.now()
2583
3139
  });
2584
3140
  } catch (error) {
2585
- return isMissingOrInvalidCredential(error) ? {
2586
- available: false,
2587
- urgency: 0,
2588
- fetchedAt: 0
2589
- } : {
2590
- available: true,
2591
- urgency: 0,
2592
- fetchedAt: 0
2593
- };
3141
+ return degradedQuota(error);
2594
3142
  }
2595
3143
  }
3144
+ /**
3145
+ * Same cache as {@link quotaFor}, for direct display (the Settings page):
3146
+ * the raw snapshot, or the original fetch error, instead of a routing
3147
+ * score.
3148
+ * @param provider - the account's provider.
3149
+ * @param account - the account key.
3150
+ * @param force - bypass a fresh cached SNAPSHOT for an honest re-check (the
3151
+ * manual Refresh button). A live failure cooldown is never bypassed —
3152
+ * retrying through it is exactly what turns a 429 into a permanent
3153
+ * lockout, so even a forced call still answers from the negative cache.
3154
+ * @returns `{ supported: false }` when the provider has no usage fetcher.
3155
+ */
3156
+ async snapshotFor(provider, account, force = false) {
3157
+ const fetcher = this.fetcherFor(provider, account);
3158
+ if (fetcher === void 0) return { supported: false };
3159
+ const key = `${provider}/${account}`;
3160
+ const entry = this.entries.get(key);
3161
+ if (entry !== void 0 && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) if (entry.snapshot !== void 0) {
3162
+ if (!force) return entry.snapshot;
3163
+ } else throw entry.error;
3164
+ return this.refresh(key, fetcher);
3165
+ }
2596
3166
  /** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
2597
3167
  invalidate(provider, account) {
2598
3168
  if (account !== void 0) {
@@ -2601,7 +3171,14 @@ var PoolUsageTracker = class {
2601
3171
  }
2602
3172
  for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`)) this.entries.delete(key);
2603
3173
  }
2604
- /** Run (or join) the single in-flight fetch for one account key. */
3174
+ /**
3175
+ * Run (or join) the single in-flight fetch for one account key, caching
3176
+ * either outcome. A missing/invalid credential is deliberately NOT
3177
+ * negative-cached: it costs no network round trip (the session lookup
3178
+ * fails before the request goes out) and re-checking live means the
3179
+ * member rejoins routing the instant its login is fixed, rather than
3180
+ * waiting out a stale cooldown.
3181
+ */
2605
3182
  refresh(key, fetcher) {
2606
3183
  let pending = this.inflight.get(key);
2607
3184
  if (pending === void 0) {
@@ -2611,6 +3188,13 @@ var PoolUsageTracker = class {
2611
3188
  at: Date.now()
2612
3189
  });
2613
3190
  return snapshot;
3191
+ }, (error) => {
3192
+ if (!isMissingOrInvalidCredential(error)) this.entries.set(key, {
3193
+ error,
3194
+ at: Date.now(),
3195
+ cooldownMs: cooldownFor(error, this.ttlMs)
3196
+ });
3197
+ throw error;
2614
3198
  }).finally(() => {
2615
3199
  this.inflight.delete(key);
2616
3200
  });
@@ -2635,6 +3219,27 @@ var PoolUsageTracker = class {
2635
3219
  }
2636
3220
  };
2637
3221
  /**
3222
+ * The routing view of a fetch failure. Logged out: the member cannot serve
3223
+ * at all. Any other failure (network, endpoint rate limit) must not block
3224
+ * routing — the member stays available with a zero score, degrading the
3225
+ * strategy to plain priority order for it.
3226
+ */
3227
+ function degradedQuota(error) {
3228
+ return isMissingOrInvalidCredential(error) ? {
3229
+ available: false,
3230
+ urgency: 0,
3231
+ fetchedAt: 0
3232
+ } : {
3233
+ available: true,
3234
+ urgency: 0,
3235
+ fetchedAt: 0
3236
+ };
3237
+ }
3238
+ /** How long to hold a failure in the negative cache: the endpoint's own `retry-after`, or the default TTL. */
3239
+ function cooldownFor(error, defaultTtlMs) {
3240
+ return error instanceof OAuthEndpointError && error.retryAfterMs !== void 0 ? error.retryAfterMs : defaultTtlMs;
3241
+ }
3242
+ /**
2638
3243
  * Whether a window constrains this model: unscoped windows always do; a
2639
3244
  * model-scoped window (Claude's Opus/Sonnet lanes) applies when its scope
2640
3245
  * names the model family.
@@ -2701,6 +3306,14 @@ async function resolveImages(messages, attachments, signal) {
2701
3306
  })));
2702
3307
  }
2703
3308
 
3309
+ //#endregion
3310
+ //#region src/compat.ts
3311
+ /** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
3312
+ const ToolCallId = (() => {
3313
+ const exports = llm;
3314
+ return exports["ToolCallId"] ?? exports["CallId"];
3315
+ })();
3316
+
2704
3317
  //#endregion
2705
3318
  //#region src/translate/sse.ts
2706
3319
  /**
@@ -2895,7 +3508,7 @@ function closeBlock$2(block) {
2895
3508
  };
2896
3509
  case "tool-call": return {
2897
3510
  type: "tool-call",
2898
- id: CallId(block.callId),
3511
+ id: ToolCallId(block.callId),
2899
3512
  name: block.name ?? "",
2900
3513
  arguments: block.text
2901
3514
  };
@@ -2985,7 +3598,7 @@ var ResponsesStreamTranslator = class {
2985
3598
  chunks.push({
2986
3599
  type: "tool-call-delta",
2987
3600
  index: block.index,
2988
- id: CallId(callId),
3601
+ id: ToolCallId(callId),
2989
3602
  ...item.name === void 0 ? {} : { name: item.name },
2990
3603
  argumentsDelta: ""
2991
3604
  });
@@ -3027,7 +3640,7 @@ var ResponsesStreamTranslator = class {
3027
3640
  chunks.push({
3028
3641
  type: "tool-call-delta",
3029
3642
  index: block.index,
3030
- id: CallId(block.callId),
3643
+ id: ToolCallId(block.callId),
3031
3644
  ...block.name === void 0 ? {} : { name: block.name },
3032
3645
  argumentsDelta: event.delta ?? ""
3033
3646
  });
@@ -3120,6 +3733,28 @@ const CODEX_CONTEXT_WINDOW = 4e5;
3120
3733
  const CODEX_DEFAULT_MAX_TOKENS = 128e3;
3121
3734
  /** Refresh when the access token has less than this much life left. */
3122
3735
  const CODEX_PREEMPT_MS = 5 * 6e4;
3736
+ /**
3737
+ * Body fields the backend uses to name a reset. A window-exhaustion rejection
3738
+ * carries `usage_limit_reached` with the seconds left on the window — the case
3739
+ * that used to classify as a terminal quota and never be retried at all.
3740
+ */
3741
+ const CODEX_RESET_FIELDS = [
3742
+ "resets_in_seconds",
3743
+ "reset_after_seconds",
3744
+ "resets_at",
3745
+ "reset_at"
3746
+ ];
3747
+ /**
3748
+ * Reads the reset instant of the Codex window that rejected a request.
3749
+ *
3750
+ * Body only. The `x-codex-{primary,secondary}-reset-after-seconds` headers are
3751
+ * rollover snapshots the backend attaches to every response, one per window,
3752
+ * so they say nothing about which window refused: a burst 429 that would clear
3753
+ * in seconds still carries a primary rollover hours out, and reading it would
3754
+ * park the turn for those hours. They reach the operator through
3755
+ * `rateLimitDiagnostics` instead.
3756
+ */
3757
+ const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
3123
3758
  /** Default instruction when the request carries no system prompt. */
3124
3759
  const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, a coding agent based on GPT-5. Help the user with their software engineering tasks.";
3125
3760
  /** Refresh-grant rejections that mean the login is gone for good. */
@@ -3368,10 +4003,6 @@ const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
3368
4003
  * the range of current codex CLI releases.
3369
4004
  */
3370
4005
  const CODEX_CLIENT_VERSION = "0.147.0";
3371
- /** Display name for a wire reasoning-effort value. */
3372
- function effortName(effort) {
3373
- return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
3374
- }
3375
4006
  /**
3376
4007
  * Whether a catalog entry advertises the fast tier. Mirrors codex-rs
3377
4008
  * `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
@@ -3407,7 +4038,7 @@ async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
3407
4038
  if (entry.visibility === "hide" || entry.visibility === "none") continue;
3408
4039
  const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0).map((level) => ({
3409
4040
  id: ReasoningEffortId(level.effort),
3410
- name: effortName(level.effort),
4041
+ name: effortDisplayName(level.effort),
3411
4042
  ...level.description === void 0 ? {} : { description: level.description }
3412
4043
  }));
3413
4044
  const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
@@ -3545,6 +4176,9 @@ var CodexAdapter = class extends LlmAdapter {
3545
4176
  name: "ChatGPT (Codex)"
3546
4177
  };
3547
4178
  }
4179
+ providerRetryPolicy(provider) {
4180
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `codex: provider "${provider}" retryPolicy`);
4181
+ }
3548
4182
  staticModels(provider) {
3549
4183
  return this.options.models.map((model) => ({
3550
4184
  provider,
@@ -3633,6 +4267,10 @@ var CodexAdapter = class extends LlmAdapter {
3633
4267
  async resolveOwnModel(provider, model) {
3634
4268
  const discovered = await this.discovered(model);
3635
4269
  const configured = this.options.models.find((entry) => entry.id === model);
4270
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? {
4271
+ efforts: CODEX_EFFORTS,
4272
+ defaultEffort: CODEX_DEFAULT_EFFORT
4273
+ }, { extendable: discovered?.reasoning === void 0 });
3636
4274
  return {
3637
4275
  provider,
3638
4276
  id: model,
@@ -3641,10 +4279,7 @@ var CodexAdapter = class extends LlmAdapter {
3641
4279
  inputModalities: configured?.inputModalities ?? CODEX_MODALITIES,
3642
4280
  context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? CODEX_CONTEXT_WINDOW },
3643
4281
  defaultMaxTokens: configured?.maxTokens ?? CODEX_DEFAULT_MAX_TOKENS,
3644
- reasoning: discovered?.reasoning ?? {
3645
- efforts: CODEX_EFFORTS,
3646
- defaultEffort: CODEX_DEFAULT_EFFORT
3647
- }
4282
+ ...reasoning === void 0 ? {} : { reasoning }
3648
4283
  };
3649
4284
  }
3650
4285
  async *stream(options) {
@@ -3668,7 +4303,10 @@ var CodexAdapter = class extends LlmAdapter {
3668
4303
  session = await this.options.tokens.session(account, true);
3669
4304
  response = await this.request(options, session, watchdog.signal);
3670
4305
  }
3671
- if (!response.ok) throw await httpLlmError(response, "codex API");
4306
+ if (!response.ok) throw await httpLlmError(response, "codex API", {
4307
+ rateLimitReset: codexRateLimitReset,
4308
+ ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
4309
+ });
3672
4310
  if (response.body === null) throw new LlmError("codex API returned no response body", EMPTY_RESPONSE_CODE);
3673
4311
  yield* streamResponses(response.body, () => {
3674
4312
  watchdog.pulse();
@@ -3921,7 +4559,7 @@ function closeBlock$1(block) {
3921
4559
  };
3922
4560
  case "tool-call": return {
3923
4561
  type: "tool-call",
3924
- id: CallId(block.callId),
4562
+ id: ToolCallId(block.callId),
3925
4563
  name: block.name ?? "",
3926
4564
  arguments: block.text
3927
4565
  };
@@ -4024,7 +4662,7 @@ var AnthropicStreamTranslator = class {
4024
4662
  chunks.push({
4025
4663
  type: "tool-call-delta",
4026
4664
  index: opened.index,
4027
- id: CallId(opened.callId),
4665
+ id: ToolCallId(opened.callId),
4028
4666
  ...block.name === void 0 ? {} : { name: block.name },
4029
4667
  argumentsDelta: ""
4030
4668
  });
@@ -4061,7 +4699,7 @@ var AnthropicStreamTranslator = class {
4061
4699
  chunks.push({
4062
4700
  type: "tool-call-delta",
4063
4701
  index: block.index,
4064
- id: CallId(block.callId),
4702
+ id: ToolCallId(block.callId),
4065
4703
  ...block.name === void 0 ? {} : { name: block.name },
4066
4704
  argumentsDelta: delta.partial_json ?? ""
4067
4705
  });
@@ -4165,6 +4803,34 @@ const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
4165
4803
  /** Refresh when the access token has less than this much life left. */
4166
4804
  const CLAUDE_PREEMPT_MS = 5 * 6e4;
4167
4805
  /**
4806
+ * Body fields Anthropic uses to name a reset instant, read when the unified
4807
+ * headers are absent.
4808
+ */
4809
+ const CLAUDE_RESET_FIELDS = [
4810
+ "resets_at",
4811
+ "resetsAt",
4812
+ "reset_at",
4813
+ "retry_after"
4814
+ ];
4815
+ /**
4816
+ * Reads the reset instant of the Anthropic window that rejected a request.
4817
+ *
4818
+ * `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
4819
+ * Claude Code renders as "resets 3pm" — and is the only header that names the
4820
+ * window which actually rejected this request. The per-bucket
4821
+ * `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
4822
+ * headers are deliberately not read: they are rollover snapshots attached to
4823
+ * every response, so on a 429 they cannot say which bucket refused, and the
4824
+ * earliest of them is typically the bucket that still had room — a wait that
4825
+ * lands straight back in the closed window. They reach the operator through
4826
+ * `rateLimitDiagnostics` instead.
4827
+ */
4828
+ const claudeRateLimitReset = (response, body, now) => {
4829
+ const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
4830
+ if (unified !== void 0) return unified;
4831
+ return resetFromFields(jsonBody(body), CLAUDE_RESET_FIELDS, now);
4832
+ };
4833
+ /**
4168
4834
  * The subscription endpoint only serves requests presenting as Claude Code,
4169
4835
  * so these headers impersonate the CLI; the harness attribution user-agent
4170
4836
  * cannot be sent here (one user-agent slot, and the CLI's wins).
@@ -4432,14 +5098,6 @@ async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal) {
4432
5098
  if (models.length === 0) throw new Error("claude models API returned an empty catalog");
4433
5099
  return models;
4434
5100
  }
4435
- /**
4436
- * Claude Code's own SDK retry shape: exponential backoff starting at 1s,
4437
- * doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
4438
- * count of retries after the first attempt (Claude Code defaults to 10).
4439
- */
4440
- const CLAUDE_RETRY_INITIAL_DELAY_MS = 1e3;
4441
- const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
4442
- const CLAUDE_RETRY_JITTER_RATIO = .2;
4443
5101
  /** The Claude 4.5 family accepts image input. */
4444
5102
  const CLAUDE_MODALITIES = ["text", "image"];
4445
5103
  /**
@@ -4533,16 +5191,7 @@ var ClaudeAdapter = class extends LlmAdapter {
4533
5191
  };
4534
5192
  }
4535
5193
  providerRetryPolicy(provider) {
4536
- if (this.options.maxRetries === void 0) return void 0;
4537
- return resolveRetryPolicy({
4538
- mode: "normal",
4539
- maxRetries: this.options.maxRetries,
4540
- backoff: {
4541
- initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
4542
- maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
4543
- jitterRatio: CLAUDE_RETRY_JITTER_RATIO
4544
- }
4545
- }, `claude: provider "${provider}" retryPolicy`);
5194
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `claude: provider "${provider}" retryPolicy`);
4546
5195
  }
4547
5196
  async listModels(provider) {
4548
5197
  const own = await this.listOwnModels(provider);
@@ -4588,7 +5237,7 @@ var ClaudeAdapter = class extends LlmAdapter {
4588
5237
  async resolveOwnModel(provider, model) {
4589
5238
  const disc = await this.discovered(model);
4590
5239
  const configured = this.options.models.find((entry) => entry.id === model);
4591
- const reasoning = disc?.reasoning;
5240
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
4592
5241
  return {
4593
5242
  provider,
4594
5243
  id: model,
@@ -4620,7 +5269,10 @@ var ClaudeAdapter = class extends LlmAdapter {
4620
5269
  session = await this.options.tokens.session(account, true);
4621
5270
  response = await this.request(options, session, watchdog.signal);
4622
5271
  }
4623
- if (!response.ok) throw await httpLlmError(response, "claude API");
5272
+ if (!response.ok) throw await httpLlmError(response, "claude API", {
5273
+ rateLimitReset: claudeRateLimitReset,
5274
+ ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
5275
+ });
4624
5276
  if (response.body === null) throw new LlmError("claude API returned no response body", EMPTY_RESPONSE_CODE);
4625
5277
  yield* streamAnthropic(response.body, () => {
4626
5278
  watchdog.pulse();
@@ -4687,6 +5339,24 @@ const GROK_CONTEXT_WINDOW = 256e3;
4687
5339
  const GROK_DEFAULT_MAX_TOKENS = 32e3;
4688
5340
  /** Refresh when the access token has less than this much life left. */
4689
5341
  const GROK_PREEMPT_MS = 2 * 6e4;
5342
+ /** Body fields xAI uses to name a delay or reset. */
5343
+ const GROK_RESET_FIELDS = [
5344
+ "retry_after",
5345
+ "retry_after_seconds",
5346
+ "resets_at",
5347
+ "reset_at"
5348
+ ];
5349
+ /**
5350
+ * Reads the reset instant of the xAI window that rejected a request.
5351
+ *
5352
+ * Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
5353
+ * whose values are rollover durations (`6m0s`) present on every response, one
5354
+ * per bucket — on a 429 the earliest of them is usually a bucket with room
5355
+ * (`0s` for the request bucket while the token bucket is the one exhausted),
5356
+ * which would burn the whole retry budget in seconds. They reach the operator
5357
+ * through `rateLimitDiagnostics` instead.
5358
+ */
5359
+ const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
4690
5360
  /** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
4691
5361
  function assertXaiEndpoint(url, field) {
4692
5362
  let parsed;
@@ -5106,6 +5776,9 @@ var GrokAdapter = class extends LlmAdapter {
5106
5776
  name: "Grok (Subscription)"
5107
5777
  };
5108
5778
  }
5779
+ providerRetryPolicy(provider) {
5780
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
5781
+ }
5109
5782
  staticModels(provider) {
5110
5783
  return this.options.models.map((model) => ({
5111
5784
  provider,
@@ -5167,6 +5840,7 @@ var GrokAdapter = class extends LlmAdapter {
5167
5840
  async resolveOwnModel(provider, model) {
5168
5841
  const discovered = await this.discovered(model);
5169
5842
  const configured = this.options.models.find((entry) => entry.id === model);
5843
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
5170
5844
  return {
5171
5845
  provider,
5172
5846
  id: model,
@@ -5175,7 +5849,7 @@ var GrokAdapter = class extends LlmAdapter {
5175
5849
  inputModalities: configured?.inputModalities ?? grokModalities(model),
5176
5850
  context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
5177
5851
  defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
5178
- ...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
5852
+ ...reasoning === void 0 ? {} : { reasoning }
5179
5853
  };
5180
5854
  }
5181
5855
  async *stream(options) {
@@ -5199,7 +5873,10 @@ var GrokAdapter = class extends LlmAdapter {
5199
5873
  session = await this.options.tokens.session(account, true);
5200
5874
  response = await this.request(options, session, watchdog.signal);
5201
5875
  }
5202
- if (!response.ok) throw await httpLlmError(response, "grok API");
5876
+ if (!response.ok) throw await httpLlmError(response, "grok API", {
5877
+ rateLimitReset: grokRateLimitReset,
5878
+ ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
5879
+ });
5203
5880
  if (response.body === null) throw new LlmError("grok API returned no response body", EMPTY_RESPONSE_CODE);
5204
5881
  yield* streamResponses(response.body, () => {
5205
5882
  watchdog.pulse();
@@ -5221,6 +5898,7 @@ var GrokAdapter = class extends LlmAdapter {
5221
5898
  parallel_tool_calls: true,
5222
5899
  ...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
5223
5900
  ...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
5901
+ ...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
5224
5902
  store: false,
5225
5903
  stream: true
5226
5904
  };
@@ -5383,7 +6061,7 @@ function closeBlock(block) {
5383
6061
  };
5384
6062
  case "tool-call": return {
5385
6063
  type: "tool-call",
5386
- id: CallId(block.callId),
6064
+ id: ToolCallId(block.callId),
5387
6065
  name: block.name ?? "",
5388
6066
  arguments: block.text
5389
6067
  };
@@ -5537,7 +6215,7 @@ var ChatCompletionsStreamTranslator = class {
5537
6215
  chunks.push({
5538
6216
  type: "tool-call-delta",
5539
6217
  index: block.index,
5540
- id: CallId(block.callId),
6218
+ id: ToolCallId(block.callId),
5541
6219
  ...block.name === void 0 ? {} : { name: block.name },
5542
6220
  argumentsDelta: ""
5543
6221
  });
@@ -5547,7 +6225,7 @@ var ChatCompletionsStreamTranslator = class {
5547
6225
  chunks.push({
5548
6226
  type: "tool-call-delta",
5549
6227
  index: block.index,
5550
- id: CallId(block.callId),
6228
+ id: ToolCallId(block.callId),
5551
6229
  argumentsDelta: call.function.arguments
5552
6230
  });
5553
6231
  }
@@ -6096,6 +6774,9 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
6096
6774
  name: "GitHub Copilot"
6097
6775
  };
6098
6776
  }
6777
+ providerRetryPolicy(provider) {
6778
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `copilot: provider "${provider}" retryPolicy`);
6779
+ }
6099
6780
  staticModels(provider) {
6100
6781
  return this.options.models.map((model) => ({
6101
6782
  provider,
@@ -6258,6 +6939,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
6258
6939
  async resolveOwnModel(provider, model) {
6259
6940
  const discovered = await this.discovered(model);
6260
6941
  const configured = this.options.models.find((entry) => entry.id === model);
6942
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
6261
6943
  return {
6262
6944
  provider,
6263
6945
  id: model,
@@ -6266,7 +6948,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
6266
6948
  inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ["text"],
6267
6949
  context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
6268
6950
  defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
6269
- ...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
6951
+ ...reasoning === void 0 ? {} : { reasoning }
6270
6952
  };
6271
6953
  }
6272
6954
  async *stream(options) {
@@ -6293,7 +6975,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
6293
6975
  session = await this.options.tokens.session(account, true);
6294
6976
  response = await this.request(options, session, watchdog.signal, wire, scope);
6295
6977
  }
6296
- if (!response.ok) throw await httpLlmError(response, "copilot API");
6978
+ if (!response.ok) throw await httpLlmError(response, "copilot API", { ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn } });
6297
6979
  if (response.body === null) throw new LlmError("copilot API returned no response body", EMPTY_RESPONSE_CODE);
6298
6980
  const pulse = () => {
6299
6981
  watchdog.pulse();
@@ -6616,13 +7298,13 @@ function truncate$1(text, max = 60) {
6616
7298
  * image input; any resolution failure means "no".
6617
7299
  */
6618
7300
  async function routeDeclaresImageInput(resolveLlm, exec) {
6619
- const llm = resolveLlm?.();
7301
+ const llm$1 = resolveLlm?.();
6620
7302
  const routed = exec.agent?.session.requestHeader()?.config;
6621
7303
  const provider = routed?.provider ?? exec.agent?.options.provider;
6622
7304
  const model = routed?.model ?? exec.agent?.options.model;
6623
- if (llm === void 0 || provider === void 0 || model === void 0) return false;
7305
+ if (llm$1 === void 0 || provider === void 0 || model === void 0) return false;
6624
7306
  try {
6625
- return (await llm.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
7307
+ return (await llm$1.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
6626
7308
  } catch {
6627
7309
  return false;
6628
7310
  }
@@ -7111,6 +7793,10 @@ const Config = z.object({
7111
7793
  "copilot"
7112
7794
  ]),
7113
7795
  streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
7796
+ rateLimit: z.object({
7797
+ wait: z.boolean().default(true),
7798
+ maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
7799
+ }),
7114
7800
  models: z.object({
7115
7801
  codex: z.array(modelEntrySchema),
7116
7802
  claude: z.array(modelEntrySchema),
@@ -7273,18 +7959,20 @@ var SubscriptionsAuthController = class {
7273
7959
  * cannot be read off the flow manager.
7274
7960
  */
7275
7961
  claims = /* @__PURE__ */ new Map();
7276
- constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials) {
7962
+ constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0) {
7277
7963
  this.flows = flows;
7278
7964
  this.deviceFlows = deviceFlows;
7279
7965
  this.onAuthChanged = onAuthChanged;
7280
7966
  this.resolveAttachments = resolveAttachments;
7281
7967
  this.usageFetchers = usageFetchers;
7282
7968
  this.readClaudeCreds = readClaudeCreds;
7969
+ this.poolUsage = poolUsage;
7283
7970
  }
7284
- usage(provider, account, signal) {
7971
+ usage(provider, account, signal, force = false) {
7285
7972
  const fetcher = this.usageFetchers[provider];
7286
7973
  if (fetcher === void 0) return Promise.resolve({ supported: false });
7287
- return fetcher(account, signal);
7974
+ if (this.poolUsage === void 0) return fetcher(account, signal);
7975
+ return this.poolUsage.snapshotFor(provider, account, force);
7288
7976
  }
7289
7977
  async readImage(ref, signal) {
7290
7978
  const attachments = this.resolveAttachments();
@@ -7451,6 +8139,7 @@ function apply(ctx, config) {
7451
8139
  const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
7452
8140
  const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
7453
8141
  if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
8142
+ const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
7454
8143
  const catalog = resolveCatalog(config.models);
7455
8144
  const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
7456
8145
  const flows = new OAuthFlowManager();
@@ -7473,6 +8162,7 @@ function apply(ctx, config) {
7473
8162
  poolAdapter?.invalidate();
7474
8163
  for (const [route, handle] of handles) handle.replace([route]);
7475
8164
  };
8165
+ loadModelDefaults();
7476
8166
  let codexTokens;
7477
8167
  let claudeTokens;
7478
8168
  let grokTokens;
@@ -7501,11 +8191,13 @@ function apply(ctx, config) {
7501
8191
  adapter = new CodexAdapter({
7502
8192
  models: catalog.codex,
7503
8193
  streamIdleTimeoutMs,
8194
+ rateLimit,
7504
8195
  tokens,
7505
8196
  discovery: !overridden.has("codex"),
7506
8197
  onWarn,
7507
8198
  resolveAttachments,
7508
8199
  catalogStore: catalogStore("codex"),
8200
+ defaultEffortOf: (model) => defaultEffortOf("codex", model),
7509
8201
  pool: () => poolAdapter,
7510
8202
  speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
7511
8203
  });
@@ -7533,12 +8225,13 @@ function apply(ctx, config) {
7533
8225
  const adapter = new ClaudeAdapter({
7534
8226
  models: catalog.claude,
7535
8227
  streamIdleTimeoutMs,
8228
+ rateLimit,
7536
8229
  tokens,
7537
8230
  discovery: !overridden.has("claude"),
7538
8231
  onWarn,
7539
- maxRetries: 10,
7540
8232
  resolveAttachments,
7541
8233
  catalogStore: catalogStore("claude"),
8234
+ defaultEffortOf: (model) => defaultEffortOf("claude", model),
7542
8235
  pool: () => poolAdapter
7543
8236
  });
7544
8237
  adapters.set("claude", adapter);
@@ -7564,11 +8257,13 @@ function apply(ctx, config) {
7564
8257
  const adapter = new GrokAdapter({
7565
8258
  models: catalog.grok,
7566
8259
  streamIdleTimeoutMs,
8260
+ rateLimit,
7567
8261
  tokens,
7568
8262
  discovery: !overridden.has("grok"),
7569
8263
  onWarn,
7570
8264
  resolveAttachments,
7571
8265
  catalogStore: catalogStore("grok"),
8266
+ defaultEffortOf: (model) => defaultEffortOf("grok", model),
7572
8267
  pool: () => poolAdapter
7573
8268
  });
7574
8269
  adapters.set("grok", adapter);
@@ -7592,11 +8287,13 @@ function apply(ctx, config) {
7592
8287
  copilotAdapter = new CopilotAdapter({
7593
8288
  models: catalog.copilot,
7594
8289
  streamIdleTimeoutMs,
8290
+ rateLimit,
7595
8291
  tokens,
7596
8292
  discovery: !overridden.has("copilot"),
7597
8293
  onWarn,
7598
8294
  resolveAttachments,
7599
8295
  catalogStore: catalogStore("copilot"),
8296
+ defaultEffortOf: (model) => defaultEffortOf("copilot", model),
7600
8297
  pool: () => poolAdapter
7601
8298
  });
7602
8299
  adapters.set("copilot", copilotAdapter);
@@ -7667,7 +8364,7 @@ function apply(ctx, config) {
7667
8364
  onWarn
7668
8365
  });
7669
8366
  }
7670
- registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers), {
8367
+ registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage), {
7671
8368
  async speed(sessionId) {
7672
8369
  return {
7673
8370
  tier: speedBySession.get(sessionId) ?? "standard",
@@ -7682,6 +8379,63 @@ function apply(ctx, config) {
7682
8379
  get: () => proxyGetConfig(),
7683
8380
  set: (input) => proxySetConfig(input),
7684
8381
  test: (payload) => proxyTestConnection(payload.url, payload.proxy)
8382
+ }, {
8383
+ async catalog() {
8384
+ const visible = new Set((await ctx.llm.listProviders()).map((provider) => provider.id));
8385
+ const catalog$1 = [];
8386
+ for (const provider of PROVIDER_IDS) {
8387
+ if (!visible.has(provider)) continue;
8388
+ let models = [];
8389
+ try {
8390
+ models = await ctx.llm.listModels(provider);
8391
+ } catch {
8392
+ continue;
8393
+ }
8394
+ let tierIds = /* @__PURE__ */ new Set();
8395
+ try {
8396
+ const tiers = await poolAdapter?.modelsForProvider(provider);
8397
+ if (tiers !== void 0) tierIds = new Set(tiers.map((tier) => tier.id));
8398
+ } catch {}
8399
+ const views = [];
8400
+ for (const model of models) {
8401
+ if (tierIds.has(model.id)) continue;
8402
+ let info;
8403
+ try {
8404
+ info = await ctx.llm.resolveModelInfo(provider, model.id);
8405
+ } catch {
8406
+ continue;
8407
+ }
8408
+ if (info === void 0) continue;
8409
+ const override = defaultEffortOf(provider, model.id);
8410
+ views.push({
8411
+ id: model.id,
8412
+ name: model.name,
8413
+ efforts: info.reasoning?.efforts.map((effort) => ({
8414
+ id: effort.id,
8415
+ name: effort.name
8416
+ })) ?? [],
8417
+ ...override === void 0 ? {} : { configured: override }
8418
+ });
8419
+ }
8420
+ catalog$1.push({
8421
+ provider,
8422
+ models: views
8423
+ });
8424
+ }
8425
+ return catalog$1;
8426
+ },
8427
+ async set(provider, model, effort) {
8428
+ if (effort !== void 0) {
8429
+ let info;
8430
+ try {
8431
+ info = await ctx.llm.resolveModelInfo(provider, model);
8432
+ } catch {}
8433
+ const offered = info?.reasoning?.efforts ?? [];
8434
+ if (offered.length > 0 && !offered.some((entry) => entry.id === effort)) throw new BadRequest(`model ${model} does not advertise a "${effort}" reasoning effort`);
8435
+ }
8436
+ await setDefaultEffort(provider, model, effort);
8437
+ handles.get(provider)?.replace([provider]);
8438
+ }
7685
8439
  });
7686
8440
  if (claudeTokens !== void 0) {
7687
8441
  const tokens = claudeTokens;