@mars-sea/dsh-commandcode-provider 0.11.5 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -56,6 +56,11 @@ const KNOWN_EFFORTS = {
56
56
  "medium",
57
57
  "xhigh"
58
58
  ],
59
+ "Qwen/Qwen3.8-Omni-Flash": [
60
+ "low",
61
+ "medium",
62
+ "xhigh"
63
+ ],
59
64
  "claude-fable-5-1": [
60
65
  "low",
61
66
  "medium",
@@ -219,6 +224,11 @@ const KNOWN_EFFORTS = {
219
224
  "high",
220
225
  "max"
221
226
  ],
227
+ "z-ai/glm-5.3-flashx": [
228
+ "low",
229
+ "high",
230
+ "max"
231
+ ],
222
232
  "zai-org/GLM-5.2": ["high", "max"],
223
233
  "zai-org/GLM-5.3": [
224
234
  "low",
@@ -293,6 +303,7 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
293
303
  "Qwen/Qwen3.8-Flash",
294
304
  "Qwen/Qwen3.8-Max",
295
305
  "Qwen/Qwen3.8-Max-0902",
306
+ "Qwen/Qwen3.8-Omni-Flash",
296
307
  "claude-fable-5-1",
297
308
  "claude-fable-5",
298
309
  "claude-haiku-4-5-20251001",
@@ -334,7 +345,8 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
334
345
  "xai/grok-4.5",
335
346
  "xai/grok-4.6",
336
347
  "xiaomi/mimo-v2.5",
337
- "z-ai/glm-5.3-flash"
348
+ "z-ai/glm-5.3-flash",
349
+ "z-ai/glm-5.3-flashx"
338
350
  ]);
339
351
  /**
340
352
  * Models the official CLI's model table (command-code@1.53.0) marks
@@ -355,8 +367,9 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
355
367
  * `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
356
368
  * `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
357
369
  * command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
358
- * to `KNOWN_EFFORTS`. command-code@1.42.0 added `meituan/LongCat-2.0:free`
359
- * (reasoning:!0, no efforts). command-code@1.45.0 gave the Muse Spark family
370
+ * to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
371
+ * `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
372
+ * no efforts). command-code@1.45.0 gave the Muse Spark family
360
373
  * (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
361
374
  * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
362
375
  * command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
@@ -382,7 +395,7 @@ const KNOWN_THINKING_MODELS = /* @__PURE__ */ new Set([
382
395
  "thinkingmachines/inkling",
383
396
  "thinkingmachines/inkling-small",
384
397
  "poolside/laguna-s-2.1-free",
385
- "meituan/LongCat-2.0:free",
398
+ "meituan/LongCat-2.0",
386
399
  "inclusionai/ling-3.0-flash-sante:free"
387
400
  ]);
