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

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/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.11.8] - 2026-09-22
10
+
11
+ ### Changed
12
+
13
+ - **Synced with the official command-code@1.62.0 CLI** (2026-09-22 check of the 2026-09-21 npm `latest`; five releases since the last sync — 1.58.1, then the 1.59.0/1.60.0/1.61.0/1.62.0 train whose changelog entries are "Add Grok 4.7", "Add Step 5 Preview", "Grok 4.7 40% off" and "Add MiMo V2.6 Flash / Add MiMo V2.6 Pro and Pro UltraSpeed"). `COMMAND_CODE_CLI_VERSION` is now `1.62.0`. This train is purely additive: the CLI registry grows 78 → 83 with exactly five additions and no removal or edit, so five models gain their picker annotations and prices. `xai/grok-4.7` (500K context, text+image, `low`/`medium`/`high`/`xhigh` efforts, GOAT and above) launches on a 40% off deal running 2026-09-21 → 2026-09-27 that reverts to $2.00 in / $6.00 out when it lapses; its snapshotted rates are the discounted $1.20 in / $3.60 out / $0.30 cache read, doubling to $2.40 / $7.20 / $0.60 past 200K, and the deal carries the page's own expiry stamp so the badge hides itself after that instant. `stepfun/Step-5-Preview` is StepFun's 600B sparse-MoE agentic coding model (1M context, text+image, `low`/`medium`/`high` efforts, Go, $1.00 / $2.70 / $0.05). The MiMo V2.6 family is text+image with a 1,048,576-token context and no reasoning flag in any source: `xiaomi/mimo-v2.6-flash` ($0.14 / $0.28 / $0.0028) and `xiaomi/mimo-v2.6-pro` ($0.435 / $0.87 / $0.0036) on Go, plus `xiaomi/mimo-v2.6-pro-ultraspeed` on GOAT and above ($4.35 / $8.70 / $0.036). In snapshot terms: the effort map gains exactly two keys (46 → 48), the Vision set the five (52 → 57), the plan map three Go and two GOAT entries without moving a tier (46/52/65/72 → 49/57/70/77), the deals five → six, and the vendored price table exactly the five matching rows (71 → 76) with no rate change to any existing row (`node scripts/sync-model-prices.mjs --check` now reports all 76 rows up to date). Everything else is re-verified unchanged against both bundles and the public sources: the subscription plan maps, the peak/off-peak membership and schedule (01:00–04:00 and 06:00–10:00 UTC, Monday–Friday only), the automatic-reasoning set, and the public catalog — which serves 76 models (71 + the five) with its route mix moving 55 → 60 × chat/completions + responses exactly by those additions, the same 8 Claude ids still messages-only and 8 models still chat/completions-only, which agrees with the adapter's `claude-*` routing rule. Static inspection of the two bundles finds no transport drift: the `/alpha/*` endpoint set, the `/alpha/generate` converters (`toWireMessages`/`toWireTools`/`toWireToolOutput`), the stream event vocabulary, the `x-command-code-version` header, the browser-login call site and the web-search field names differ only by minified variable renames, and the documented Provider API endpoint set is unchanged. Two CLI-local changelog items reach nothing this adapter owns: 1.58.1's "Auto-compact past images and keep their result" (the CLI's own compaction behavior, not a wire change) and 1.59.0's YOLO/accept-edits keybinding rework. The two upstream leftovers stay deliberately un-snapshotted, exactly as the previous check recorded them: `gpt-6-astra` is still a CLI/pricing/docs-only model the public catalog does not serve, and the pricing page still embeds an already-expired (`2026-06-22`) `qwen-3.7-max-2x-usage` deal record its own Deals section does not list.
14
+
15
+ ## [0.11.7] - 2026-09-20
16
+
17
+ ### Fixed
18
+ - **A plain 429 is no longer reported as an exhausted usage window** ([#54](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/54)). The report was a contradiction: the turn failed with `llm-commandcode: all 1 Command Code account(s) have exhausted their usage window` while the account card at that moment showed the five-hour window at 2% ($0.22 / $14.00) and the weekly one at 10% ($3.43 / $35.00), and the reporter noted it only reproduced at the default reasoning effort and always recovered by itself a little later. The message was the tell: it carried no "the earliest window resets at …" clause, and that clause appears only when a mark is a `cooldown` with a known reset — so the pool was reporting an exhausted window from a mark that had no reset at all, i.e. from nothing but a 429. The chain: `classifyAccountRejection()` treated ANY 429 (or a bare `RATE_LIMITED` code) as a usage-window rejection, `markRejected()` filed it as `unknown`, the follow-up `/alpha/billing/credits` probe either failed (the same account was still being refused) or read no reset, and `allAccountsUnusable()` then asserted "exhausted their usage window" regardless of the evidence — exactly the sentence the reporter was shown. Classification now follows the official CLI's own bar (command-code@1.56.0 `parseWindowLimitError` → `resolveWindowLabel`): a rejection is a WINDOW limit only when the body names one — `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the "usage limit for your plan" wording — and `readWindowLimitEvidence()` reads that window and its `reset` (seconds, with the `resets at <ISO>` wording as the fallback) in one place. Everything else stays a new `throttled` reason: it rotates and marks the key exactly like a window limit does (so rotation across a multi-account pool is unchanged, and a burst or model-level limiter still moves the request off that key), but the mark carries a `cause` — `window`, `throttle` or `auth` — and only a `window` cause may be described as an exhausted window. A throttle mark is deliberately left `unknown` with no reset, so the next all-marked pass still probes the real windows and upgrades the cause to `window` when a probe reads one as exceeded; a throttle mark that a probe cannot confirm now answers `all N Command Code account(s) are rate limited (429) — the provider did not report an exhausted usage window; retrying` instead, with no invented reset and no invented wait. The same honesty was applied to a window mark whose provider published no reset (`— the provider published no reset time for it` / `,服务商未公布重置时间`), where the old message silently said nothing, and the adapter's end-of-turn `RATE_LIMIT` for a non-429 status carrying a bare `RATE_LIMITED` now says the same thing. `tests/accounts.test.ts` pins the throttle diagnosis (and that it never claims a window), the probe upgrade from throttle to window, the reset-stamped window cooldown, and the no-reset wording; `tests/adapter.test.ts` pins the classification of a bare 429 as `throttled`, the three window-carrying shapes as `rate-limit` (including the plan-usage-limit wording and a window without a reset), and the end-of-turn message.
19
+
20
+ ### Changed
21
+
22
+ - **Synced with the official command-code@1.58.0 CLI** (2026-09-20 check of the 2026-09-19 npm `latest`, whose changelog entry is "Retire the free LongCat 2.0 tier and sell LongCat 2.0 as a paid model"). `COMMAND_CODE_CLI_VERSION` is now `1.58.0`. The CLI registry grows 77 → 78 with exactly one addition — the paid `meituan/LongCat-2.0` — while its retired `meituan/LongCat-2.0:free` sibling gains `hidden` and the display name "LongCat 2.0 (Free)": that is the CLI catching up with the backend rename the 1.57.0 sync had already snapshotted, so no capability-snapshot entry moves, the model was already served under the paid id at $0.30 in / $1.20 out / $0.006 cache read on Go, and no runtime behavior changes. Everything else is re-verified unchanged against both the bundle and the public sources: the per-model effort map, the subscription plan maps (8 plans including their monthly credits), every plan tier (map still 46/52/65/72), the five model deals, the peak/off-peak membership and schedule (01:00–04:00 and 06:00–10:00 UTC, Monday–Friday only), the vendored price table (`node scripts/sync-model-prices.mjs --check` reports all 71 rows up to date), and the public catalog (71 models, its `supported_endpoints` mix still 55 × chat/completions + responses, the same 8 Claude ids × messages-only, 8 × chat/completions, which agrees with the adapter's `claude-*` routing rule). Static inspection of both bundles finds no transport drift: the `/alpha/*` endpoint set, the `/alpha/generate` converters (`toWireMessages`/`toWireTools`/`toWireToolOutput`), the stream event vocabulary, the `x-command-code-version` header, the browser-login call site and the web-search field names are byte-identical to 1.57.0, and the documented Provider API endpoint set is unchanged. Two upstream leftovers stay deliberately un-snapshotted, exactly as the previous check recorded them: `gpt-6-astra` is still a CLI/pricing/docs-only model the public catalog does not serve, and the pricing page still embeds an already-expired (`2026-06-22`) `qwen-3.7-max-2x-usage` deal record that its own Deals section does not list. The Provider API docs now also document the optional `x-cmd-zdr: 1` zero-data-retention header (the CLI parity of `CMD_ZDR=1`); this plugin does not send it, because ZDR opt-in would be a new feature rather than a data sync.
23
+
9
24
  ## [0.11.6] - 2026-09-19
10
25
 
11
26
  ### Fixed
package/lib/client.js CHANGED
@@ -2157,7 +2157,7 @@ window.__ModuleLoader__.load({
2157
2157
  }
2158
2158
  //#endregion
2159
2159
  //#region package.json
2160
- var version = "0.11.6";
2160
+ var version = "0.11.8";
2161
2161
  var repository = {
2162
2162
  "type": "git",
2163
2163
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
package/lib/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import { Context } from "@deepseek-ai/cordis";
7
7
  import { AttachmentStore, ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
8
8
  import { CommandDefinition } from "@deepseek-ai/dsh-commands";
9
9
  //#region src/adapter.d.ts
10
- declare const COMMAND_CODE_CLI_VERSION = "1.57.0";
10
+ declare const COMMAND_CODE_CLI_VERSION = "1.62.0";
11
11
  declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
12
12
  declare const DEFAULT_GENERATE_MAX_TOKENS = 64000;
13
13
  declare const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -101,14 +101,19 @@ interface CommandCodeConnectionOptions {
101
101
  */
102
102
  type ResolveAttachments = () => AttachmentStore | undefined;
103
103
  /**
104
- * Why a pre-stream rejection rotates to another account. `rate-limit` and
105
- * `invalid-credential` are the two the pool records as marks; `unavailable` is
106
- * an account-scoped rejection that must NOT become a mark — the account's key
107
- * is valid and its windows may be open, the ACCOUNT just cannot serve THIS
104
+ * Why a pre-stream rejection rotates to another account. `rate-limit` (a usage
105
+ * window the provider NAMED), `throttled` (a 429 that named no window) and
106
+ * `invalid-credential` are the three the pool records as marks; `unavailable`
107
+ * is an account-scoped rejection that must NOT become a mark — the account's
108
+ * key is valid and its windows may be open, the ACCOUNT just cannot serve THIS
108
109
  * request (no credits, a model outside its plan) — so the pool moves on
109
110
  * without remembering anything.
111
+ *
112
+ * The window/throttle split decides what the pool may CLAIM, never whether it
113
+ * rotates: both leave rotation and mark the key, but only a named window lets
114
+ * the pool report an exhausted usage window (issue #54).
110
115
  */
111
- type AccountRotationReason = 'rate-limit' | 'invalid-credential' | 'unavailable';
116
+ type AccountRotationReason = 'rate-limit' | 'throttled' | 'invalid-credential' | 'unavailable';
112
117
  /** What the rotation hook knows about the request it is rotating within. */
113
118
  interface AccountRotationContext {
114
119
  /** Every API key this request has already used, just-rejected key included. */
@@ -461,8 +466,21 @@ interface CommandCodeAccountSlot {
461
466
  /** Whether the official CLI auth file may back this slot (default slot only). */
462
467
  allowAuthFile: boolean;
463
468
  }
464
- /** Why a key stopped serving requests. */
465
- type AccountRejection = 'rate-limit' | 'invalid-credential';
469
+ /**
470
+ * Why a key stopped serving requests.
471
+ *
472
+ * `rate-limit` is a USAGE-WINDOW rejection: the provider named one of its
473
+ * metered windows (`error.rateLimit.window`, or a message saying the plan's
474
+ * usage limit was reached — the only two shapes the official CLI's own
475
+ * `parseWindowLimitError`/`resolveWindowLabel` accept). `throttled` is the
476
+ * plain 429: the provider refused the request without saying anything about a
477
+ * window, so the key leaves rotation for now but the pool must not describe it
478
+ * as an exhausted window. Collapsing the two is issue #54: the reporter's
479
+ * account panel showed the five-hour window at 2% and the weekly one at 10%
480
+ * while the turn failed with "all 1 Command Code account(s) have exhausted
481
+ * their usage window".
482
+ */
483
+ type AccountRejection = 'rate-limit' | 'throttled' | 'invalid-credential';
466
484
  /** One key's rotation state. */
467
485
  interface CommandCodeAccountState {
468
486
  kind:
@@ -472,6 +490,14 @@ interface CommandCodeAccountState {
472
490
  'cooldown' |
473
491
  /** Marked by a 401: skipped until the stored credential changes. */
474
492
  'disabled';
493
+ /**
494
+ * The evidence behind the mark — what the pool may CLAIM when it reports
495
+ * that no account can serve. `window`: the provider named a usage window, or
496
+ * a `/alpha/billing/credits` probe read one as exceeded. `throttle`: a 429
497
+ * that said nothing about a window (a burst limiter, or a model- or
498
+ * spend-level limit the billing endpoint cannot show). `auth`: a 401.
499
+ */
500
+ cause: 'window' | 'throttle' | 'auth';
475
501
  /** Human-readable reason for the mark (e.g. `rate limited (429)`). */
476
502
  reason: string;
477
503
  /** Cooldown end in millis; 0 for the other kinds. */
@@ -628,16 +654,21 @@ declare class CommandCodeAccountPool {
628
654
  slot: CommandCodeAccountSlot;
629
655
  } | undefined>;
630
656
  /**
631
- * Record a rejection against one key. `rate-limit` marks the key exhausted
632
- * (`429`/`RATE_LIMITED`) — as a `cooldown` until `resetAtMs` when the
633
- * rejection body published the provider's own reset time, otherwise as an
634
- * `unknown` mark whose reset is probed lazily — and `invalid-credential`
635
- * (401) disables the key until the stored credential changes.
657
+ * Record a rejection against one key. `rate-limit` marks the key's usage
658
+ * WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
659
+ * body named the provider's own reset time, otherwise as an `unknown` mark
660
+ * whose window is probed lazily. `throttled` (the plain 429 that named no
661
+ * window) marks the key with the `throttle` cause instead: it leaves rotation
662
+ * exactly like a window mark does, but the pool's own diagnosis keeps saying
663
+ * "rate limited" rather than inventing an exhausted window. A 401
664
+ * (`invalid-credential`) disables the key until the stored credential
665
+ * changes.
636
666
  *
637
667
  * `resetAtMs` is seconds-to-millis converted by the adapter from the
638
668
  * provider's `error.rateLimit.reset`: knowing the real reset immediately is
639
669
  * what keeps an exhausted account out of rotation for exactly as long as the
640
- * provider said, instead of until a probe happens to run.
670
+ * provider said. It only applies to a window mark; a plain throttle keeps the
671
+ * window unknown on purpose, so a probe can still find out.
641
672
  */
642
673
  markRejected(apiKey: string, rejection: AccountRejection, resetAtMs?: number): void;
643
674
  /**
@@ -778,7 +809,11 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
778
809
  * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
779
810
  * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
780
811
  * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
781
- * `z-ai/glm-5.3-flashx` (Go).
812
+ * `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
813
+ * and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
814
+ * MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
815
+ * `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
816
+ * only tier changes are additions and the superset chain still holds.
782
817
  *
783
818
  * The Provider API exposes no plan metadata, so this snapshot is the source of
784
819
  * truth for the picker's plan annotation — it answers "which plan do I need to
package/lib/index.js CHANGED
@@ -203,6 +203,11 @@ const KNOWN_EFFORTS = {
203
203
  "high"
204
204
  ],
205
205
  "sakana/fugu-ultra": ["high", "xhigh"],
206
+ "stepfun/Step-5-Preview": [
207
+ "low",
208
+ "medium",
209
+ "high"
210
+ ],
206
211
  "tencent/hy4-preview": [
207
212
  "low",
208
213
  "medium",
@@ -219,6 +224,12 @@ const KNOWN_EFFORTS = {
219
224
  "high",
220
225
  "xhigh"
221
226
  ],
227
+ "xai/grok-4.7": [
228
+ "low",
229
+ "medium",
230
+ "high",
231
+ "xhigh"
232
+ ],
222
233
  "z-ai/glm-5.3-flash": [
223
234
  "low",
224
235
  "high",
@@ -340,11 +351,16 @@ const KNOWN_IMAGE_MODELS = /* @__PURE__ */ new Set([
340
351
  "moonshotai/Kimi-K3",
341
352
  "sakana/fugu-ultra",
342
353
  "stepfun/Step-3.7-Flash",
354
+ "stepfun/Step-5-Preview",
343
355
  "thinkingmachines/inkling",
344
356
  "thinkingmachines/inkling-small",
345
357
  "xai/grok-4.5",
346
358
  "xai/grok-4.6",
359
+ "xai/grok-4.7",
347
360
  "xiaomi/mimo-v2.5",
361
+ "xiaomi/mimo-v2.6-flash",
362
+ "xiaomi/mimo-v2.6-pro",
363
+ "xiaomi/mimo-v2.6-pro-ultraspeed",
348
364
  "z-ai/glm-5.3-flash",
349
365
  "z-ai/glm-5.3-flashx"
350
366
  ]);
@@ -458,7 +474,11 @@ function requiresMessagesEndpoint(modelId) {
458
474
  * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
459
475
  * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
460
476
  * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
461
- * `z-ai/glm-5.3-flashx` (Go).
477
+ * `z-ai/glm-5.3-flashx` (Go); command-code@1.59.0 added `xai/grok-4.7` (GOAT)
478
+ * and 1.60.0 added `stepfun/Step-5-Preview` (Go); command-code@1.62.0 added the
479
+ * MiMo V2.6 family — `xiaomi/mimo-v2.6-flash` + `xiaomi/mimo-v2.6-pro` (Go) and
480
+ * `xiaomi/mimo-v2.6-pro-ultraspeed` (GOAT) — so the 1.58.0 -> 1.62.0 window's
481
+ * only tier changes are additions and the superset chain still holds.
462
482
  *
463
483
  * The Provider API exposes no plan metadata, so this snapshot is the source of
464
484
  * truth for the picker's plan annotation — it answers "which plan do I need to
@@ -501,6 +521,7 @@ const KNOWN_PLANS = {
501
521
  "poolside/laguna-s-2.1-free": "go",
502
522
  "stepfun/Step-3.5-Flash": "go",
503
523
  "stepfun/Step-3.7-Flash": "go",
524
+ "stepfun/Step-5-Preview": "go",
504
525
  "tencent/hy3-paid": "go",
505
526
  "tencent/hy4-preview": "go",
506
527
  "thinkingmachines/inkling": "go",
@@ -508,6 +529,8 @@ const KNOWN_PLANS = {
508
529
  "xai/grok-4.5": "go",
509
530
  "xiaomi/mimo-v2.5": "go",
510
531
  "xiaomi/mimo-v2.5-pro": "go",
532
+ "xiaomi/mimo-v2.6-flash": "go",
533
+ "xiaomi/mimo-v2.6-pro": "go",
511
534
  "z-ai/glm-5.3-flash": "go",
512
535
  "z-ai/glm-5.3-flashx": "go",
513
536
  "zai-org/GLM-5": "go",
@@ -521,6 +544,8 @@ const KNOWN_PLANS = {
521
544
  "meta/muse-spark-1.2": "goat",
522
545
  "meta/muse-spark-1.3": "goat",
523
546
  "xai/grok-4.6": "goat",
547
+ "xai/grok-4.7": "goat",
548
+ "xiaomi/mimo-v2.6-pro-ultraspeed": "goat",
524
549
  "claude-haiku-4-5-20251001": "pro",
525
550
  "claude-sonnet-4-6": "pro",
526
551
  "claude-sonnet-5": "pro",
@@ -691,6 +716,10 @@ const KNOWN_DEALS = {
691
716
  "MiniMaxAI/MiniMax-M3": { label: "50% off" },
692
717
  "xiaomi/mimo-v2.5-pro": { label: "99% off" },
693
718
  "xiaomi/mimo-v2.5": { label: "98% off" },
719
+ "xai/grok-4.7": {
720
+ label: "40% off",
721
+ expiresAt: "2026-09-27T23:59:59.999Z"
722
+ },
694
723
  "poolside/laguna-s-2.1-free": {
695
724
  label: "FREE",
696
725
  free: true
@@ -1273,6 +1302,26 @@ const MODEL_PRICE_ROWS = [
1273
1302
  1
1274
1303
  ] }]
1275
1304
  },
1305
+ {
1306
+ id: "grok-4.7",
1307
+ rates: [
1308
+ 1.2,
1309
+ 3.6,
1310
+ .3
1311
+ ],
1312
+ contextTiers: [{
1313
+ "maxContext": 2e5,
1314
+ "rates": [
1315
+ 1.2,
1316
+ 3.6,
1317
+ .3
1318
+ ]
1319
+ }, { "rates": [
1320
+ 2.4,
1321
+ 7.2,
1322
+ .6
1323
+ ] }]
1324
+ },
1276
1325
  {
1277
1326
  id: "inkling",
1278
1327
  rates: [
@@ -1353,6 +1402,30 @@ const MODEL_PRICE_ROWS = [
1353
1402
  .0036
1354
1403
  ]
1355
1404
  },
1405
+ {
1406
+ id: "mimo-v2.6-flash",
1407
+ rates: [
1408
+ .14,
1409
+ .28,
1410
+ .0028
1411
+ ]
1412
+ },
1413
+ {
1414
+ id: "mimo-v2.6-pro",
1415
+ rates: [
1416
+ .435,
1417
+ .87,
1418
+ .0036
1419
+ ]
1420
+ },
1421
+ {
1422
+ id: "mimo-v2.6-pro-ultraspeed",
1423
+ rates: [
1424
+ 4.35,
1425
+ 8.7,
1426
+ .036
1427
+ ]
1428
+ },
1356
1429
  {
1357
1430
  id: "minimax-m2.5",
1358
1431
  rates: [
@@ -1578,6 +1651,14 @@ const MODEL_PRICE_ROWS = [
1578
1651
  .04
1579
1652
  ]
1580
1653
  },
1654
+ {
1655
+ id: "step-5-preview",
1656
+ rates: [
1657
+ 1,
1658
+ 2.7,
1659
+ .05
1660
+ ]
1661
+ },
1581
1662
  {
1582
1663
  id: "tencent/hy3-paid",
1583
1664
  rates: [
@@ -1924,10 +2005,11 @@ function installCostProjection(ctx) {
1924
2005
  * usable, the routed account — resolving each slot's key lazily (literal
1925
2006
  * config key → credential seam → launch environment → the official CLI
1926
2007
  * auth file for the default slot only).
1927
- * - {@link CommandCodeAccountPool.markRejected} records a 429 (rate limit,
1928
- * window unknown) or 401 (invalid key, disabled until the config changes)
1929
- * against the exact API key, so several slots sharing one key share one
1930
- * state.
2008
+ * - {@link CommandCodeAccountPool.markRejected} records a 429 or 401 against
2009
+ * the exact API key, so several slots sharing one key share one state. A
2010
+ * 429 that NAMED a usage window marks the window exhausted; a 429 that
2011
+ * named none is recorded as a plain throttle, which rotates the same way
2012
+ * but is never reported as an exhausted window (issue #54).
1931
2013
  * - When every account is marked, the pool probes each key's
1932
2014
  * `/alpha/billing/credits` window limits (through the injected
1933
2015
  * {@link CommandCodeAccountPoolDeps.probeWindow}): an account whose window
@@ -2125,9 +2207,16 @@ var CommandCodeAccountPool = class {
2125
2207
  }
2126
2208
  if (probe.resetAt > 0) this.states.set(account.key, {
2127
2209
  kind: "cooldown",
2128
- reason: account.state?.reason ?? "rate limited (429)",
2210
+ cause: "window",
2211
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2129
2212
  until: probe.resetAt
2130
2213
  });
2214
+ else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2215
+ kind: "unknown",
2216
+ cause: "window",
2217
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2218
+ until: 0
2219
+ });
2131
2220
  }));
2132
2221
  const latestAccounts = await this.resolvedAccounts();
2133
2222
  const revived = selectActiveAccount(tried === void 0 || tried.length === 0 ? latestAccounts : latestAccounts.filter((account) => !tried.includes(account.key)), preferred);
@@ -2136,31 +2225,45 @@ var CommandCodeAccountPool = class {
2136
2225
  throw allAccountsUnusable(latestAccounts.length > 0 ? latestAccounts : accounts);
2137
2226
  }
2138
2227
  /**
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.
2228
+ * Record a rejection against one key. `rate-limit` marks the key's usage
2229
+ * WINDOW exhausted — as a `cooldown` until `resetAtMs` when the rejection
2230
+ * body named the provider's own reset time, otherwise as an `unknown` mark
2231
+ * whose window is probed lazily. `throttled` (the plain 429 that named no
2232
+ * window) marks the key with the `throttle` cause instead: it leaves rotation
2233
+ * exactly like a window mark does, but the pool's own diagnosis keeps saying
2234
+ * "rate limited" rather than inventing an exhausted window. A 401
2235
+ * (`invalid-credential`) disables the key until the stored credential
2236
+ * changes.
2144
2237
  *
2145
2238
  * `resetAtMs` is seconds-to-millis converted by the adapter from the
2146
2239
  * provider's `error.rateLimit.reset`: knowing the real reset immediately is
2147
2240
  * 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.
2241
+ * provider said. It only applies to a window mark; a plain throttle keeps the
2242
+ * window unknown on purpose, so a probe can still find out.
2149
2243
  */
2150
2244
  markRejected(apiKey, rejection, resetAtMs) {
2151
2245
  if (rejection === "invalid-credential") this.states.set(apiKey, {
2152
2246
  kind: "disabled",
2247
+ cause: "auth",
2153
2248
  reason: "invalid API key (401)",
2154
2249
  until: 0
2155
2250
  });
2251
+ else if (rejection === "throttled") this.states.set(apiKey, {
2252
+ kind: "unknown",
2253
+ cause: "throttle",
2254
+ reason: "rate limited (429)",
2255
+ until: 0
2256
+ });
2156
2257
  else if (resetAtMs !== void 0 && resetAtMs > Date.now()) this.states.set(apiKey, {
2157
2258
  kind: "cooldown",
2158
- reason: "rate limited (429)",
2259
+ cause: "window",
2260
+ reason: "usage window exhausted (429)",
2159
2261
  until: resetAtMs
2160
2262
  });
2161
2263
  else this.states.set(apiKey, {
2162
2264
  kind: "unknown",
2163
- reason: "rate limited (429)",
2265
+ cause: "window",
2266
+ reason: "usage window exhausted (429)",
2164
2267
  until: 0
2165
2268
  });
2166
2269
  }
@@ -2255,9 +2358,16 @@ var CommandCodeAccountPool = class {
2255
2358
  }
2256
2359
  if (probe.resetAt > 0) this.states.set(account.key, {
2257
2360
  kind: "cooldown",
2258
- reason: account.state?.reason ?? "rate limited (429)",
2361
+ cause: "window",
2362
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2259
2363
  until: probe.resetAt
2260
2364
  });
2365
+ else if (account.state?.kind !== "cooldown") this.states.set(account.key, {
2366
+ kind: "unknown",
2367
+ cause: "window",
2368
+ reason: account.state?.reason ?? "usage window exhausted (429)",
2369
+ until: 0
2370
+ });
2261
2371
  }
2262
2372
  /** Hand out the chosen account's key. */
2263
2373
  pick(account) {
@@ -2268,26 +2378,39 @@ var CommandCodeAccountPool = class {
2268
2378
  }
2269
2379
  };
2270
2380
  /**
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}
2381
+ * The error for "accounts exist but none of them can serve". Three shapes, each
2382
+ * claiming only what the pool actually knows:
2383
+ *
2384
+ * - every account rejected with 401 → `INVALID_CREDENTIAL`;
2385
+ * - at least one account marked by a NAMED usage window (or by a probe that
2386
+ * read a window as exceeded) → the window diagnosis, naming the earliest
2387
+ * known reset when the provider published one;
2388
+ * - every mark a plain throttle (a 429 that said nothing about a window, and
2389
+ * no probe could confirm one) → a throttle diagnosis that says so. Claiming
2390
+ * "all accounts have exhausted their usage window" here is issue #54: the
2391
+ * report's account panel showed the five-hour window at 2% and the weekly
2392
+ * one at 10% while the turn failed with exactly that sentence.
2393
+ *
2394
+ * Shared by the all-marked tail of {@link CommandCodeAccountPool.resolveKey}
2274
2395
  * and by its already-tried diagnosis, so the two can never drift apart.
2275
2396
  *
2276
2397
  * 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
2398
+ * known window reset, so dsh-llm-retry sleeps through the window instead of
2399
+ * polling at its backoff cadence. It is capped at {@link RETRY_MAX_DELAY_MS}:
2400
+ * the executor honors a provider wait verbatim only at or below the policy's
2280
2401
  * `maxDelayMs` — a LONGER attached wait makes it abandon the retry entirely
2281
2402
  * (normal mode), which would turn "poll until the window opens" into "fail
2282
2403
  * now". Longer resets simply ride the capped local backoff and the probe
2283
- * revival.
2404
+ * revival. A throttle diagnosis attaches none: its marks carry no reset.
2284
2405
  */
2285
2406
  function allAccountsUnusable(accounts) {
2286
2407
  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);
2408
+ const windowMarked = accounts.map((account) => account.state).filter((state) => state !== void 0).filter((state) => state.cause === "window");
2409
+ 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");
2410
+ const resets = windowMarked.filter((state) => state.kind === "cooldown" && state.until > 0).map((state) => state.until);
2288
2411
  const earliest = resets.length > 0 ? Math.min(...resets) : 0;
2289
2412
  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);
2413
+ 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);
2291
2414
  }
2292
2415
  /**
2293
2416
  * Normalize one resolved credential to the form every consumer sees. Trim
@@ -2538,7 +2661,7 @@ function scaleBy(size, factor) {
2538
2661
  * and API key or subscription, and Command Code's terms apply.
2539
2662
  *
2540
2663
  * Wire protocol (reverse-engineered by the pi plugin, command-code@1.28.4;
2541
- * re-verified against command-code@1.57.0 — endpoints, request shape, and
2664
+ * re-verified against command-code@1.62.0 — endpoints, request shape, and
2542
2665
  * stream events unchanged):
2543
2666
  * POST {apiBase}/alpha/generate
2544
2667
  * body: { config, memory, taste, skills, params: { model, messages, tools,
@@ -2551,7 +2674,7 @@ function scaleBy(size, factor) {
2551
2674
  * per-request options thunk and an API-key resolver from the plugin entry
2552
2675
  * (src/index.ts), so a settings change reaches the very next request.
2553
2676
  */
2554
- const COMMAND_CODE_CLI_VERSION = "1.57.0";
2677
+ const COMMAND_CODE_CLI_VERSION = "1.62.0";
2555
2678
  const DEFAULT_API_BASE = "https://api.commandcode.ai";
2556
2679
  const DEFAULT_GENERATE_MAX_TOKENS = 64e3;
2557
2680
  const DEFAULT_MAX_OUTPUT_TOKENS = 65536;
@@ -4272,11 +4395,12 @@ var CommandCodeAdapter = class extends LlmAdapter {
4272
4395
  continue;
4273
4396
  }
4274
4397
  }
4275
- if (rejection?.reason === "rate-limit" && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4398
+ if (rejection !== void 0 && (rejection.reason === "rate-limit" || rejection.reason === "throttled") && (rejection.resetAtMs !== void 0 || attempt.status !== 429)) {
4276
4399
  const reset = rejection.resetAtMs;
4277
4400
  const wait = reset === void 0 ? 0 : Math.min(Math.max(1e3, reset - Date.now()), RETRY_MAX_DELAY_MS);
4278
4401
  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);
4402
+ const window = rejection.reason === "rate-limit";
4403
+ 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);
4280
4404
  }
4281
4405
  throw generateHttpError(attempt.status, attempt.errText, attempt.retryAfterMs);
4282
4406
  }
@@ -4687,10 +4811,19 @@ function mapFinishReason(reason) {
4687
4811
  * the host should do with them, which is the caller's decision, not this
4688
4812
  * function's:
4689
4813
  *
4690
- * - `rate-limit`: a usage window is spent. The code `RATE_LIMITED` is the
4814
+ * - `rate-limit`: a usage WINDOW is spent. The code `RATE_LIMITED` is the
4691
4815
  * authoritative signal — the CLI accepts the code OR a 429 status — and the
4692
4816
  * body's `error.rateLimit` names the window and its `reset` (in seconds),
4693
4817
  * so the host can hold the account out until the provider's own reset time.
4818
+ * - `throttled`: the provider refused the request (429 or a bare
4819
+ * `RATE_LIMITED` code) without naming a usage window. It is account-scoped
4820
+ * like the above, so it rotates and marks the key the same way, but it is
4821
+ * NOT evidence that a metered window is spent: the CLI's own
4822
+ * `parseWindowLimitError` returns a window limit only when
4823
+ * `resolveWindowLabel` can name one, and a burst/model/spend limiter the
4824
+ * billing endpoint cannot see is exactly this shape (issue #54's report: a
4825
+ * 429 whose probe read five-hour 2% and weekly 10% while the error claimed
4826
+ * both were exhausted).
4694
4827
  * - `invalid-credential`: 401; the key itself is bad.
4695
4828
  * - `unavailable`: the key is fine but this account cannot serve: no credits
4696
4829
  * (`400 Insufficient credits`, the codes `INSUFFICIENT_CREDITS`,
@@ -4702,10 +4835,11 @@ function mapFinishReason(reason) {
4702
4835
  */
4703
4836
  function classifyAccountRejection(status, errText) {
4704
4837
  if (status === 429 || readProviderErrorCode(errText)?.toUpperCase() === "RATE_LIMITED") {
4705
- const resetAtMs = readWindowResetAtMs(errText);
4706
- return resetAtMs === void 0 ? { reason: "rate-limit" } : {
4838
+ const evidence = readWindowLimitEvidence(errText);
4839
+ if (evidence.window === void 0) return { reason: "throttled" };
4840
+ return evidence.resetAtMs === void 0 ? { reason: "rate-limit" } : {
4707
4841
  reason: "rate-limit",
4708
- resetAtMs
4842
+ resetAtMs: evidence.resetAtMs
4709
4843
  };
4710
4844
  }
4711
4845
  if (status === 401) return { reason: "invalid-credential" };
@@ -4726,31 +4860,44 @@ function readProviderErrorCode(errText) {
4726
4860
  }
4727
4861
  }
4728
4862
  /**
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) {
4863
+ * The usage-window evidence carried by a rejected request, mirroring the CLI's
4864
+ * `parseWindowLimitError` → `resolveWindowLabel`/`extractResetAtMs` pair.
4865
+ *
4866
+ * `window` is present only when the body actually names one: a
4867
+ * `error.rateLimit.window` of `fiveHour`/`weekly`/`daily`, or the
4868
+ * "usage limit for your plan" wording (read as `weekly` when it says so, else
4869
+ * `fiveHour`). That is the CLI's own bar for calling a rejection a WINDOW
4870
+ * limit, and it is what keeps a bare 429 — a burst limiter, a model-level
4871
+ * limit, a spend cap `windowLimits` cannot show — from being reported as an
4872
+ * exhausted window (issue #54).
4873
+ *
4874
+ * `resetAtMs` is the reset the body publishes: `error.rateLimit.reset` is
4875
+ * SECONDS (`1e3 * reset`, like the CLI), and a `resets at <ISO>` stamp in the
4876
+ * message is honored as the fallback. Either can be present without the other;
4877
+ * the window label is what decides the classification.
4878
+ */
4879
+ function readWindowLimitEvidence(errText) {
4880
+ let named;
4881
+ let message = "";
4882
+ let seconds;
4736
4883
  try {
4737
4884
  const parsed = JSON.parse(errText);
4738
4885
  if (isRecord(parsed)) {
4739
4886
  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
- }
4887
+ const rateLimit = isRecord(error.rateLimit) ? error.rateLimit : void 0;
4888
+ named = stringValue(rateLimit?.window);
4889
+ message = stringValue(error.message) ?? stringValue(parsed.message) ?? "";
4890
+ seconds = numberValue(rateLimit?.reset);
4748
4891
  }
4749
4892
  } 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;
4893
+ 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;
4894
+ 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];
4895
+ const stamped = stamp === void 0 ? void 0 : Date.parse(stamp);
4896
+ const resetAtMs = seconds !== void 0 && seconds > 0 ? Math.round(seconds * 1e3) : stamped !== void 0 && !Number.isNaN(stamped) ? stamped : void 0;
4897
+ return {
4898
+ ...window === void 0 ? {} : { window },
4899
+ ...resetAtMs === void 0 ? {} : { resetAtMs }
4900
+ };
4754
4901
  }
4755
4902
  function isUpgradeRequiredError(status, errText) {
4756
4903
  if (status !== 403) return false;