@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/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ 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.7] - 2026-09-20
10
+
11
+ ### Fixed
12
+ - **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.
13
+
14
+ ### Changed
15
+
16
+ - **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.
17
+
18
+ ## [0.11.6] - 2026-09-19
19
+
20
+ ### Fixed
21
+ - **An account the user explicitly selected comes back on its own** ([#51](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/51)). With several accounts, pinning one in the **Active account** dropdown and then hitting a 429 on it moved every later request to `default` for the rest of the process, and re-selecting the account in the settings page could not bring it back — only restarting dsh did (the report's 被切换后需要重启 dsh 才能换回来). The mechanism: a 429 marks the key `unknown`, which only a `/alpha/billing/credits` window probe can clear, and the pool's only probe pass runs once EVERY account is marked — a state that never arrives while another account can still serve. The documented promise ("if it is exhausted, requests fall back to other accounts and return once its window resets") was therefore permanent in practice, and because that state is in-memory, a restart was the only way out. `resolveKey()` now re-checks the account the user explicitly asked for — the manual pin, or a model rule's routed slot — before serving the fallback: its window is probed at most once a minute, a window that is no longer exceeded drops the mark and restores the selection on that very request, and an exhausted one is stamped as a cooldown carrying the provider's own `resetAt`, after which the account returns by the clock with no further probe. Everything that keeps the steady state cheap is unchanged: a failed probe keeps the mark and is retried at most once per interval, a 401 stays out until the stored credential changes, the just-rejected key is still skipped during rotation, and an account nobody explicitly asked for is never probed while another one can serve. One visible side effect: a pinned 429 now shows a real cooldown end in the account usage card instead of an open-ended "rate limited". `tests/accounts.test.ts` pins the cleared-window revival, the clock-based return, the throttled failed probe, the `tried`/401 gates, the zero-probe steady state, and the routing case.
22
+ - **An account that cannot serve no longer traps the turn** ([#51](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/51), follow-up report). The same reporter added that it is "not only `default`": a request would land on an unusable account and fail with a bare `400`, frequently, while the other three accounts were fine. Three verified gaps made that possible. (1) Only 429/401 rotated, but the provider's account-scoped rejections are wider — the official CLI's own classifier (command-code@1.56.0: `parseWindowLimitError`, `isInsufficientCreditsRequestError`, `parseSpendCapError` and its terminal-marker list `["premium_credits_exhausted", "model_not_in_plan", "insufficient credits"]`) accepts the code `RATE_LIMITED` on ANY status plus `400 Insufficient credits`, `USAGE_EXCEEDED`, `PREMIUM_CREDITS_EXHAUSTED` and `MODEL_NOT_IN_PLAN`, all of which this adapter reported as a permanent `PROVIDER_HTTP_ERROR` — and a rejection that is neither marked nor rotated keeps the account "usable", so the pool hands it out again on the next request and the same error returns. (2) Even once classified, rotation could not have moved: the pool was asked to exclude only the just-rejected key and answered with the first *usable* account, which is the same key whenever the rejection marked nothing, so the adapter's already-tried guard ended the loop and the accounts behind it were never reached. (3) The revival probe read only `windowLimits.fiveHour`, so an account whose WEEKLY quota was spent but whose five-hour window was open was revived and handed out on every all-marked pass. The adapter now classifies account-scoped rejections with the CLI's rules and rotates on them, the whole tried set travels with the rejection so one request can walk a four-account pool, `error.rateLimit.reset` (seconds) becomes the mark's own cooldown instead of waiting for a probe, and `probeWindowLimits()` reads every window the endpoint publishes. A rejection that is NOT account-scoped — an invalid tool schema, a context overflow — still fails fast on the first attempt. Two deliberate refusals: an `unavailable` rejection is rotated past but never marked (the fact is about the model or the balance rather than the key, either can change without the key changing, and a `:free` model is still served by a credits-empty account), and when the tried set covers every account the pool still answers with its own all-exhausted diagnosis (earliest reset + the retry wait) only if they are all marked — an unmarked rejection is surfaced as itself rather than mislabelled as an exhausted window. `tests/adapter.test.ts` pins the credits/plan rotations, the `RATE_LIMITED`-code-with-reset path and the no-rotation control; `tests/accounts.test.ts` pins the pool walk, the all-tried diagnosis and the marks that must stay probe-eligible.
23
+
24
+ - **The model picker no longer changes with the serving account** ([#51](https://github.com/Mars-Sea/dsh-commandcode-provider/issues/51), same follow-up report: 《卡片会切换》). The picker's **Hide out-of-plan models** filter asked the account that would serve *right now* — `loadBillingAccess()` resolved a single key through the pool — so with accounts on different plans the model list changed as rotation moved between them, and a model only ANOTHER account could run was hidden outright: no request could ever bring it back, because the filter had already removed it from the picker. The question is now asked of every account in the pool (`modelVisibleForAnyAccount()`, fed by a new optional `resolveAccountKeys` seam the plugin entry implements from `pool.resolvedAccounts()`), so a model is hidden only when NO account can run it — the honest question for a pool, and the one that composes with the entitlement rotation above: a model offered because some account includes it is served by that account after at most one rejected attempt. Nothing else about the filter changes: three requests per account per `BILLING_ACCESS_TTL_MS`, cached per key exactly like the single-account path; an account whose facts are unknown counts as "may include it" (the existing fail-open rule, so a transient billing failure still never hides a usable model); a host without the seam falls back to the serving key; and a tier no account reaches (Provider, in a Go+Pro pool) is still hidden. `tests/adapter.test.ts` pins the union through a per-account fetch stub, the still-hidden tier, the unknown-account fail-open and the per-account caching.
25
+
26
+ - **The plugin boots on DSH Desktop** ([#52](https://github.com/Mars-Sea/dsh-commandcode-provider/pull/52)). An older Desktop release validates the entire peer closure of every active plugin, and its runtime tree ships host packages only — so the three peers whose modules the Web frontend already hands to every client bundle through its `staticModules` seed table (`react`, `@deepseek-ai/dsh-client-ui-primitives`, `@deepseek-ai/dsh-client-ui-slots`) were a hard startup failure there (`desktop profile: … requires missing …`) even though no installed copy of them is ever needed: `lib/client.js` requires exactly those three, and the webview resolves them from the seed on every engine this plugin supports. They now carry `peerDependenciesMeta.optional: true`, and every other peer stays required, because a `dsh.client` row is resolved and served by the Host and a harness peer a fresh generation does not install is a bundle that cannot load. `react` also moved into `devDependencies`: npm and pnpm auto-install only NON-optional peers, and the test suite imports the React component tree at runtime, so without it the next `package-lock.json` refresh would have dropped `react` and turned `npm test` red for a reason that looks unrelated to this change. `tests/package.test.ts` pins the exact optional set, that each optional name is a declared peer, and that each stays a development package.
27
+ - **The issue #51 rotation work is hardened where review found it short.** Four paths could still misbehave, each now pinned by a test. (1) A pool whose untried accounts were all marked, while an account the request had ALREADY used sat unmarked — a credits/plan refusal marks nothing — threw a synthesized `INVALID_CREDENTIAL`/`RATE_LIMIT` that named the wrong cause, counted only the tried-filtered subset ("every configured Command Code account (1)…" out of two), and could hand dsh-llm-retry a ~15-minute wait for a permanent failure; `resolveKey()` now returns undefined in that case exactly as the all-tried branch already did, and builds its diagnosis from the pool's FULL account list (a second resolution that came back empty used to report "every configured account (0) was rejected with 401"). (2) A revival dropped the probe throttle's timestamp, so a pinned account whose window probe said "open" while the chat endpoint still answered 429 was probed once per request, forever — the stamp now survives the revival, and the interval's other half is reachable through a new `now()` clock seam instead of a real minute of waiting. (3) The account-scoped classifier read only spaced wordings, so a code-only `400 {"error":{"code":"INSUFFICIENT_CREDITS"}}` — the shape its own fixture implied — rotated nothing; the code list gained `INSUFFICIENT_CREDITS` and the wording check normalizes underscores, so both wire spellings classify. (4) An unroutable window limit on a non-429 status ended the turn as a dead `PROVIDER_HTTP_ERROR` even though the provider had named its reset; it is now a retryable `RATE_LIMIT` carrying that reset (a plain 429 keeps the `Retry-After` mapping it had). Also: an empty scalar `reasoning: ""` no longer shadows a populated `reasoning_details` array in the same chunk — the array layer already refused to let an empty `text` hide a `summary`, and the scalar layer now agrees, keeping the thinking issue #34's tool-loop continuity depends on. `tests/accounts.test.ts` and `tests/adapter.test.ts` pin all of it, including the 5xx negative control (a code-less 5xx rotates nobody) beside the deliberate exception (a 5xx carrying `RATE_LIMITED` is the window limit it says it is — the classifier reads the code before its status guard).
28
+
29
+ ### Changed
30
+
31
+ - **Synced with the official command-code@1.57.0 CLI** (2026-09-19, npm `latest`; no changelog entry yet — the page stops at 1.56.1, and 1.56.2 shipped without one). `COMMAND_CODE_CLI_VERSION` is now `1.57.0`. The CLI's only registry change is the new `z-ai/glm-5.3-flashx` ("GLM-5.3 FlashX": chatComplete, text + image, 1M context, selectable `low`/`high`/`max` efforts, Go and every tier above it, $0.37 in / $1.25 out / $0.075 cache read with no cache-write rate, no deal and no hourly schedule) — the picker now annotates it like its `z-ai/glm-5.3-flash` sibling, through a capability-snapshot entry and a vendored price row. The backend catalog changed under this same check: Meituan's LongCat 2.0 promo ended on 2026-09-19, so the public catalog renamed `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0` ($0.30 in / $1.20 out / $0.006 cache read, same 1M context, text-only, automatic reasoning), the pricing page dropped both the free slug and the 100%-off deal (its deal count fell 6 → 5 and its free count 4 → 3), and the docs list the new id — while the command-code@1.57.0 CLI registry still carries the retired `:free` id, one release behind the backend. The snapshot follows the catalog this plugin actually serves: the new id takes the old one's Go tier, its `FREE` deal is removed rather than left to badge a now-paid model, the automatic-reasoning set is renamed with it, and `longcat-2.0` joins the vendored price table — `node scripts/sync-model-prices.mjs` reports exactly those two rows as real drift. The public catalog serves 71 models, one more than at 1.56.0, with `gpt-6-astra` still a CLI/pricing/docs-only model as before. Static inspection of both bundles finds no transport drift: the `/alpha/*` endpoint set, the `/alpha/generate` converters (`toWireMessages`/`toWireTools`/`toWireToolOutput`) and the stream event vocabulary are byte-identical to 1.56.0, and the version rides every request as `x-command-code-version` as usual. The one 1.56.1 changelog item — "Send session id to BYOK hosts that require it" — adds the `x-opencode-session` header on the BYOK transport, host-gated to `opencode.ai`, so neither of this adapter's transports is affected, and 1.57.0's other additions (a `command-code/<version>` BYOK user agent, a renamed internal warning kind, the removal of the CLI's own image-vision consent prompt) are CLI-side as well. Everything else is re-verified unchanged: the per-model effort map beyond the addition, the subscription plan maps, every plan tier, the five active deals, and the peak/off-peak membership and schedule (still 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday only). One upstream leftover was deliberately NOT snapshotted: the pricing page still embeds a `qwen-3.7-max-2x-usage` deal record for Qwen 3.7 Max, but its own Deals section (five entries) does not list it, its `expires` stamp (2026-06-22) is already in the past, and the vendored rate row already carries the discounted figures.
32
+
9
33
  ## [0.11.5] - 2026-09-18
10
34
 
11
35
  ### 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.5";
2160
+ var version = "0.11.7";
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.54.0";
10
+ declare const COMMAND_CODE_CLI_VERSION = "1.58.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;
@@ -100,6 +100,27 @@ interface CommandCodeConnectionOptions {
100
100
  * text-only request never depends on the attachment seam.
101
101
  */
102
102
  type ResolveAttachments = () => AttachmentStore | undefined;
103
+ /**
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
109
+ * request (no credits, a model outside its plan) — so the pool moves on
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).
115
+ */
116
+ type AccountRotationReason = 'rate-limit' | 'throttled' | 'invalid-credential' | 'unavailable';
117
+ /** What the rotation hook knows about the request it is rotating within. */
118
+ interface AccountRotationContext {
119
+ /** Every API key this request has already used, just-rejected key included. */
120
+ tried: readonly string[];
121
+ /** The provider's own reset time for a `rate-limit` rejection, in millis. */
122
+ resetAtMs?: number;
123
+ }
103
124
  /** Everything the adapter needs beyond the request itself. */
104
125
  interface CommandCodeAdapterDeps<C extends CommandCodeConnectionOptions = CommandCodeConnectionOptions> {
105
126
  /** Resolve the current connection facts (fresh per request, settings-aware). */
@@ -112,13 +133,33 @@ interface CommandCodeAdapterDeps<C extends CommandCodeConnectionOptions = Comman
112
133
  resolveApiKey: (connection: C, model?: string) => Promise<string>;
113
134
  /**
114
135
  * Multi-account rotation hook: the request sent with `rejectedKey` was
115
- * refused with 429 (`rate-limit`) or 401 (`invalid-credential`) before
116
- * any response body streamed. The host marks that key and returns the next
117
- * account's key to retry with, or `undefined` to surface the failure.
118
- * Only pre-stream rejections rotate — a mid-stream failure never replays a
119
- * partially consumed generation against another account.
136
+ * refused before any response body streamed, for an account-scoped reason
137
+ * (see {@link AccountRotationReason}). The host marks that key when the
138
+ * reason warrants it and returns the next account's key to retry with, or
139
+ * `undefined` to surface the failure. Only pre-stream rejections rotate — a
140
+ * mid-stream failure never replays a partially consumed generation against
141
+ * another account.
142
+ *
143
+ * `rotation.tried` lists every key this request has already used (the
144
+ * just-rejected one included). The host must not offer one of them again:
145
+ * with several accounts a rejection that does not mark the key (a plan or
146
+ * balance rejection is model-specific, not account-fatal) would otherwise
147
+ * re-offer the same account on every attempt and the pool could never reach
148
+ * the accounts behind it.
149
+ *
150
+ * `rotation.resetAtMs` carries the provider's own reset time when the
151
+ * rejection body published one, so the host can hold the key out until then
152
+ * instead of waiting for a billing probe to learn the same fact.
153
+ */
154
+ rotateApiKey?: (rejectedKey: string, rejection: AccountRotationReason, connection: C, model?: string, rotation?: AccountRotationContext) => Promise<string | undefined>;
155
+ /**
156
+ * Every API key the host can serve from, in rotation order and deduplicated
157
+ * — the multi-account pool's own list. The picker's plan filter asks all of
158
+ * them, because the pool (not any single account) is what serves a request;
159
+ * a host without a pool omits this seam and the filter falls back to the key
160
+ * that would serve the current request.
120
161
  */
121
- rotateApiKey?: (rejectedKey: string, rejection: 'rate-limit' | 'invalid-credential', connection: C, model?: string) => Promise<string | undefined>;
162
+ resolveAccountKeys?: () => Promise<readonly string[]>;
122
163
  /** HTTP transport override (tests); defaults to the global `fetch`. */
123
164
  fetchImpl?: typeof fetch;
124
165
  /** Resolve the optional durable attachment service for image input (tests); defaults to none. */
@@ -302,11 +343,30 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
302
343
  */
303
344
  private fetchEndpointJson;
304
345
  /**
305
- * The billing facts behind the picker's plan filter, cached for
306
- * {@link BILLING_ACCESS_TTL_MS} and shared across concurrent callers.
307
- * `undefined` means "unknown — show everything" (fail-open).
346
+ * Every account's billing facts behind the picker's plan filter, in the
347
+ * pool's rotation order and cached per key for {@link BILLING_ACCESS_TTL_MS}
348
+ * (so the extra accounts cost their three requests once per TTL, not once
349
+ * per picker load). `undefined` means the filter cannot be evaluated at all
350
+ * — no key resolved — which {@link modelVisibleForAnyAccount} reads as
351
+ * "show everything".
352
+ *
353
+ * The serving account is only the first entry: with several accounts the
354
+ * model list must not depend on which of them rotation happens to be using.
355
+ */
356
+ private loadPoolBillingAccess;
357
+ /**
358
+ * The API keys the picker's plan filter must consult: every account the host
359
+ * can serve from, in rotation order, when the host exposes one. A host
360
+ * without a pool (or without the seam) reports just the key that would serve
361
+ * this request — exactly the pre-pool behaviour.
308
362
  */
309
- private loadBillingAccess;
363
+ private poolAccountKeys;
364
+ /**
365
+ * One account's billing facts, cached for {@link BILLING_ACCESS_TTL_MS} and
366
+ * shared across concurrent callers. `undefined` means "unknown — show
367
+ * everything" (fail-open).
368
+ */
369
+ private loadBillingAccessForKey;
310
370
  /**
311
371
  * The billing facts behind the picker's plan filter, mirroring the CLI's
312
372
  * `createBilling` flow: whoami yields the org id, then the subscriptions
@@ -354,14 +414,29 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
354
414
  */
355
415
  getUsage(apiKey?: string): Promise<CommandCodeUsageReport>;
356
416
  /**
357
- * Probe one account's five-hour window from `/alpha/billing/credits`. The
417
+ * Probe one account's real usage windows from `/alpha/billing/credits`. The
358
418
  * multi-account pool calls this when every account is marked exhausted: an
359
- * account whose window no longer reports `exceeded` is revived, and the
360
- * `resetAt` values feed the "earliest reset" error message. Returns
361
- * `undefined` when the probe itself failed (transport, non-200, or a
362
- * payload without window limits) — a failed probe never changes pool state.
419
+ * account whose windows no longer report `exceeded` is revived, and the
420
+ * `resetAt` values feed the "earliest reset" error message.
421
+ *
422
+ * BOTH windows the endpoint publishes are read, not just `fiveHour`. An
423
+ * account can have an open five-hour window and an exhausted WEEKLY quota at
424
+ * the same time (the two are metered separately), and reading only the
425
+ * shorter one revived such an account as "usable" — the pool then handed it
426
+ * out, the provider rejected the request again, and the next all-marked pass
427
+ * revived it once more, so a used-up account kept coming back (issue #51's
428
+ * follow-up: "切到一个不可用账号"). `exceeded` is therefore true when ANY
429
+ * published window is exceeded, and `resetAt` is the LATEST reset among the
430
+ * exceeded ones — the binding constraint, because a clear five-hour window
431
+ * buys nothing while the weekly quota is spent.
432
+ *
433
+ * Returns `undefined` when the probe itself failed (transport, non-200, or a
434
+ * payload with no window limits at all) — a failed probe never changes pool
435
+ * state. The monthly/purchased/free credit balances are deliberately NOT part
436
+ * of this answer: they are balances rather than windows, they carry no reset
437
+ * time, and the server remains the final gate on them.
363
438
  */
364
- probeFiveHourWindow(apiKey: string): Promise<{
439
+ probeWindowLimits(apiKey: string): Promise<{
365
440
  exceeded: boolean;
366
441
  resetAt: number;
367
442
  } | undefined>;
@@ -391,8 +466,21 @@ interface CommandCodeAccountSlot {
391
466
  /** Whether the official CLI auth file may back this slot (default slot only). */
392
467
  allowAuthFile: boolean;
393
468
  }
394
- /** Why a key stopped serving requests. */
395
- 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';
396
484
  /** One key's rotation state. */
397
485
  interface CommandCodeAccountState {
398
486
  kind:
@@ -402,6 +490,14 @@ interface CommandCodeAccountState {
402
490
  'cooldown' |
403
491
  /** Marked by a 401: skipped until the stored credential changes. */
404
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';
405
501
  /** Human-readable reason for the mark (e.g. `rate limited (429)`). */
406
502
  reason: string;
407
503
  /** Cooldown end in millis; 0 for the other kinds. */
@@ -414,8 +510,13 @@ interface ResolvedAccount {
414
510
  /** The key's current rotation state; undefined means usable. */
415
511
  state: CommandCodeAccountState | undefined;
416
512
  }
417
- /** Five-hour window facts probed from `/alpha/billing/credits`. */
418
- interface FiveHourWindowProbe {
513
+ /**
514
+ * One account's real rate-limit state, probed from `/alpha/billing/credits`:
515
+ * `exceeded` is true when ANY of the account's usage windows is spent, and
516
+ * `resetAt` is the latest reset among them (the binding constraint — an open
517
+ * five-hour window buys nothing while the weekly quota is exhausted).
518
+ */
519
+ interface AccountWindowProbe {
419
520
  exceeded: boolean;
420
521
  resetAt: number;
421
522
  }
@@ -427,13 +528,19 @@ interface CommandCodeAccountPoolDeps {
427
528
  resolveRef(ref: CredentialRef): Promise<string | undefined>;
428
529
  /** The official CLI auth-file key (`~/.commandcode/auth.json`); default slot only. */
429
530
  authFileKey(): string | undefined;
430
- /** Probe one key's five-hour window; undefined when the probe itself failed. */
431
- probeWindow(apiKey: string): Promise<FiveHourWindowProbe | undefined>;
531
+ /**
532
+ * Probe one key's usage windows — BOTH windows the endpoint publishes (the
533
+ * five-hour and the weekly one), not just the five-hour one; see
534
+ * {@link AccountWindowProbe}. Undefined when the probe itself failed.
535
+ */
536
+ probeWindow(apiKey: string): Promise<AccountWindowProbe | undefined>;
432
537
  /**
433
538
  * The manually selected account (a slot id, e.g. `default` or an extra's
434
539
  * credential reference), re-read per resolution. The preferred account
435
540
  * serves whenever it is usable; an unknown id or an exhausted preferred
436
- * account falls back to the first usable slot.
541
+ * account falls back to the first usable slot — and an exhausted one is
542
+ * re-probed on the way (see {@link CommandCodeAccountPool.resolveKey}), so
543
+ * the fallback lasts only as long as the window really is exceeded.
437
544
  */
438
545
  preferredId?(): string | undefined;
439
546
  /**
@@ -445,6 +552,13 @@ interface CommandCodeAccountPoolDeps {
445
552
  * back to the normal selection (the router is a hint, never a hard gate).
446
553
  */
447
554
  modelAccountRules?(): readonly CommandCodeModelAccountRule[];
555
+ /**
556
+ * Clock seam for the explicit-account probe throttle. Tests drive it so the
557
+ * "the interval elapsed, probe again" half of
558
+ * {@link CommandCodeAccountPool.canProbeExplicit} is reachable without
559
+ * waiting a real minute; production reads `Date.now()`.
560
+ */
561
+ now?(): number;
448
562
  }
449
563
  /**
450
564
  * One "route these models to that account" rule. `models` lists catalog ids
@@ -493,6 +607,8 @@ declare class CommandCodeAccountPool {
493
607
  private readonly deps;
494
608
  /** Rotation state by API key. */
495
609
  private readonly states;
610
+ /** Last explicit-revival probe attempt by API key (throttles a failing probe). */
611
+ private readonly explicitProbes;
496
612
  constructor(deps: CommandCodeAccountPoolDeps);
497
613
  /**
498
614
  * Resolve every slot's key, deduplicated by key (first slot wins). Slots
@@ -519,25 +635,42 @@ declare class CommandCodeAccountPool {
519
635
  * `options.model` is the request's model id; routing rules re-read per
520
636
  * resolution, so a settings change applies live.
521
637
  *
522
- * `options.exclude` skips one key during the probe-revival pass: the
523
- * rotation hook excludes the just-rejected key so a probe that clears its
524
- * window cannot re-offer the same key within the same request (the adapter
525
- * refuses already-tried keys; the next request picks the revived key up).
638
+ * `options.tried` lists the keys this request has already used — the
639
+ * just-rejected one included. They are removed from the resolution entirely,
640
+ * which is what lets one request walk a four-account pool: an account-scoped
641
+ * rejection that does not mark the key (no credits, a model outside the
642
+ * account's plan) would otherwise be offered again on every attempt, and the
643
+ * accounts behind it would never be reached.
644
+ *
645
+ * An explicit selection (the pin or a model rule) that a rate-limit mark
646
+ * would demote is probed before the fallback serves, so "falls back while
647
+ * exhausted" never becomes "stays demoted until the process restarts".
526
648
  */
527
649
  resolveKey(options?: {
528
- exclude?: string;
650
+ tried?: readonly string[];
529
651
  model?: string;
530
652
  }): Promise<{
531
653
  key: string;
532
654
  slot: CommandCodeAccountSlot;
533
655
  } | undefined>;
534
656
  /**
535
- * Record a rejection against one key. `rate-limit` (429) marks the key
536
- * exhausted with an unknown reset (probed lazily at the next resolution
537
- * once every account is marked); `invalid-credential` (401) disables the
538
- * 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.
666
+ *
667
+ * `resetAtMs` is seconds-to-millis converted by the adapter from the
668
+ * provider's `error.rateLimit.reset`: knowing the real reset immediately is
669
+ * what keeps an exhausted account out of rotation for exactly as long as the
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.
539
672
  */
540
- markRejected(apiKey: string, rejection: AccountRejection): void;
673
+ markRejected(apiKey: string, rejection: AccountRejection, resetAtMs?: number): void;
541
674
  /**
542
675
  * One account's key: literal → credential seam → auth file (default slot).
543
676
  *
@@ -553,6 +686,35 @@ declare class CommandCodeAccountPool {
553
686
  * agree on one string.
554
687
  */
555
688
  private resolveSlotKey;
689
+ /**
690
+ * The account the user explicitly asked for: the slot a model rule routes
691
+ * the request to when that id exists among the resolved slots, else the
692
+ * manually pinned slot. Consulted only on the fallback path — a usable
693
+ * routed account already returned above — so unlike
694
+ * {@link selectAccountForModel} it does NOT require the account to be
695
+ * usable, which is exactly what {@link resolveKey} re-probes.
696
+ */
697
+ private explicitAccount;
698
+ /**
699
+ * Whether an explicitly selected account's mark is due for a window probe.
700
+ * Only an `unknown` mark (a 429 whose reset was never learned) is worth
701
+ * re-probing: a `cooldown` already carries its reset time and expires by
702
+ * itself, and a `disabled` (401) key stays out until the stored credential
703
+ * changes. The interval bounds a probe endpoint that keeps failing.
704
+ */
705
+ private canProbeExplicit;
706
+ /** The clock the probe throttle reads; injected so a test can travel in time. */
707
+ private now;
708
+ /**
709
+ * Probe one explicitly selected account's window and apply the answer. A
710
+ * window that is no longer exceeded drops the mark, so the user's own
711
+ * selection serves again on this very request. An exceeded one is stamped
712
+ * as a cooldown carrying the provider's reset time, after which
713
+ * {@link accountUsable} lets the account back in with no further probe. A
714
+ * probe that fails changes nothing: the mark stays `unknown` and the next
715
+ * attempt waits out the interval.
716
+ */
717
+ private probeExplicit;
556
718
  /** Hand out the chosen account's key. */
557
719
  private pick;
558
720
  }
@@ -615,8 +777,9 @@ declare const KNOWN_IMAGE_MODELS: ReadonlySet<string>;
615
777
  * `['low', 'medium', 'high']` efforts in command-code@1.38.0 and moved to
616
778
  * `KNOWN_EFFORTS`. `moonshotai/Kimi-K3` followed the same path in
617
779
  * command-code@1.39.3 — it gained `['low', 'high', 'max']` efforts and moved
618
- * to `KNOWN_EFFORTS`. command-code@1.42.0 added `meituan/LongCat-2.0:free`
619
- * (reasoning:!0, no efforts). command-code@1.45.0 gave the Muse Spark family
780
+ * to `KNOWN_EFFORTS`. command-code@1.42.0 added LongCat 2.0 (then
781
+ * `meituan/LongCat-2.0:free`, now the paid `meituan/LongCat-2.0`; reasoning:!0,
782
+ * no efforts). command-code@1.45.0 gave the Muse Spark family
620
783
  * (1.1, 1.2, 1.2-contributor, 1.3, 1.3-contributor) selectable
621
784
  * `['low', 'medium', 'high', 'xhigh']` efforts — they moved to `KNOWN_EFFORTS`.
622
785
  * command-code@1.51.3 gave `MiniMaxAI/MiniMax-M3` selectable
@@ -639,11 +802,14 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
639
802
  * the official plan/pricing pages grants individual-provider/max/ultra and
640
803
  * teams-pro only, and the CLI's plan-access map blocks it on Go/GOAT/Pro.
641
804
  * command-code@1.41.0 added `Qwen/Qwen3.8-Max-0902` (Go) and 1.42.0 added
642
- * `meituan/LongCat-2.0:free` (Go, free promo); command-code@1.43.0 added
805
+ * LongCat 2.0 (Go — a free promo until 2026-09-19, when the backend renamed
806
+ * `meituan/LongCat-2.0:free` to the paid `meituan/LongCat-2.0`); 1.43.0 added
643
807
  * `google/gemini-3.8-flash` (GOAT) and 1.44.0 added `meta/muse-spark-1.3`
644
808
  * (GOAT) plus its Contributor sibling (Go); command-code@1.52.0 added the
645
809
  * free `inclusionai/ling-3.0-flash-sante:free` (Go); command-code@1.53.0
646
- * added `deepseek/deepseek-v4.1-flash` (Go).
810
+ * added `deepseek/deepseek-v4.1-flash` (Go); command-code@1.56.0 added
811
+ * `Qwen/Qwen3.8-Omni-Flash` (Go); command-code@1.57.0 added
812
+ * `z-ai/glm-5.3-flashx` (Go).
647
813
  *
648
814
  * The Provider API exposes no plan metadata, so this snapshot is the source of
649
815
  * truth for the picker's plan annotation — it answers "which plan do I need to