388
401
  /**
@@ -438,11 +451,14 @@ function requiresMessagesEndpoint(modelId) {
438
451
  * the official plan/pricing pages grants individual-provider/max/ultra and
439
452
  * teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
440
453
  * command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
441
- * `meituan/LongCat-2.0:free` (Go, free promo); command-code@1.43.0 added
454
+ * LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
455
+ * `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
442
456
  * `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
443
457
  * (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
444
458
  * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
445
- * added `deepseek/deepseek-v4.1-flash` (Go).
459
+ * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
460
+ * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
461
+ * `z-ai/glm-5.3-flashx` (Go).
446
462
  *
447
463
  * The Provider API exposes no plan metadata, so this snapshot is the source of
448
464
  * truth for the picker's plan annotation — it answers "which plan do I need to
@@ -465,13 +481,14 @@ const KNOWN_PLANS = {
465
481
  "Qwen/Qwen3.8-Flash": "go",
466
482
  "Qwen/Qwen3.8-Max": "go",
467
483
  "Qwen/Qwen3.8-Max-0902": "go",
484
+ "Qwen/Qwen3.8-Omni-Flash": "go",
468
485
  "deepseek/deepseek-v4-flash-fast": "go",
469
486
  "deepseek/deepseek-v4.1-flash": "go",
470
487
  "deepseek/deepseek-v4-flash": "go",
471
488
  "deepseek/deepseek-v4-flash-vision-exp": "go",
472
489
  "deepseek/deepseek-v4-pro": "go",
473
490
  "gpt-5.6-luna": "go",
474
- "meituan/LongCat-2.0:free": "go",
491
+ "meituan/LongCat-2.0": "go",
475
492
  "inclusionai/ling-3.0-flash-sante:free": "go",
476
493
  "meta/muse-spark-1.2-contributor": "go",
477
494
  "meta/muse-spark-1.3-contributor": "go",
@@ -492,6 +509,7 @@ const KNOWN_PLANS = {
492
509
  "xiaomi/mimo-v2.5": "go",
493
510
  "xiaomi/mimo-v2.5-pro": "go",
494
511
  "z-ai/glm-5.3-flash": "go",
512
+ "z-ai/glm-5.3-flashx": "go",
495
513
  "zai-org/GLM-5": "go",
496
514
  "zai-org/GLM-5.1": "go",
497
515
  "zai-org/GLM-5.2": "go",
@@ -652,6 +670,23 @@ function modelVisibleInPlan(modelId, access) {
652
670
  if (weight === void 0) return true;
653
671
  return weight <= access.tierWeight;
654
672
  }
673
+ /**
674
+ * Whether the picker lists `modelId` for a whole POOL of accounts: true when
675
+ * at least one of them includes it. The picker's plan filter is asked this
676
+ * rather than {@link modelVisibleInPlan} for a single account because the pool
677
+ * — not any one account — is what serves a request: keying the list on
678
+ * whichever account rotation happened to reach made models appear and vanish
679
+ * as accounts rotated, and hid models the user's other accounts could run.
680
+ *
681
+ * An account whose facts are unknown counts as "may include it" (the same
682
+ * fail-open rule `modelVisibleInPlan` applies one level down), and an empty
683
+ * pool is "show everything": hiding a model somebody can run is the one
684
+ * failure this filter must never cause.
685
+ */
686
+ function modelVisibleForAnyAccount(modelId, accounts) {
687
+ if (accounts === void 0 || accounts.length === 0) return true;
688
+ return accounts.some((access) => modelVisibleInPlan(modelId, access));
689
+ }
655
690
  const KNOWN_DEALS = {
656
691
  "MiniMaxAI/MiniMax-M3": { label: "50% off" },
657
692
  "xiaomi/mimo-v2.5-pro": { label: "99% off" },
@@ -660,10 +695,6 @@ const KNOWN_DEALS = {
660
695
  label: "FREE",
661
696
  free: true
662
697
  },
663
- "meituan/LongCat-2.0:free": {
664
- label: "FREE",
665
- free: true
666
- },
667
698
  "inclusionai/ling-3.0-flash-sante:free": {
668
699
  label: "FREE",
669
700
  free: true
@@ -1078,6 +1109,14 @@ const MODEL_PRICE_ROWS = [
1078
1109
  .03
1079
1110
  ]
1080
1111
  },
1112
+ {
1113
+ id: "glm-5.3-flashx",
1114
+ rates: [
1115
+ .37,
1116
+ 1.25,
1117
+ .075
1118
+ ]
1119
+ },
1081
1120
  {
1082
1121
  id: "gpt-5.3-codex",
1083
1122
  rates: [
@@ -1290,6 +1329,14 @@ const MODEL_PRICE_ROWS = [
1290
1329
  .3
1291
1330
  ]
1292
1331
  },
1332
+ {
1333
+ id: "longcat-2.0",
1334
+ rates: [
1335
+ .3,
1336
+ 1.2,
1337
+ .006
1338
+ ]
1339
+ },
1293
1340
  {
1294
1341
  id: "mimo-v2.5",
1295
1342
  rates: [
@@ -1507,6 +1554,14 @@ const MODEL_PRICE_ROWS = [
1507
1554
  .25
1508
1555
  ]
1509
1556
  },
1557
+ {
1558
+ id: "qwen-3.8-omni-flash",
1559
+ rates: [
1560
+ .15,
1561
+ .47,
1562
+ .016
1563
+ ]
1564
+ },
1510
1565
  {
1511
1566
  id: "step-3.5-flash",
1512
1567
  rates: [
@@ -1869,15 +1924,20 @@ function installCostProjection(ctx) {
1869
1924
  * usable, the routed account — resolving each slot's key lazily (literal
1870
1925
  * config key → credential seam → launch environment → the official CLI
1871
1926
  * auth file for the default slot only).
1872
- * - {@link CommandCodeAccountPool.markRejected} records a 429 (rate limit,
1873
- * window unknown) or 401 (invalid key, disabled until the config changes)
1874
- * against the exact API key, so several slots sharing one key share one
1875
- * state.
1927
+ * - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
1928
+ * the exact API key, so several slots sharing one key share one state. A
1929
+ * 429 that NAMED a usage window marks the window exhausted; a 429 that
1930
+ * named none is recorded as a plain throttle, which rotates the same way
1931
+ * but is never reported as an exhausted window (issue #54).
1876
1932
  * - When every account is marked, the pool probes each key's
1877
1933
  * `/alpha/billing/credits` window limits (through the injected
1878
1934
  * {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
1879
1935
  * no longer reports `exceeded` is revived, otherwise the pool throws a
1880
1936
  * `RATE_LIMIT` error naming the earliest reset time.
1937
+ * - An account the user explicitly asked for — the manual pin
1938
+ * ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
1939
+ * routed account — is probed the same way when a mark would demote it, so
1940
+ * a fallback is never permanent (issue #51).
1881
1941
  *
1882
1942
  * The pool is deliberately cordis-free (like the adapter): every host fact
1883
1943
  * arrives through injected thunks, so node tests can drive it directly.
@@ -1894,6 +1954,18 @@ function installCostProjection(ctx) {
1894
1954
  * window opens" into "fail now".
1895
1955
  */
1896
1956
  const RETRY_MAX_DELAY_MS = 9e5;
1957
+ /**
1958
+ * How long an explicitly selected account's rate-limit mark may stand before
1959
+ * its window is probed again. A 429 whose reset was never learned marks the
1960
+ * key `unknown`, which {@link accountUsable} refuses forever — deliberately,
1961
+ * because the all-marked revival pass is the only thing meant to clear it and
1962
+ * it costs one billing call per key. That is fine for a key nobody asked for,
1963
+ * and wrong for the one the user pinned: a single 429 would otherwise demote
1964
+ * the user's own selection until the process restarts (issue #51). The probe
1965
+ * is therefore allowed once per interval per key, which bounds an endpoint
1966
+ * that keeps failing while a successful probe resolves the mark for good.
1967
+ */
1968
+ const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
1897
1969
  /** A labeled, human-readable clock reading for error messages. */
1898
1970
  function clockLabel(ms) {
1899
1971
  return new Date(ms).toLocaleString();
@@ -1951,6 +2023,8 @@ var CommandCodeAccountPool = class {
1951
2023
  deps;
1952
2024
  /** Rotation state by API key. */
1953
2025
  states = /* @__PURE__ */ new Map();
2026
+ /** Last explicit-revival probe attempt by API key (throttles a failing probe). */
2027
+ explicitProbes = /* @__PURE__ */ new Map();
1954
2028
  constructor(deps) {
1955
2029
  this.deps = deps;
1956
2030
  }
@@ -2005,22 +2079,40 @@ var CommandCodeAccountPool = class {
2005
2079
  * `options.model` is the request's model id; routing rules re-read per
2006
2080
  * resolution, so a settings change applies live.
2007
2081
  *
2008
- * `options.exclude` skips one key during the probe-revival pass: the
2009
- * rotation hook excludes the just-rejected key so a probe that clears its
2010
- * window cannot re-offer the same key within the same request (the adapter
2011
- * refuses already-tried keys; the next request picks the revived key up).
2082
+ * `options.tried` lists the keys this request has already used — the
2083
+ * just-rejected one included. They are removed from the resolution entirely,
2084
+ * which is what lets one request walk a four-account pool: an account-scoped
2085
+ * rejection that does not mark the key (no credits, a model outside the
2086
+ * account's plan) would otherwise be offered again on every attempt, and the
2087
+ * accounts behind it would never be reached.
2088
+ *
2089
+ * An explicit selection (the pin or a model rule) that a rate-limit mark
2090
+ * would demote is probed before the fallback serves, so "falls back while
2091
+ * exhausted" never becomes "stays demoted until the process restarts".
2012
2092
  */
2013
2093
  async resolveKey(options) {
2014
2094
  const accounts = await this.resolvedAccounts();
2015
2095
  if (accounts.length === 0) return;
2016
- const routed = selectAccountForModel(accounts, options?.model ?? "", this.deps.modelAccountRules?.());
2017
- if (routed !== void 0) return this.pick(routed);
2096
+ const tried = options?.tried;
2097
+ const available = tried === void 0 || tried.length === 0 ? accounts : accounts.filter((account) => !tried.includes(account.key));
2018
2098
  const preferred = this.deps.preferredId?.();
2019
- const chosen = selectActiveAccount(accounts, preferred);
2020
- if (chosen !== void 0) return this.pick(chosen);
2021
- await Promise.all(accounts.map(async (account) => {
2099
+ if (available.length === 0) {
2100
+ if (selectActiveAccount(accounts, preferred) !== void 0) return void 0;
2101
+ throw allAccountsUnusable(accounts);
2102
+ }
2103
+ const routed = selectAccountForModel(available, options?.model ?? "", this.deps.modelAccountRules?.());
2104
+ if (routed !== void 0) return this.pick(routed);
2105
+ const chosen = selectActiveAccount(available, preferred);
2106
+ if (chosen !== void 0) {
2107
+ const explicit = this.explicitAccount(available, options?.model ?? "", preferred);
2108
+ if (explicit !== void 0 && explicit.key !== chosen.key && this.canProbeExplicit(explicit)) {
2109
+ const revived = await this.probeExplicit(explicit);
2110
+ if (revived !== void 0) return this.pick(revived);
2111
+ }
2112
+ return this.pick(chosen);
2113
+ }
2114
+ await Promise.all(available.map(async (account) => {
2022
2115
  if (account.state?.kind === "disabled") return;
2023
- if (options?.exclude !== void 0 && account.key === options.exclude) return;
2024
2116
  let probe;
2025
2117
  try {
2026
2118
  probe = await this.deps.probeWindow(account.key);
@@ -2028,39 +2120,71 @@ var CommandCodeAccountPool = class {
2028
2120
  return;
2029
2121
  }
2030
2122
  if (probe === void 0) return;
2031
- if (!probe.exceeded) this.states.delete(account.key);
2032
- else this.states.set(account.key, {
2123
+ if (!probe.exceeded) {
2124
+ this.states.delete(account.key);
2125
+ return;
2126
+ }
2127
+ if (probe.resetAt > 0) this.states.set(account.key, {
2033
2128
  kind: "cooldown",
2034
- reason: account.state?.reason ?? "rate limited (429)",
2129
+ cause: "window",
2130
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2035
2131
  until: probe.resetAt
2036
2132
  });
2133
+ else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2134
+ kind: "unknown",
2135
+ cause: "window",
2136
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2137
+ until: 0
2138
+ });
2037
2139
  }));
2038
- const latest = await this.resolvedAccounts();
2039
- const revived = selectActiveAccount(latest, preferred);
2140
+ const latestAccounts = await this.resolvedAccounts();
2141
+ const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
2040
2142
  if (revived !== void 0) return this.pick(revived);
2041
- if (latest.filter((account) => account.state?.kind === "disabled").length === latest.length) throw new LlmError(`llm-commandcode: every configured Command Code account (${latest.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${latest.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
2042
- const resets = latest.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2043
- const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2044
- const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
2045
- throw new LlmError(`llm-commandcode: all ${latest.length} Command Code account(s) have exhausted their usage window` + (earliest > 0 ? `; the earliest window resets at ${clockLabel(earliest)}` : "") + ` — requests will succeed again after the reset (or add another account);已用尽全部 ${latest.length} 个 Command Code 账户的用量窗口` + (earliest > 0 ? `,最早的重置时间为 ${clockLabel(earliest)}` : "") + "——窗口重置后请求会自动恢复(也可以添加更多账户)", "RATE_LIMIT", wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : void 0);
2143
+ if (selectActiveAccount(latestAccounts, preferred) !== void 0) return void 0;
2144
+ throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2046
2145
  }
2047
2146
  /**
2048
- * Record a rejection against one key. `rate-limit` (429) marks the key
2049
- * exhausted with an unknown reset (probed lazily at the next resolution
2050
- * once every account is marked); `invalid-credential` (401) disables the
2051
- * key until the stored credential changes.
2147
+ * Record a rejection against one key. `rate-limit` marks the key's usage
2148
+ * WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
2149
+ * body named the provider's own reset time, otherwise as an `unknown` mark
2150
+ * whose window is probed lazily. `throttled` (the plain 429 that named no
2151
+ * window) marks the key with the `throttle` cause instead: it leaves rotation
2152
+ * exactly like a window mark does, but the pool's own diagnosis keeps saying
2153
+ * "rate limited" rather than inventing an exhausted window. A 401
2154
+ * (`invalid-credential`) disables the key until the stored credential
2155
+ * changes.
2156
+ *
2157
+ * `resetAtMs` is seconds-to-millis converted by the adapter from the
2158
+ * provider's `error.rateLimit.reset`: knowing the real reset immediately is
2159
+ * what keeps an exhausted account out of rotation for exactly as long as the
2160
+ * provider said. It only applies to a window mark; a plain throttle keeps the
2161
+ * window unknown on purpose, so a probe can still find out.
2052
2162
  */
2053
- markRejected(apiKey, rejection) {
2163
+ markRejected(apiKey, rejection, resetAtMs) {
2054
2164
  if (rejection === "invalid-credential") this.states.set(apiKey, {
2055
2165
  kind: "disabled",
2166
+ cause: "auth",
2056
2167
  reason: "invalid API key (401)",
2057
2168
  until: 0
2058
2169
  });
2059
- else this.states.set(apiKey, {
2170
+ else if (rejection === "throttled") this.states.set(apiKey, {
2060
2171
  kind: "unknown",
2172
+ cause: "throttle",
2061
2173
  reason: "rate limited (429)",
2062
2174
  until: 0
2063
2175
  });
2176
+ else if (resetAtMs !== void 0 && resetAtMs > Date.now()) this.states.set(apiKey, {
2177
+ kind: "cooldown",
2178
+ cause: "window",
2179
+ reason: "usage window exhausted (429)",
2180
+ until: resetAtMs
2181
+ });
2182
+ else this.states.set(apiKey, {
2183
+ kind: "unknown",
2184
+ cause: "window",
2185
+ reason: "usage window exhausted (429)",
2186
+ until: 0
2187
+ });
2064
2188
  }
2065
2189
  /**
2066
2190
  * One account's key: literal → credential seam → auth file (default slot).
@@ -2096,6 +2220,74 @@ var CommandCodeAccountPool = class {
2096
2220
  }
2097
2221
  }
2098
2222
  }
2223
+ /**
2224
+ * The account the user explicitly asked for: the slot a model rule routes
2225
+ * the request to when that id exists among the resolved slots, else the
2226
+ * manually pinned slot. Consulted only on the fallback path — a usable
2227
+ * routed account already returned above — so unlike
2228
+ * {@link selectAccountForModel} it does NOT require the account to be
2229
+ * usable, which is exactly what {@link resolveKey} re-probes.
2230
+ */
2231
+ explicitAccount(accounts, model, preferredId) {
2232
+ const rule = matchModelRule(model, this.deps.modelAccountRules?.());
2233
+ const id = rule !== void 0 && accounts.some((account) => account.slot.id === rule.account) ? rule.account : preferredId;
2234
+ return id === void 0 ? void 0 : accounts.find((account) => account.slot.id === id);
2235
+ }
2236
+ /**
2237
+ * Whether an explicitly selected account's mark is due for a window probe.
2238
+ * Only an `unknown` mark (a 429 whose reset was never learned) is worth
2239
+ * re-probing: a `cooldown` already carries its reset time and expires by
2240
+ * itself, and a `disabled` (401) key stays out until the stored credential
2241
+ * changes. The interval bounds a probe endpoint that keeps failing.
2242
+ */
2243
+ canProbeExplicit(account) {
2244
+ if (account.state?.kind !== "unknown") return false;
2245
+ const last = this.explicitProbes.get(account.key);
2246
+ return last === void 0 || this.now() - last >= EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS;
2247
+ }
2248
+ /** The clock the probe throttle reads; injected so a test can travel in time. */
2249
+ now() {
2250
+ return this.deps.now?.() ?? Date.now();
2251
+ }
2252
+ /**
2253
+ * Probe one explicitly selected account's window and apply the answer. A
2254
+ * window that is no longer exceeded drops the mark, so the user's own
2255
+ * selection serves again on this very request. An exceeded one is stamped
2256
+ * as a cooldown carrying the provider's reset time, after which
2257
+ * {@link accountUsable} lets the account back in with no further probe. A
2258
+ * probe that fails changes nothing: the mark stays `unknown` and the next
2259
+ * attempt waits out the interval.
2260
+ */
2261
+ async probeExplicit(account) {
2262
+ this.explicitProbes.set(account.key, this.now());
2263
+ let probe;
2264
+ try {
2265
+ probe = await this.deps.probeWindow(account.key);
2266
+ } catch {
2267
+ return;
2268
+ }
2269
+ if (probe === void 0) return void 0;
2270
+ if (!probe.exceeded) {
2271
+ this.states.delete(account.key);
2272
+ return {
2273
+ slot: account.slot,
2274
+ key: account.key,
2275
+ state: void 0
2276
+ };
2277
+ }
2278
+ if (probe.resetAt > 0) this.states.set(account.key, {
2279
+ kind: "cooldown",
2280
+ cause: "window",
2281
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2282
+ until: probe.resetAt
2283
+ });
2284
+ else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2285
+ kind: "unknown",
2286
+ cause: "window",
2287
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2288
+ until: 0
2289
+ });
2290
+ }
2099
2291
  /** Hand out the chosen account's key. */
2100
2292
  pick(account) {
2101
2293
  return {
@@ -2105,6 +2297,41 @@ var CommandCodeAccountPool = class {
2105
2297
  }
2106
2298
  };
2107
2299
  /**
2300
+ * The error for "accounts exist but none of them can serve". Three shapes, each
2301
+ * claiming only what the pool actually knows:
2302
+ *
2303
+ * - every account rejected with 401 → `INVALID_CREDENTIAL`;
2304
+ * - at least one account marked by a NAMED usage window (or by a probe that
2305
+ * read a window as exceeded) → the window diagnosis, naming the earliest
2306
+ * known reset when the provider published one;
2307
+ * - every mark a plain throttle (a 429 that said nothing about a window, and
2308
+ * no probe could confirm one) → a throttle diagnosis that says so. Claiming
2309
+ * "all accounts have exhausted their usage window" here is issue #54: the
2310
+ * report's account panel showed the five-hour window at 2% and the weekly
2311
+ * one at 10% while the turn failed with exactly that sentence.
2312
+ *
2313
+ * Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
2314
+ * and by its already-tried diagnosis, so the two can never drift apart.
2315
+ *
2316
+ * The attached `providerRetryAfterMs` is the exact wait until the earliest
2317
+ * known window reset, so dsh-llm-retry sleeps through the window instead of
2318
+ * polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
2319
+ * the executor honors a provider wait verbatim only at or below the policy's
2320
+ * `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
2321
+ * (normal mode), which would turn "poll until the window opens" into "fail
2322
+ * now". Longer resets simply ride the capped local backoff and the probe
2323
+ * revival. A throttle diagnosis attaches none: its marks carry no reset.
2324
+ */
2325
+ function allAccountsUnusable(accounts) {
2326
+ if (accounts.filter((account) => account.state?.kind === "disabled").length === accounts.length) return new LlmError(`llm-commandcode: every configured Command Code account (${accounts.length}) was rejected with 401 — check the stored API keys (Models page / settings) or the auth file;已配置的 ${accounts.length} 个 Command Code 账户密钥均被拒绝(401)——请在设置页检查存储的 API 密钥,或重新运行 command-code login`, "INVALID_CREDENTIAL");
2327
+ const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
2328
+ if (windowMarked.length === 0) return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying;全部 ${accounts.length} 个 Command Code 账户被限流(429)——服务商未报告用量窗口用尽,正在重试`, "RATE_LIMIT");
2329
+ const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2330
+ const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2331
+ const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
2332
+ return new LlmError(`llm-commandcode: all ${accounts.length} Command Code account(s) have exhausted their usage window` + (earliest > 0 ? `; the earliest window resets at ${clockLabel(earliest)}` : " — the provider published no reset time for it") + ` — requests will succeed again after the reset (or add another account);已用尽全部 ${accounts.length} 个 Command Code 账户的用量窗口` + (earliest > 0 ? `,最早的重置时间为 ${clockLabel(earliest)}` : ",服务商未公布重置时间") + "——窗口重置后请求会自动恢复(也可以添加更多账户)", "RATE_LIMIT", wait > 0 && wait <= 9e5 ? { providerRetryAfterMs: wait } : void 0);
2333
+ }
2334
+ /**
2108
2335
  * Normalize one resolved credential to the form every consumer sees. Trim
2109
2336
  * only: a blank-after-trim value means "no key" (the slot is omitted and the
2110
2337
  * caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
@@ -2353,7 +2580,7 @@ function scaleBy(size, factor) {
2353
2580
  * and API key or subscription, and Command Code's terms apply.
2354
2581
  *
2355
2582
  * Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
2356
- * re-verified against command-code@1.54.0 — endpoints, request shape, and
2583
+ * re-verified against command-code@1.58.0 — endpoints, request shape, and
2357
2584
  * stream events unchanged):
2358
2585
  * POST {apiBase}/alpha/generate
2359
2586
  * body: { config, memory, taste, skills, params: { model, messages, tools,
@@ -2366,7 +2593,7 @@ function scaleBy(size, factor) {
2366
2593
  * per-request options thunk and an API-key resolver from the plugin entry
2367
2594
  * (src/index.ts), so a settings change reaches the very next request.
2368
2595
  */
2369
- const COMMAND_CODE_CLI_VERSION = "1.54.0";
2596
+ const COMMAND_CODE_CLI_VERSION = "1.58.0";
2370
2597
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2371
2598
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2372
2599
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -2404,15 +2631,22 @@ function booleanValue(value) {
2404
2631
  return typeof value === "boolean" ? value : void 0;
2405
2632
  }
2406
2633
  /**
2407
- * Thinking text carried by an OpenRouter-shaped `reasoning_details` array
2408
- * (`[{ type: 'reasoning.text', text, format, index }]`), concatenated in the
2409
- * order the gateway sent it.
2634
+ * Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
2635
+ * concatenated in the order the gateway sent it.
2636
+ *
2637
+ * Entries are read by their own text members rather than filtered on `type`:
2638
+ * DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
2639
+ * sends `{ type: 'reasoning.summary', summary, format: 'openai-responses-v1' }`
2640
+ * — the same summary text the scalar `delta.reasoning` carries, in the
2641
+ * Responses protocol's vocabulary. A `type` filter would drop one family or the
2642
+ * other, and losing thinking is the failure this exists to prevent.
2643
+ *
2644
+ * `text` wins per entry, but an EMPTY `text` must not shadow a populated
2645
+ * `summary` (the Responses wire emits `text: ''` placeholders), so the fallback
2646
+ * tests for non-empty rather than merely present. Entries carrying neither
2647
+ * member — the encrypted blobs — contribute nothing, and no entry contributes
2648
+ * both, so a summary is never counted twice.
2410
2649
  *
2411
- * Entries are read by their `text` member alone rather than filtered on
2412
- * `type === 'reasoning.text'`: the sibling shapes in that protocol carry
2413
- * `summary` or an encrypted blob instead of `text`, so a plain read already
2414
- * ignores them, while a type filter would silently drop a text-bearing variant
2415
- * added later — and losing thinking is the failure this exists to prevent.
2416
2650
  * Returns undefined when the array yields nothing, so the caller's `??` chain
2417
2651
  * keeps falling through instead of treating "no text" as a reasoning delta.
2418
2652
  */
@@ -2421,10 +2655,24 @@ function reasoningDetailsText(value) {
2421
2655
  let text = "";
2422
2656
  for (const entry of value) {
2423
2657
  if (!isRecord(entry)) continue;
2424
- text += stringValue(entry.text) ?? "";
2658
+ const exact = stringValue(entry.text);
2659
+ const summary = stringValue(entry.summary);
2660
+ text += exact !== void 0 && exact !== "" ? exact : summary ?? "";
2425
2661
  }
2426
2662
  return text === "" ? void 0 : text;
2427
2663
  }
2664
+ /**
2665
+ * One SCALAR thinking delta, treating an empty string as "this chunk carries
2666
+ * none" — the same rule {@link reasoningDetailsText} applies inside the array.
2667
+ * The scalar and the array describe the same thinking, so an empty spelling of
2668
+ * one must not shadow populated text in the other (the Responses wire pads with
2669
+ * `text: ''`). `??` alone cannot express this: an empty string is not nullish,
2670
+ * so `reasoning: ''` beside a populated `reasoning_details` used to drop the
2671
+ * whole chunk's thinking.
2672
+ */
2673
+ function nonEmptyText(value) {
2674
+ return value === void 0 || value === "" ? void 0 : value;
2675
+ }
2428
2676
  /** Parse a billing-period timestamp (ISO string or millis) into millis; 0 when absent/invalid. */
2429
2677
  function periodEndValue(value) {
2430
2678
  const asNumber = numberValue(value);
@@ -3674,11 +3922,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
3674
3922
  };
3675
3923
  };
3676
3924
  if (opts?.unfiltered === true) return catalog.map(toInfo).sort(compareByPlan);
3677
- const access = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadBillingAccess();
3925
+ const accesses = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadPoolBillingAccess();
3678
3926
  const visible = this.deps.options().visibleModels;
3679
3927
  const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
3680
3928
  const overrides = this.deps.options().modelVisibility;
3681
- return catalog.filter((model) => modelVisibleInPlan(model.id, access)).filter((model) => {
3929
+ return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => {
3682
3930
  const override = overrides?.[model.id];
3683
3931
  if (typeof override === "boolean") return override;
3684
3932
  return allow === void 0 || allow.has(model.id);
@@ -3734,17 +3982,44 @@ var CommandCodeAdapter = class extends LlmAdapter {
3734
3982
  };
3735
3983
  }
3736
3984
  /**
3737
- * The billing facts behind the picker's plan filter, cached for
3738
- * {@link BILLING_ACCESS_TTL_MS} and shared across concurrent callers.
3739
- * `undefined` means "unknown — show everything" (fail-open).
3985
+ * Every account's billing facts behind the picker's plan filter, in the
3986
+ * pool's rotation order and cached per key for {@link BILLING_ACCESS_TTL_MS}
3987
+ * (so the extra accounts cost their three requests once per TTL, not once
3988
+ * per picker load). `undefined` means the filter cannot be evaluated at all
3989
+ * — no key resolved — which {@link modelVisibleForAnyAccount} reads as
3990
+ * "show everything".
3991
+ *
3992
+ * The serving account is only the first entry: with several accounts the
3993
+ * model list must not depend on which of them rotation happens to be using.
3740
3994
  */
3741
- async loadBillingAccess() {
3742
- let apiKey;
3995
+ async loadPoolBillingAccess() {
3996
+ const keys = await this.poolAccountKeys();
3997
+ if (keys.length === 0) return void 0;
3998
+ return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
3999
+ }
4000
+ /**
4001
+ * The API keys the picker's plan filter must consult: every account the host
4002
+ * can serve from, in rotation order, when the host exposes one. A host
4003
+ * without a pool (or without the seam) reports just the key that would serve
4004
+ * this request — exactly the pre-pool behaviour.
4005
+ */
4006
+ async poolAccountKeys() {
4007
+ try {
4008
+ const usable = (await this.deps.resolveAccountKeys?.() ?? []).filter((key) => typeof key === "string" && key !== "");
4009
+ if (usable.length > 0) return usable;
4010
+ } catch {}
3743
4011
  try {
3744
- apiKey = await this.deps.resolveApiKey(this.deps.options());
4012
+ return [await this.deps.resolveApiKey(this.deps.options())];
3745
4013
  } catch {
3746
- return;
4014
+ return [];
3747
4015
  }
4016
+ }
4017
+ /**
4018
+ * One account's billing facts, cached for {@link BILLING_ACCESS_TTL_MS} and
4019
+ * shared across concurrent callers. `undefined` means "unknown — show
4020
+ * everything" (fail-open).
4021
+ */
4022
+ async loadBillingAccessForKey(apiKey) {
3748
4023
  const cached = this.billingAccess.get(apiKey);
3749
4024
  if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
3750
4025
  const existing = this.billingAccessInflight.get(apiKey);
@@ -3906,14 +4181,29 @@ var CommandCodeAdapter = class extends LlmAdapter {
3906
4181
  return report;
3907
4182
  }
3908
4183
  /**
3909
- * Probe one account's five-hour window from `/alpha/billing/credits`. The
4184
+ * Probe one account's real usage windows from `/alpha/billing/credits`. The
3910
4185
  * multi-account pool calls this when every account is marked exhausted: an
3911
- * account whose window no longer reports `exceeded` is revived, and the
3912
- * `resetAt` values feed the "earliest reset" error message. Returns
3913
- * `undefined` when the probe itself failed (transport, non-200, or a
3914
- * payload without window limits) — a failed probe never changes pool state.
4186
+ * account whose windows no longer report `exceeded` is revived, and the
4187
+ * `resetAt` values feed the "earliest reset" error message.
4188
+ *
4189
+ * BOTH windows the endpoint publishes are read, not just `fiveHour`. An
4190
+ * account can have an open five-hour window and an exhausted WEEKLY quota at
4191
+ * the same time (the two are metered separately), and reading only the
4192
+ * shorter one revived such an account as "usable" — the pool then handed it
4193
+ * out, the provider rejected the request again, and the next all-marked pass
4194
+ * revived it once more, so a used-up account kept coming back (issue #51's
4195
+ * follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
4196
+ * published window is exceeded, and `resetAt` is the LATEST reset among the
4197
+ * exceeded ones — the binding constraint, because a clear five-hour window
4198
+ * buys nothing while the weekly quota is spent.
4199
+ *
4200
+ * Returns `undefined` when the probe itself failed (transport, non-200, or a
4201
+ * payload with no window limits at all) — a failed probe never changes pool
4202
+ * state. The monthly/purchased/free credit balances are deliberately NOT part
4203
+ * of this answer: they are balances rather than windows, they carry no reset
4204
+ * time, and the server remains the final gate on them.
3915
4205
  */
3916
- async probeFiveHourWindow(apiKey) {
4206
+ async probeWindowLimits(apiKey) {
3917
4207
  try {
3918
4208
  const connection = this.deps.options();
3919
4209
  const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
@@ -3924,11 +4214,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
3924
4214
  const parsed = await response.json();
3925
4215
  if (!isRecord(parsed)) return void 0;
3926
4216
  const windowLimits = isRecord(parsed.windowLimits) ? parsed.windowLimits : void 0;
3927
- const fiveHour = windowLimits && isRecord(windowLimits.fiveHour) ? windowLimits.fiveHour : void 0;
3928
- if (fiveHour === void 0) return void 0;
4217
+ if (windowLimits === void 0) return void 0;
4218
+ let reported = false;
4219
+ let exceeded = false;
4220
+ let resetAt = 0;
4221
+ for (const raw of [windowLimits.fiveHour, windowLimits.weekly]) {
4222
+ const window = isRecord(raw) ? parseWindowLimit(raw) : void 0;
4223
+ if (window === void 0) continue;
4224
+ reported = true;
4225
+ if (!window.exceeded) continue;
4226
+ exceeded = true;
4227
+ resetAt = Math.max(resetAt, window.resetAt);
4228
+ }
4229
+ if (!reported) return void 0;
3929
4230
  return {
3930
- exceeded: fiveHour.exceeded === true,
3931
- resetAt: numberValue(fiveHour.resetAt) ?? 0
4231
+ exceeded,
4232
+ resetAt: exceeded ? resetAt : 0
3932
4233
  };
3933
4234
  } catch {
3934
4235
  return;
@@ -4002,13 +4303,24 @@ var CommandCodeAdapter = class extends LlmAdapter {
4002
4303
  }
4003
4304
  }
4004
4305
  const rotate = this.deps.rotateApiKey;
4005
- if ((attempt.status === 429 || attempt.status === 401) && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4006
- const next = await rotate(apiKey, attempt.status === 429 ? "rate-limit" : "invalid-credential", connection, options.model);
4306
+ const rejection = classifyAccountRejection(attempt.status, attempt.errText);
4307
+ if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4308
+ const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4309
+ tried: [...tried],
4310
+ ...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
4311
+ });
4007
4312
  if (next !== void 0 && !tried.has(next)) {
4008
4313
  apiKey = next;
4009
4314
  continue;
4010
4315
  }
4011
4316
  }
4317
+ if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4318
+ const reset = rejection.resetAtMs;
4319
+ const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4320
+ const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4321
+ const window = rejection.reason === "rate-limit";
4322
+ throw new LlmError((window ? "llm-commandcode: the Command Code account is rate limited" : "llm-commandcode: the Command Code account is rate limited (429) — the provider did not report an exhausted usage window") + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + (window ? ";当前 Command Code 账户已被限流" : ";当前 Command Code 账户被限流(429),服务商未报告用量窗口用尽") + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
4323
+ }
4012
4324
  throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
4013
4325
  }
4014
4326
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
@@ -4284,7 +4596,7 @@ function handleOpenAIEvent(asm, event) {
4284
4596
  }
4285
4597
  const choice = isRecord(choices[0]) ? choices[0] : {};
4286
4598
  const delta = isRecord(choice.delta) ? choice.delta : {};
4287
- const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
4599
+ const reasoningDelta = nonEmptyText(stringValue(delta.reasoning)) ?? nonEmptyText(stringValue(delta.reasoning_content)) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
4288
4600
  if (reasoningDelta !== "") {
4289
4601
  chunks.push(...closeText(asm));
4290
4602
  if (asm.reasoningIndex < 0) {
@@ -4400,6 +4712,112 @@ function mapFinishReason(reason) {
4400
4712
  * back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
4401
4713
  * ordinary errors so real account/model problems are not masked.
4402
4714
  */
4715
+ /**
4716
+ * Classify a pre-stream rejection that is about the ACCOUNT rather than about
4717
+ * the request, using the official CLI's own rules (command-code@1.56.0:
4718
+ * `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
4719
+ * `parseSpendCapError` and the terminal-marker list
4720
+ * `["premium_credits_exhausted", "model_not_in_plan", "insufficient credits"]`).
4721
+ * Undefined means "not an account-scoped rejection": a malformed request, an
4722
+ * oversized body or a context overflow must fail fast instead of walking the
4723
+ * whole account pool.
4724
+ *
4725
+ * Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
4726
+ * every other account-scoped rejection was terminal AND invisible to the pool:
4727
+ * an account that could not pay, or could not run the model, stayed "usable",
4728
+ * was handed out again on the next request, and produced the same error again
4729
+ * ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
4730
+ * the host should do with them, which is the caller's decision, not this
4731
+ * function's:
4732
+ *
4733
+ * - `rate-limit`: a usage WINDOW is spent. The code `RATE_LIMITED` is the
4734
+ * authoritative signal — the CLI accepts the code OR a 429 status — and the
4735
+ * body's `error.rateLimit` names the window and its `reset` (in seconds),
4736
+ * so the host can hold the account out until the provider's own reset time.
4737
+ * - `throttled`: the provider refused the request (429 or a bare
4738
+ * `RATE_LIMITED` code) without naming a usage window. It is account-scoped
4739
+ * like the above, so it rotates and marks the key the same way, but it is
4740
+ * NOT evidence that a metered window is spent: the CLI's own
4741
+ * `parseWindowLimitError` returns a window limit only when
4742
+ * `resolveWindowLabel` can name one, and a burst/model/spend limiter the
4743
+ * billing endpoint cannot see is exactly this shape (issue #54's report: a
4744
+ * 429 whose probe read five-hour 2% and weekly 10% while the error claimed
4745
+ * both were exhausted).
4746
+ * - `invalid-credential`: 401; the key itself is bad.
4747
+ * - `unavailable`: the key is fine but this account cannot serve: no credits
4748
+ * (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
4749
+ * `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
4750
+ * or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
4751
+ * the account: the fact is about the model or the balance, it can change
4752
+ * without the key changing, and a `:free` model can still be served by an
4753
+ * account with an empty balance.
4754
+ */
4755
+ function classifyAccountRejection(status, errText) {
4756
+ if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
4757
+ const evidence = readWindowLimitEvidence(errText);
4758
+ if (evidence.window === void 0) return { reason: "throttled" };
4759
+ return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
4760
+ reason: "rate-limit",
4761
+ resetAtMs: evidence.resetAtMs
4762
+ };
4763
+ }
4764
+ if (status === 401) return { reason: "invalid-credential" };
4765
+ if (status < 400 || status >= 500) return void 0;
4766
+ const lower = errText.toLowerCase().replaceAll("_", " ");
4767
+ const code = readProviderErrorCode(errText)?.toUpperCase();
4768
+ if (code === "USAGE_EXCEEDED" || code === "INSUFFICIENT_CREDITS" || code === "PREMIUM_CREDITS_EXHAUSTED" || code === "MODEL_NOT_IN_PLAN" || lower.includes("insufficient credits") || lower.includes("premium credits exhausted") || lower.includes("model not in plan") || /insufficient (credit|balance)/.test(lower) || /out of credit/.test(lower) || /not (available|included)[^.]{0,40}plan/.test(lower) || lower.includes("upgrade your plan")) return { reason: "unavailable" };
4769
+ }
4770
+ /** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
4771
+ function readProviderErrorCode(errText) {
4772
+ try {
4773
+ const parsed = JSON.parse(errText);
4774
+ if (!isRecord(parsed)) return void 0;
4775
+ const error = isRecord(parsed.error) ? parsed.error : parsed;
4776
+ return stringValue(error.code) ?? stringValue(error.type);
4777
+ } catch {
4778
+ return;
4779
+ }
4780
+ }
4781
+ /**
4782
+ * The usage-window evidence carried by a rejected request, mirroring the CLI's
4783
+ * `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
4784
+ *
4785
+ * `window` is present only when the body actually names one: a
4786
+ * `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
4787
+ * "usage limit for your plan" wording (read as `weekly` when it says so, else
4788
+ * `fiveHour`). That is the CLI's own bar for calling a rejection a WINDOW
4789
+ * limit, and it is what keeps a bare 429 — a burst limiter, a model-level
4790
+ * limit, a spend cap `windowLimits` cannot show — from being reported as an
4791
+ * exhausted window (issue #54).
4792
+ *
4793
+ * `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
4794
+ * SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
4795
+ * message is honored as the fallback. Either can be present without the other;
4796
+ * the window label is what decides the classification.
4797
+ */
4798
+ function readWindowLimitEvidence(errText) {
4799
+ let named;
4800
+ let message = "";
4801
+ let seconds;
4802
+ try {
4803
+ const parsed = JSON.parse(errText);
4804
+ if (isRecord(parsed)) {
4805
+ const error = isRecord(parsed.error) ? parsed.error : parsed;
4806
+ const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
4807
+ named = stringValue(rateLimit?.window);
4808
+ message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
4809
+ seconds = numberValue(rateLimit?.reset);
4810
+ }
4811
+ } catch {}
4812
+ const window = named === "fiveHour" || named === "weekly" || named === "daily" ? named : /usage limit for your plan/i.test(message) || /usage limit for your plan/i.test(errText) ? /weekly/i.test(message) ? "weekly" : "fiveHour" : void 0;
4813
+ const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1] ?? /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
4814
+ const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
4815
+ const resetAtMs = seconds !== void 0 && seconds > 0 ? Math.round(seconds * 1e3) : stamped !== void 0 && !Number.isNaN(stamped) ? stamped : void 0;
4816
+ return {
4817
+ ...window === void 0 ? {} : { window },
4818
+ ...resetAtMs === void 0 ? {} : { resetAtMs }
4819
+ };
4820
+ }
4403
4821
  function isUpgradeRequiredError(status, errText) {
4404
4822
  if (status !== 403) return false;
4405
4823
  const lower = errText.toLowerCase();
@@ -6335,7 +6753,7 @@ function apply(ctx, config) {
6335
6753
  slots,
6336
6754
  resolveRef,
6337
6755
  authFileKey: resolveAuthFileApiKey,
6338
- probeWindow: (apiKey) => adapter.probeFiveHourWindow(apiKey),
6756
+ probeWindow: (apiKey) => adapter.probeWindowLimits(apiKey),
6339
6757
  preferredId,
6340
6758
  modelAccountRules: () => current().modelAccountRules ?? []
6341
6759
  });
@@ -6348,10 +6766,11 @@ function apply(ctx, config) {
6348
6766
  const adapter = new CommandCodeAdapter({
6349
6767
  options,
6350
6768
  resolveApiKey,
6351
- rotateApiKey: async (rejectedKey, rejection, _connection, model) => {
6352
- pool.markRejected(rejectedKey, rejection);
6353
- const resolved = await pool.resolveKey(model === void 0 ? { exclude: rejectedKey } : {
6354
- exclude: rejectedKey,
6769
+ rotateApiKey: async (rejectedKey, rejection, _connection, model, rotation) => {
6770
+ if (rejection !== "unavailable") pool.markRejected(rejectedKey, rejection, rotation?.resetAtMs);
6771
+ const tried = rotation?.tried?.length ? rotation.tried : [rejectedKey];
6772
+ const resolved = await pool.resolveKey(model === void 0 ? { tried } : {
6773
+ tried,
6355
6774
  model
6356
6775
  });
6357
6776
  return resolved === void 0 ? void 0 : assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
@@ -6359,6 +6778,9 @@ function apply(ctx, config) {
6359
6778
  resolveAttachments: () => {
6360
6779
  const attachments = ctx.get("attachments");
6361
6780
  return attachments === void 0 ? void 0 : attachments;
6781
+ },
6782
+ resolveAccountKeys: async () => {
6783
+ return (await pool.resolvedAccounts()).map((account) => account.key);
6362
6784
  }
6363
6785
  });
6364
6786
  ctx.llm.registerConfigurableProviders([{