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

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: [
@@ -1878,6 +1933,10 @@ function installCostProjection(ctx) {
1878
1933
  * {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
1879
1934
  * no longer reports `exceeded` is revived, otherwise the pool throws a
1880
1935
  * `RATE_LIMIT` error naming the earliest reset time.
1936
+ * - An account the user explicitly asked for — the manual pin
1937
+ * ({@link CommandCodeAccountPoolDeps.preferredId}) or a model rule's
1938
+ * routed account — is probed the same way when a mark would demote it, so
1939
+ * a fallback is never permanent (issue #51).
1881
1940
  *
1882
1941
  * The pool is deliberately cordis-free (like the adapter): every host fact
1883
1942
  * arrives through injected thunks, so node tests can drive it directly.
@@ -1894,6 +1953,18 @@ function installCostProjection(ctx) {
1894
1953
  * window opens" into "fail now".
1895
1954
  */
1896
1955
  const RETRY_MAX_DELAY_MS = 9e5;
1956
+ /**
1957
+ * How long an explicitly selected account's rate-limit mark may stand before
1958
+ * its window is probed again. A 429 whose reset was never learned marks the
1959
+ * key `unknown`, which {@link accountUsable} refuses forever — deliberately,
1960
+ * because the all-marked revival pass is the only thing meant to clear it and
1961
+ * it costs one billing call per key. That is fine for a key nobody asked for,
1962
+ * and wrong for the one the user pinned: a single 429 would otherwise demote
1963
+ * the user's own selection until the process restarts (issue #51). The probe
1964
+ * is therefore allowed once per interval per key, which bounds an endpoint
1965
+ * that keeps failing while a successful probe resolves the mark for good.
1966
+ */
1967
+ const EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS = 6e4;
1897
1968
  /** A labeled, human-readable clock reading for error messages. */
1898
1969
  function clockLabel(ms) {
1899
1970
  return new Date(ms).toLocaleString();
@@ -1951,6 +2022,8 @@ var CommandCodeAccountPool = class {
1951
2022
  deps;
1952
2023
  /** Rotation state by API key. */
1953
2024
  states = /* @__PURE__ */ new Map();
2025
+ /** Last explicit-revival probe attempt by API key (throttles a failing probe). */
2026
+ explicitProbes = /* @__PURE__ */ new Map();
1954
2027
  constructor(deps) {
1955
2028
  this.deps = deps;
1956
2029
  }
@@ -2005,22 +2078,40 @@ var CommandCodeAccountPool = class {
2005
2078
  * `options.model` is the request's model id; routing rules re-read per
2006
2079
  * resolution, so a settings change applies live.
2007
2080
  *
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).
2081
+ * `options.tried` lists the keys this request has already used — the
2082
+ * just-rejected one included. They are removed from the resolution entirely,
2083
+ * which is what lets one request walk a four-account pool: an account-scoped
2084
+ * rejection that does not mark the key (no credits, a model outside the
2085
+ * account's plan) would otherwise be offered again on every attempt, and the
2086
+ * accounts behind it would never be reached.
2087
+ *
2088
+ * An explicit selection (the pin or a model rule) that a rate-limit mark
2089
+ * would demote is probed before the fallback serves, so "falls back while
2090
+ * exhausted" never becomes "stays demoted until the process restarts".
2012
2091
  */
2013
2092
  async resolveKey(options) {
2014
2093
  const accounts = await this.resolvedAccounts();
2015
2094
  if (accounts.length === 0) return;
2016
- const routed = selectAccountForModel(accounts, options?.model ?? "", this.deps.modelAccountRules?.());
2017
- if (routed !== void 0) return this.pick(routed);
2095
+ const tried = options?.tried;
2096
+ const available = tried === void 0 || tried.length === 0 ? accounts : accounts.filter((account) => !tried.includes(account.key));
2018
2097
  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) => {
2098
+ if (available.length === 0) {
2099
+ if (selectActiveAccount(accounts, preferred) !== void 0) return void 0;
2100
+ throw allAccountsUnusable(accounts);
2101
+ }
2102
+ const routed = selectAccountForModel(available, options?.model ?? "", this.deps.modelAccountRules?.());
2103
+ if (routed !== void 0) return this.pick(routed);
2104
+ const chosen = selectActiveAccount(available, preferred);
2105
+ if (chosen !== void 0) {
2106
+ const explicit = this.explicitAccount(available, options?.model ?? "", preferred);
2107
+ if (explicit !== void 0 && explicit.key !== chosen.key && this.canProbeExplicit(explicit)) {
2108
+ const revived = await this.probeExplicit(explicit);
2109
+ if (revived !== void 0) return this.pick(revived);
2110
+ }
2111
+ return this.pick(chosen);
2112
+ }
2113
+ await Promise.all(available.map(async (account) => {
2022
2114
  if (account.state?.kind === "disabled") return;
2023
- if (options?.exclude !== void 0 && account.key === options.exclude) return;
2024
2115
  let probe;
2025
2116
  try {
2026
2117
  probe = await this.deps.probeWindow(account.key);
@@ -2028,34 +2119,45 @@ var CommandCodeAccountPool = class {
2028
2119
  return;
2029
2120
  }
2030
2121
  if (probe === void 0) return;
2031
- if (!probe.exceeded) this.states.delete(account.key);
2032
- else this.states.set(account.key, {
2122
+ if (!probe.exceeded) {
2123
+ this.states.delete(account.key);
2124
+ return;
2125
+ }
2126
+ if (probe.resetAt > 0) this.states.set(account.key, {
2033
2127
  kind: "cooldown",
2034
2128
  reason: account.state?.reason ?? "rate limited (429)",
2035
2129
  until: probe.resetAt
2036
2130
  });
2037
2131
  }));
2038
- const latest = await this.resolvedAccounts();
2039
- const revived = selectActiveAccount(latest, preferred);
2132
+ const latestAccounts = await this.resolvedAccounts();
2133
+ const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
2040
2134
  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);
2135
+ if (selectActiveAccount(latestAccounts, preferred) !== void 0) return void 0;
2136
+ throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2046
2137
  }
2047
2138
  /**
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.
2139
+ * Record a rejection against one key. `rate-limit` marks the key exhausted
2140
+ * (`429`/`RATE_LIMITED`) — as a `cooldown` until `resetAtMs` when the
2141
+ * rejection body published the provider's own reset time, otherwise as an
2142
+ * `unknown` mark whose reset is probed lazily — and `invalid-credential`
2143
+ * (401) disables the key until the stored credential changes.
2144
+ *
2145
+ * `resetAtMs` is seconds-to-millis converted by the adapter from the
2146
+ * provider's `error.rateLimit.reset`: knowing the real reset immediately is
2147
+ * what keeps an exhausted account out of rotation for exactly as long as the
2148
+ * provider said, instead of until a probe happens to run.
2052
2149
  */
2053
- markRejected(apiKey, rejection) {
2150
+ markRejected(apiKey, rejection, resetAtMs) {
2054
2151
  if (rejection === "invalid-credential") this.states.set(apiKey, {
2055
2152
  kind: "disabled",
2056
2153
  reason: "invalid API key (401)",
2057
2154
  until: 0
2058
2155
  });
2156
+ else if (resetAtMs !== void 0 && resetAtMs > Date.now()) this.states.set(apiKey, {
2157
+ kind: "cooldown",
2158
+ reason: "rate limited (429)",
2159
+ until: resetAtMs
2160
+ });
2059
2161
  else this.states.set(apiKey, {
2060
2162
  kind: "unknown",
2061
2163
  reason: "rate limited (429)",
@@ -2096,6 +2198,67 @@ var CommandCodeAccountPool = class {
2096
2198
  }
2097
2199
  }
2098
2200
  }
2201
+ /**
2202
+ * The account the user explicitly asked for: the slot a model rule routes
2203
+ * the request to when that id exists among the resolved slots, else the
2204
+ * manually pinned slot. Consulted only on the fallback path — a usable
2205
+ * routed account already returned above — so unlike
2206
+ * {@link selectAccountForModel} it does NOT require the account to be
2207
+ * usable, which is exactly what {@link resolveKey} re-probes.
2208
+ */
2209
+ explicitAccount(accounts, model, preferredId) {
2210
+ const rule = matchModelRule(model, this.deps.modelAccountRules?.());
2211
+ const id = rule !== void 0 && accounts.some((account) => account.slot.id === rule.account) ? rule.account : preferredId;
2212
+ return id === void 0 ? void 0 : accounts.find((account) => account.slot.id === id);
2213
+ }
2214
+ /**
2215
+ * Whether an explicitly selected account's mark is due for a window probe.
2216
+ * Only an `unknown` mark (a 429 whose reset was never learned) is worth
2217
+ * re-probing: a `cooldown` already carries its reset time and expires by
2218
+ * itself, and a `disabled` (401) key stays out until the stored credential
2219
+ * changes. The interval bounds a probe endpoint that keeps failing.
2220
+ */
2221
+ canProbeExplicit(account) {
2222
+ if (account.state?.kind !== "unknown") return false;
2223
+ const last = this.explicitProbes.get(account.key);
2224
+ return last === void 0 || this.now() - last >= EXPLICIT_ACCOUNT_PROBE_INTERVAL_MS;
2225
+ }
2226
+ /** The clock the probe throttle reads; injected so a test can travel in time. */
2227
+ now() {
2228
+ return this.deps.now?.() ?? Date.now();
2229
+ }
2230
+ /**
2231
+ * Probe one explicitly selected account's window and apply the answer. A
2232
+ * window that is no longer exceeded drops the mark, so the user's own
2233
+ * selection serves again on this very request. An exceeded one is stamped
2234
+ * as a cooldown carrying the provider's reset time, after which
2235
+ * {@link accountUsable} lets the account back in with no further probe. A
2236
+ * probe that fails changes nothing: the mark stays `unknown` and the next
2237
+ * attempt waits out the interval.
2238
+ */
2239
+ async probeExplicit(account) {
2240
+ this.explicitProbes.set(account.key, this.now());
2241
+ let probe;
2242
+ try {
2243
+ probe = await this.deps.probeWindow(account.key);
2244
+ } catch {
2245
+ return;
2246
+ }
2247
+ if (probe === void 0) return void 0;
2248
+ if (!probe.exceeded) {
2249
+ this.states.delete(account.key);
2250
+ return {
2251
+ slot: account.slot,
2252
+ key: account.key,
2253
+ state: void 0
2254
+ };
2255
+ }
2256
+ if (probe.resetAt > 0) this.states.set(account.key, {
2257
+ kind: "cooldown",
2258
+ reason: account.state?.reason ?? "rate limited (429)",
2259
+ until: probe.resetAt
2260
+ });
2261
+ }
2099
2262
  /** Hand out the chosen account's key. */
2100
2263
  pick(account) {
2101
2264
  return {
@@ -2105,6 +2268,28 @@ var CommandCodeAccountPool = class {
2105
2268
  }
2106
2269
  };
2107
2270
  /**
2271
+ * The error for "accounts exist but none of them can serve": all-401 becomes
2272
+ * `INVALID_CREDENTIAL`, anything else `RATE_LIMIT` naming the earliest known
2273
+ * window reset. Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
2274
+ * and by its already-tried diagnosis, so the two can never drift apart.
2275
+ *
2276
+ * The attached `providerRetryAfterMs` is the exact wait until the earliest
2277
+ * known reset, so dsh-llm-retry sleeps through the window instead of polling
2278
+ * at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}: the
2279
+ * executor honors a provider wait verbatim only at or below the policy's
2280
+ * `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
2281
+ * (normal mode), which would turn "poll until the window opens" into "fail
2282
+ * now". Longer resets simply ride the capped local backoff and the probe
2283
+ * revival.
2284
+ */
2285
+ function allAccountsUnusable(accounts) {
2286
+ 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");
2287
+ const resets = accounts.map((account) => account.state).filter((state) => state !== void 0 && state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2288
+ const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2289
+ const wait = earliest > 0 ? Math.max(1e3, earliest - Date.now()) : 0;
2290
+ 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)}` : "") + ` — 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);
2291
+ }
2292
+ /**
2108
2293
  * Normalize one resolved credential to the form every consumer sees. Trim
2109
2294
  * only: a blank-after-trim value means "no key" (the slot is omitted and the
2110
2295
  * caller reports `MISSING_CREDENTIAL`), while characters an HTTP header cannot
@@ -2353,7 +2538,7 @@ function scaleBy(size, factor) {
2353
2538
  * and API key or subscription, and Command Code's terms apply.
2354
2539
  *
2355
2540
  * 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
2541
+ * re-verified against command-code@1.57.0 — endpoints, request shape, and
2357
2542
  * stream events unchanged):
2358
2543
  * POST {apiBase}/alpha/generate
2359
2544
  * body: { config, memory, taste, skills, params: { model, messages, tools,
@@ -2366,7 +2551,7 @@ function scaleBy(size, factor) {
2366
2551
  * per-request options thunk and an API-key resolver from the plugin entry
2367
2552
  * (src/index.ts), so a settings change reaches the very next request.
2368
2553
  */
2369
- const COMMAND_CODE_CLI_VERSION = "1.54.0";
2554
+ const COMMAND_CODE_CLI_VERSION = "1.57.0";
2370
2555
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2371
2556
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2372
2557
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -2404,15 +2589,22 @@ function booleanValue(value) {
2404
2589
  return typeof value === "boolean" ? value : void 0;
2405
2590
  }
2406
2591
  /**
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.
2592
+ * Thinking text carried by an OpenRouter-shaped `reasoning_details` array,
2593
+ * concatenated in the order the gateway sent it.
2594
+ *
2595
+ * Entries are read by their own text members rather than filtered on `type`:
2596
+ * DeepSeek sends `{ type: 'reasoning.text', text }` while the OpenAI family
2597
+ * sends `{ type: 'reasoning.summary', summary, format: 'openai-responses-v1' }`
2598
+ * — the same summary text the scalar `delta.reasoning` carries, in the
2599
+ * Responses protocol's vocabulary. A `type` filter would drop one family or the
2600
+ * other, and losing thinking is the failure this exists to prevent.
2601
+ *
2602
+ * `text` wins per entry, but an EMPTY `text` must not shadow a populated
2603
+ * `summary` (the Responses wire emits `text: ''` placeholders), so the fallback
2604
+ * tests for non-empty rather than merely present. Entries carrying neither
2605
+ * member — the encrypted blobs — contribute nothing, and no entry contributes
2606
+ * both, so a summary is never counted twice.
2410
2607
  *
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
2608
  * Returns undefined when the array yields nothing, so the caller's `??` chain
2417
2609
  * keeps falling through instead of treating "no text" as a reasoning delta.
2418
2610
  */
@@ -2421,10 +2613,24 @@ function reasoningDetailsText(value) {
2421
2613
  let text = "";
2422
2614
  for (const entry of value) {
2423
2615
  if (!isRecord(entry)) continue;
2424
- text += stringValue(entry.text) ?? "";
2616
+ const exact = stringValue(entry.text);
2617
+ const summary = stringValue(entry.summary);
2618
+ text += exact !== void 0 && exact !== "" ? exact : summary ?? "";
2425
2619
  }
2426
2620
  return text === "" ? void 0 : text;
2427
2621
  }
2622
+ /**
2623
+ * One SCALAR thinking delta, treating an empty string as "this chunk carries
2624
+ * none" — the same rule {@link reasoningDetailsText} applies inside the array.
2625
+ * The scalar and the array describe the same thinking, so an empty spelling of
2626
+ * one must not shadow populated text in the other (the Responses wire pads with
2627
+ * `text: ''`). `??` alone cannot express this: an empty string is not nullish,
2628
+ * so `reasoning: ''` beside a populated `reasoning_details` used to drop the
2629
+ * whole chunk's thinking.
2630
+ */
2631
+ function nonEmptyText(value) {
2632
+ return value === void 0 || value === "" ? void 0 : value;
2633
+ }
2428
2634
  /** Parse a billing-period timestamp (ISO string or millis) into millis; 0 when absent/invalid. */
2429
2635
  function periodEndValue(value) {
2430
2636
  const asNumber = numberValue(value);
@@ -3674,11 +3880,11 @@ var CommandCodeAdapter = class extends LlmAdapter {
3674
3880
  };
3675
3881
  };
3676
3882
  if (opts?.unfiltered === true) return catalog.map(toInfo).sort(compareByPlan);
3677
- const access = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadBillingAccess();
3883
+ const accesses = this.deps.options().filterModelsByPlan === false ? void 0 : await this.loadPoolBillingAccess();
3678
3884
  const visible = this.deps.options().visibleModels;
3679
3885
  const allow = Array.isArray(visible) && visible.length > 0 ? new Set(visible.filter((id) => typeof id === "string" && id !== "")) : void 0;
3680
3886
  const overrides = this.deps.options().modelVisibility;
3681
- return catalog.filter((model) => modelVisibleInPlan(model.id, access)).filter((model) => {
3887
+ return catalog.filter((model) => modelVisibleForAnyAccount(model.id, accesses)).filter((model) => {
3682
3888
  const override = overrides?.[model.id];
3683
3889
  if (typeof override === "boolean") return override;
3684
3890
  return allow === void 0 || allow.has(model.id);
@@ -3734,17 +3940,44 @@ var CommandCodeAdapter = class extends LlmAdapter {
3734
3940
  };
3735
3941
  }
3736
3942
  /**
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).
3943
+ * Every account's billing facts behind the picker's plan filter, in the
3944
+ * pool's rotation order and cached per key for {@link BILLING_ACCESS_TTL_MS}
3945
+ * (so the extra accounts cost their three requests once per TTL, not once
3946
+ * per picker load). `undefined` means the filter cannot be evaluated at all
3947
+ * — no key resolved — which {@link modelVisibleForAnyAccount} reads as
3948
+ * "show everything".
3949
+ *
3950
+ * The serving account is only the first entry: with several accounts the
3951
+ * model list must not depend on which of them rotation happens to be using.
3952
+ */
3953
+ async loadPoolBillingAccess() {
3954
+ const keys = await this.poolAccountKeys();
3955
+ if (keys.length === 0) return void 0;
3956
+ return Promise.all(keys.map((key) => this.loadBillingAccessForKey(key)));
3957
+ }
3958
+ /**
3959
+ * The API keys the picker's plan filter must consult: every account the host
3960
+ * can serve from, in rotation order, when the host exposes one. A host
3961
+ * without a pool (or without the seam) reports just the key that would serve
3962
+ * this request — exactly the pre-pool behaviour.
3740
3963
  */
3741
- async loadBillingAccess() {
3742
- let apiKey;
3964
+ async poolAccountKeys() {
3743
3965
  try {
3744
- apiKey = await this.deps.resolveApiKey(this.deps.options());
3966
+ const usable = (await this.deps.resolveAccountKeys?.() ?? []).filter((key) => typeof key === "string" && key !== "");
3967
+ if (usable.length > 0) return usable;
3968
+ } catch {}
3969
+ try {
3970
+ return [await this.deps.resolveApiKey(this.deps.options())];
3745
3971
  } catch {
3746
- return;
3972
+ return [];
3747
3973
  }
3974
+ }
3975
+ /**
3976
+ * One account's billing facts, cached for {@link BILLING_ACCESS_TTL_MS} and
3977
+ * shared across concurrent callers. `undefined` means "unknown — show
3978
+ * everything" (fail-open).
3979
+ */
3980
+ async loadBillingAccessForKey(apiKey) {
3748
3981
  const cached = this.billingAccess.get(apiKey);
3749
3982
  if (cached !== void 0 && Date.now() - cached.at < 3e5) return cached.value;
3750
3983
  const existing = this.billingAccessInflight.get(apiKey);
@@ -3906,14 +4139,29 @@ var CommandCodeAdapter = class extends LlmAdapter {
3906
4139
  return report;
3907
4140
  }
3908
4141
  /**
3909
- * Probe one account's five-hour window from `/alpha/billing/credits`. The
4142
+ * Probe one account's real usage windows from `/alpha/billing/credits`. The
3910
4143
  * 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.
4144
+ * account whose windows no longer report `exceeded` is revived, and the
4145
+ * `resetAt` values feed the "earliest reset" error message.
4146
+ *
4147
+ * BOTH windows the endpoint publishes are read, not just `fiveHour`. An
4148
+ * account can have an open five-hour window and an exhausted WEEKLY quota at
4149
+ * the same time (the two are metered separately), and reading only the
4150
+ * shorter one revived such an account as "usable" — the pool then handed it
4151
+ * out, the provider rejected the request again, and the next all-marked pass
4152
+ * revived it once more, so a used-up account kept coming back (issue #51's
4153
+ * follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
4154
+ * published window is exceeded, and `resetAt` is the LATEST reset among the
4155
+ * exceeded ones — the binding constraint, because a clear five-hour window
4156
+ * buys nothing while the weekly quota is spent.
4157
+ *
4158
+ * Returns `undefined` when the probe itself failed (transport, non-200, or a
4159
+ * payload with no window limits at all) — a failed probe never changes pool
4160
+ * state. The monthly/purchased/free credit balances are deliberately NOT part
4161
+ * of this answer: they are balances rather than windows, they carry no reset
4162
+ * time, and the server remains the final gate on them.
3915
4163
  */
3916
- async probeFiveHourWindow(apiKey) {
4164
+ async probeWindowLimits(apiKey) {
3917
4165
  try {
3918
4166
  const connection = this.deps.options();
3919
4167
  const response = await this.fetchImpl(`${connection.apiBase}/alpha/billing/credits`, {
@@ -3924,11 +4172,22 @@ var CommandCodeAdapter = class extends LlmAdapter {
3924
4172
  const parsed = await response.json();
3925
4173
  if (!isRecord(parsed)) return void 0;
3926
4174
  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;
4175
+ if (windowLimits === void 0) return void 0;
4176
+ let reported = false;
4177
+ let exceeded = false;
4178
+ let resetAt = 0;
4179
+ for (const raw of [windowLimits.fiveHour, windowLimits.weekly]) {
4180
+ const window = isRecord(raw) ? parseWindowLimit(raw) : void 0;
4181
+ if (window === void 0) continue;
4182
+ reported = true;
4183
+ if (!window.exceeded) continue;
4184
+ exceeded = true;
4185
+ resetAt = Math.max(resetAt, window.resetAt);
4186
+ }
4187
+ if (!reported) return void 0;
3929
4188
  return {
3930
- exceeded: fiveHour.exceeded === true,
3931
- resetAt: numberValue(fiveHour.resetAt) ?? 0
4189
+ exceeded,
4190
+ resetAt: exceeded ? resetAt : 0
3932
4191
  };
3933
4192
  } catch {
3934
4193
  return;
@@ -4002,13 +4261,23 @@ var CommandCodeAdapter = class extends LlmAdapter {
4002
4261
  }
4003
4262
  }
4004
4263
  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);
4264
+ const rejection = classifyAccountRejection(attempt.status, attempt.errText);
4265
+ if (rejection !== void 0 && rotate !== void 0 && options.signal?.aborted !== true && tried.size < MAX_ACCOUNT_ROTATIONS) {
4266
+ const next = await rotate(apiKey, rejection.reason, connection, options.model, {
4267
+ tried: [...tried],
4268
+ ...rejection.resetAtMs !== void 0 && { resetAtMs: rejection.resetAtMs }
4269
+ });
4007
4270
  if (next !== void 0 && !tried.has(next)) {
4008
4271
  apiKey = next;
4009
4272
  continue;
4010
4273
  }
4011
4274
  }
4275
+ if (rejection?.reason === "rate-limit" && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4276
+ const reset = rejection.resetAtMs;
4277
+ const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4278
+ const when = reset === void 0 ? void 0 : new Date(reset).toISOString();
4279
+ throw new LlmError("llm-commandcode: the Command Code account is rate limited" + (when === void 0 ? "" : ` — the provider reports it resets at ${when}`) + ";当前 Command Code 账户已被限流" + (when === void 0 ? "" : `,服务商给出的重置时间为 ${when}`), "RATE_LIMIT", wait > 0 ? { providerRetryAfterMs: wait } : void 0);
4280
+ }
4012
4281
  throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
4013
4282
  }
4014
4283
  if (connected === void 0) throw new LlmError("Command Code API connection failed without a response", "TRANSPORT");
@@ -4284,7 +4553,7 @@ function handleOpenAIEvent(asm, event) {
4284
4553
  }
4285
4554
  const choice = isRecord(choices[0]) ? choices[0] : {};
4286
4555
  const delta = isRecord(choice.delta) ? choice.delta : {};
4287
- const reasoningDelta = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
4556
+ const reasoningDelta = nonEmptyText(stringValue(delta.reasoning)) ?? nonEmptyText(stringValue(delta.reasoning_content)) ?? reasoningDetailsText(delta.reasoning_details) ?? "";
4288
4557
  if (reasoningDelta !== "") {
4289
4558
  chunks.push(...closeText(asm));
4290
4559
  if (asm.reasoningIndex < 0) {
@@ -4400,6 +4669,89 @@ function mapFinishReason(reason) {
4400
4669
  * back to the CLI /alpha/generate transport; other 4xx/5xx must surface as
4401
4670
  * ordinary errors so real account/model problems are not masked.
4402
4671
  */
4672
+ /**
4673
+ * Classify a pre-stream rejection that is about the ACCOUNT rather than about
4674
+ * the request, using the official CLI's own rules (command-code@1.56.0:
4675
+ * `parseWindowLimitError`, `isInsufficientCreditsRequestError`,
4676
+ * `parseSpendCapError` and the terminal-marker list
4677
+ * `["premium_credits_exhausted", "model_not_in_plan", "insufficient credits"]`).
4678
+ * Undefined means "not an account-scoped rejection": a malformed request, an
4679
+ * oversized body or a context overflow must fail fast instead of walking the
4680
+ * whole account pool.
4681
+ *
4682
+ * Issue #51's follow-up is why this exists. Only 429/401 used to rotate, so
4683
+ * every other account-scoped rejection was terminal AND invisible to the pool:
4684
+ * an account that could not pay, or could not run the model, stayed "usable",
4685
+ * was handed out again on the next request, and produced the same error again
4686
+ * ("切到一个不可用账号后就报 400,还挺频繁"). The three reasons differ in what
4687
+ * the host should do with them, which is the caller's decision, not this
4688
+ * function's:
4689
+ *
4690
+ * - `rate-limit`: a usage window is spent. The code `RATE_LIMITED` is the
4691
+ * authoritative signal — the CLI accepts the code OR a 429 status — and the
4692
+ * body's `error.rateLimit` names the window and its `reset` (in seconds),
4693
+ * so the host can hold the account out until the provider's own reset time.
4694
+ * - `invalid-credential`: 401; the key itself is bad.
4695
+ * - `unavailable`: the key is fine but this account cannot serve: no credits
4696
+ * (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
4697
+ * `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED`, or a spend-cap wording)
4698
+ * or a model outside its plan (`MODEL_NOT_IN_PLAN`). Rotating must NOT mark
4699
+ * the account: the fact is about the model or the balance, it can change
4700
+ * without the key changing, and a `:free` model can still be served by an
4701
+ * account with an empty balance.
4702
+ */
4703
+ function classifyAccountRejection(status, errText) {
4704
+ if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
4705
+ const resetAtMs = readWindowResetAtMs(errText);
4706
+ return resetAtMs === void 0 ? { reason: "rate-limit" } : {
4707
+ reason: "rate-limit",
4708
+ resetAtMs
4709
+ };
4710
+ }
4711
+ if (status === 401) return { reason: "invalid-credential" };
4712
+ if (status < 400 || status >= 500) return void 0;
4713
+ const lower = errText.toLowerCase().replaceAll("_", " ");
4714
+ const code = readProviderErrorCode(errText)?.toUpperCase();
4715
+ 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" };
4716
+ }
4717
+ /** The machine-readable `error.code`/`error.type` of a pre-stream failure body. */
4718
+ function readProviderErrorCode(errText) {
4719
+ try {
4720
+ const parsed = JSON.parse(errText);
4721
+ if (!isRecord(parsed)) return void 0;
4722
+ const error = isRecord(parsed.error) ? parsed.error : parsed;
4723
+ return stringValue(error.code) ?? stringValue(error.type);
4724
+ } catch {
4725
+ return;
4726
+ }
4727
+ }
4728
+ /**
4729
+ * The exact reset time carried by a window-limit rejection body, in millis.
4730
+ * The CLI reads `error.rateLimit.reset` as SECONDS (`1e3 * reset`) and falls
4731
+ * back to a `resets at <ISO>` stamp in the message; both are honored here so a
4732
+ * 429 does not need a billing probe to learn a fact the provider already told
4733
+ * us. Undefined when the body carries neither.
4734
+ */
4735
+ function readWindowResetAtMs(errText) {
4736
+ try {
4737
+ const parsed = JSON.parse(errText);
4738
+ if (isRecord(parsed)) {
4739
+ const error = isRecord(parsed.error) ? parsed.error : parsed;
4740
+ const seconds = numberValue((isRecord(error.rateLimit) ? error.rateLimit : void 0)?.reset);
4741
+ if (seconds !== void 0 && seconds > 0) return Math.round(seconds * 1e3);
4742
+ const message = stringValue(error.message) ?? "";
4743
+ const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(message)?.[1];
4744
+ if (stamp !== void 0) {
4745
+ const at = Date.parse(stamp);
4746
+ if (!Number.isNaN(at)) return at;
4747
+ }
4748
+ }
4749
+ } catch {}
4750
+ const stamp = /resets at (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/i.exec(errText)?.[1];
4751
+ if (stamp === void 0) return void 0;
4752
+ const at = Date.parse(stamp);
4753
+ return Number.isNaN(at) ? void 0 : at;
4754
+ }
4403
4755
  function isUpgradeRequiredError(status, errText) {
4404
4756
  if (status !== 403) return false;
4405
4757
  const lower = errText.toLowerCase();
@@ -6335,7 +6687,7 @@ function apply(ctx, config) {
6335
6687
  slots,
6336
6688
  resolveRef,
6337
6689
  authFileKey: resolveAuthFileApiKey,
6338
- probeWindow: (apiKey) => adapter.probeFiveHourWindow(apiKey),
6690
+ probeWindow: (apiKey) => adapter.probeWindowLimits(apiKey),
6339
6691
  preferredId,
6340
6692
  modelAccountRules: () => current().modelAccountRules ?? []
6341
6693
  });
@@ -6348,10 +6700,11 @@ function apply(ctx, config) {
6348
6700
  const adapter = new CommandCodeAdapter({
6349
6701
  options,
6350
6702
  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,
6703
+ rotateApiKey: async (rejectedKey, rejection, _connection, model, rotation) => {
6704
+ if (rejection !== "unavailable") pool.markRejected(rejectedKey, rejection, rotation?.resetAtMs);
6705
+ const tried = rotation?.tried?.length ? rotation.tried : [rejectedKey];
6706
+ const resolved = await pool.resolveKey(model === void 0 ? { tried } : {
6707
+ tried,
6355
6708
  model
6356
6709
  });
6357
6710
  return resolved === void 0 ? void 0 : assertUsableApiKey(resolved.key, "llm-commandcode", resolved.slot.ref ?? `${resolved.slot.label} (config.apiKey)`);
@@ -6359,6 +6712,9 @@ function apply(ctx, config) {
6359
6712
  resolveAttachments: () => {
6360
6713
  const attachments = ctx.get("attachments");
6361
6714
  return attachments === void 0 ? void 0 : attachments;
6715
+ },
6716
+ resolveAccountKeys: async () => {
6717
+ return (await pool.resolvedAccounts()).map((account) => account.key);
6362
6718
  }
6363
6719
  });
6364
6720
  ctx.llm.registerConfigurableProviders([{