@mars-sea/dsh-commandcode-provider 0.12.0 → 0.12.1

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
@@ -4,6 +4,86 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.12.1] - 2026-09-30
8
+
9
+ ### Added
10
+
11
+ - Upstream skill regression coverage for the three reformats: rendered plan-page rows, a page with no model rows, the extended availability ladder (including a model served only through a `-v1` variant), and changelog parsing — 27 Python tests and 12 Node tests pass.
12
+
13
+ - Optional numeric request timing through `DSH_COMMANDCODE_TIMING`, including request phases, attempts, and first content without request text or credentials.
14
+
15
+ - **Claude models now answer on `/provider/v1/messages` — a real third transport, not the CLI's private protocol.** The Provider API serves the whole Claude family only through that endpoint: posted to `/provider/v1/chat/completions` every catalog Claude id answers `400 Model "<id>" must be called via /provider/v1/messages`, and a non-Claude model posted the other way answers `400 Model "<id>" is not supported on this endpoint. Use /provider/v1/chat/completions for OpenAI and OSS models.` Until now the adapter answered that refusal by routing Claude to `/alpha/generate`, a private protocol with its own body wrapper, its own event vocabulary and its own cache semantics. Re-measured 2026-09-29 against the 84-model catalog: the ten `claude-*` ids are the only ones carrying `supported_endpoints: ["/messages"]`, and `supported_endpoints` is now the **routing authority** — `MESSAGES_ONLY_MODELS` in `./capabilities.ts` dropped to a fallback for a catalog entry that carries no route list, which is every pre-1.55 cache file and any hand-built one, so a Claude model shipping tomorrow still routes correctly through the `claude-*` prefix. The decision is still never written to `protocolCache`, which is keyed by gateway and API key and would otherwise pin the whole ACCOUNT to one transport and drag DeepSeek, GLM and Qwen off the Provider API with it (issue #46); the same rule now governs the `upgrade_required` downgrade, where a Messages refusal is not cached but a Chat Completions one is. `/alpha/generate` remains for Go-plan keys — the one plan without Provider API access, which is the only reason it still exists — and as the fallback.
16
+
17
+ The contract was measured live rather than read off the Anthropic docs, and it departs from `dsh-llm-deepseek`'s Messages API in ways that make copying that adapter fail. **`thinking` accepts only `{ type: 'adaptive' }`**: `enabled` and `disabled` are refused outright (`"thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.`), `between_tools` fails validation, and strength travels in `output_config.effort` over `low | medium | high | xhigh | max` — no `none`, and `xhigh` is absent from DeepSeek's own type, so `reasoningEffort: 'off'` now sends no `output_config` rather than an `off` the endpoint would reject. **`max_tokens` is capped per model** and the endpoint names the ceiling in its own rejection (`max_tokens: 200000 > 128000, which is the maximum allowed number of output tokens for claude-sonnet-5-5`); `/provider/v1/models` publishes no `max_output_tokens`, so the ceiling comes from a new `MODEL_OUTPUT_TOKEN_LIMITS` snapshot, an unlisted Messages-route model falls back to a conservative 64 000, and `modelOutputTokenLimit()` applies it even when the catalog has not warmed — otherwise every first request of a fresh process would have been refused. **Rejections come in three shapes** and all three are now read: the Anthropic envelope, the same envelope with a whole second JSON document inside `message` (unwrapped once for the sentence the user sees, while the raw text is kept for pattern matching, because the embedded `"type":"provider_error"` is part of how the overflow classifier tells an over-reserved request from an ordinary one), and **bare prose** (`Invalid input: expected number, received undefined at max_tokens`), which used to parse to nothing at all. A plan refusal carries no `code` member — `MODEL_NOT_IN_PLAN` is a message PREFIX — and the existing prose classifier already reads it, so the account rotates instead of the turn dying.
18
+
19
+ Two things the endpoint does that the other two transports cannot. **`tool_result.content` accepts image blocks**, so a tool-result image stays inside its tool result instead of being split into a following user message — issue #30's workaround is now scoped to the two wires that actually need it. And **a replayed `thinking` block requires its `signature`** (`thinking.signature: Field required`), which rides the per-block replay envelope and is dropped entirely when no signature was recorded: the OPPOSITE of issue #34, where the DeepSeek route refuses a tool loop whose reasoning was not sent back, but the Messages endpoint accepts the block omitted (verified 200 either way). This is the only transport that writes `replayState.blocks`.
20
+
21
+ ### Changed
22
+
23
+ - **Updated the exact DeepSeek Harness pairing from `0.2.0-rc.1` to `0.2.0-rc.2`.** Every Harness peer and development range, `dsh.compatibility.dsh`, `engines.dsh`, and the single `dshReleases` record now name 0.2.0-rc.2 together, and no business code changed: a release audit over the full public source delta (1025 changed paths, 321 package manifests) found every package this plugin imports with **no breaking change** — `dsh-llm` (including the `GenerateOptions`/`RequestMessage`/`ContentBlock`/`LlmError` wire types), `dsh-settings`, `dsh-credentials`, `dsh-commands`, `dsh-web`, `dsh-typert-protocol`, `dsh-timeout`, `dsh-invariants` and `dsh-launch-environment` carry zero source changes, and the published `.d.ts` of the packages this plugin links against are byte-identical. The rc.2 deltas that do exist are additive and were checked one by one: `dsh-client-ui-primitives` gains `MenuGroup`/`observeStickyMenuGroups` and turns `Input` into a `forwardRef` (this plugin renders its own `InlineInput`, so it is unaffected), `dsh-api-remotes` mounts one more `userQuestionsRemote`, and `dsh-llm`/`dsh-commands` add a `'user-question-reply'` message source. All five slot ids this plugin registers (`settings.section`, `settings.models.provider-card`, `settings.models.footer`, `sidebar.footer.action`, `conversation.composer.dock`) survive in the new slot catalog, the Web shell's `staticModules` seed table (`seed.ts`, `platform.ts`) is byte-identical, and the `data-composer-stats` outlet the cost readout attaches to is still present at its original site. `npm run test:engine` passes against a real `@deepseek-ai/dsh@0.2.0-rc.2`. Deliberately not adopted: the new `user-question-reply` chat node, the `userQuestionPanels` service, the `sidebar.right.tab.files.actions` slot and `ui-commands`' optional `group`/`searchMode` — none of them is required by a surface this plugin owns.
24
+
25
+ - **Upstream sync: command-code@1.68.0 → 1.72.1.** Re-verified against the npm `latest` tarball (sha512-checked before extraction), `/provider/v1/models`, the pricing page's embedded availability, the CLI registry's effort/ZDR/plan tables and the rendered changelog. Everything below is what actually moved; efforts, Vision, automatic reasoning, peak membership and the ZDR exception list were re-checked and are unchanged.
26
+
27
+ **Two models joined the catalog and none left it** (86 rows, up from 84). `gpt-6.1-sol` (command-code@1.71.0, GPT-6.1 Sol, 1 050 000 context) is the **first Max-minimum model this catalog has had**: the page's availability sets every lower tier false and only `individual-max` / `individual-ultra` / `teams-pro` true. `KNOWN_PLANS` therefore records `max`, which needs no new `PLAN_ORDER` entry — `plan-tiers.ts` already ranks `max` above `provider` — so the picker sorts it last and a Pro account's plan filter hides it without further work. It is also Vision-capable and takes the same five-level effort set as `gpt-6-astra`. `inclusionai/ling-3.1-flash:free` (command-code@1.70.0, Ling 3.1 Flash) is free "while it lasts" with the same 100-requests-a-day cap as its predecessor, available on every tier, **text-only**, and — unlike that predecessor — carries selectable `low | medium | high` efforts, so it belongs in `KNOWN_EFFORTS` and NOT in `KNOWN_THINKING_MODELS`.
28
+
29
+ **Grok 4.7's 40%-off launch deal ended and left the page.** Its window ran to 2026-09-27T23:59:59.999Z; the pricing data no longer carries a `deal` member at all and publishes the model at its list price directly. The `KNOWN_DEALS` entry is removed rather than left to lapse, and its price row drops `listRates` for plain `rates` ($2 / $6 / $0.5) — no longer switching with any clock. `Qwen/Qwen3.7-Max` appears in the page's embedded `deal` field with an `expires` of 2026-06-22, three months past, and is **not** recorded: an already-elapsed promotion is evidence of a stale record, not of a live discount. Its row already carries the discounted figures either way.
30
+
31
+ **command-code@1.69.0 moved Go to per-model credits**, adding a second Go row to the CLI's plan maps: `individual-go-v1` (Go, 10 credits/month), recorded alongside `individual-go` at the same `tierWeight` 0 — exactly how the pre-existing `individual-pro-v1` already sits at Pro's weight. Longest-prefix resolution keeps the explicit suffix winning over the base key. That release also introduced `individual-ultra` (Ultra, 300 credits/month) into the availability ladder.
32
+
33
+ **Output ceilings for both arrivals.** `scripts/sync-output-limits.mjs` now covers 76 of the 86 catalog ids. `gpt-6.1-sol` is 128 000 from OpenAI's own row, corroborated by every other provider carrying it. `inclusionai/ling-3.1-flash:free` has **no models.dev row at all** yet, so it is the first entry in the script's new `MANUAL_CEILINGS` map: it takes the Ling family's uniform 32 768 rather than starting at `DEFAULT_MESSAGES_MAX_TOKENS`, and the map exists so a genuine hand-recorded inference survives the rewrite instead of being silently dropped by the "learn it at runtime" fallback. The map is keyed by id and each entry names its evidence; wrong values still cost exactly one refused request before `streamRequest()` remembers the truth (issue #71).
34
+
35
+ ### Fixed
36
+
37
+ - **Three Harness packages the cost-projection tests import are now declared directly.** `tests/cost-projection.test.ts` imports `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-session-projection` by name, but both arrived only as transitive peers of other Harness packages — and the lock file for 0.2.0-rc.2 initially dropped them entirely, which broke `npm ci` outright (`Missing: @deepseek-ai/dsh-session@0.2.0-rc.2 from lock file`, alongside 24 other packages). `dsh-session`, `dsh-scope` and `dsh-session-projection` are now explicit `devDependencies`, so the test tree no longer depends on another package happening to hoist them.
38
+ - **`npm run test:install` no longer depends on one `npm pack --json` output shape.** The script read `packed[0].filename`, which assumes the array form. `npm pack --json` reports an array in every published npm (`logTar` keys each entry by index and the CLI merges numeric keys back into one), but that shape is not contractual, so a keyed object is now accepted too instead of failing with a confusing "did not report a tarball filename".
39
+ - **The upstream compatibility skill's audit script was failing on every run**, independent of any plugin change. Three official pages reformatted around 2026-09-30 and the parser read all three as errors. The per-plan pages (`/docs/plans/go|goat|pro`) replaced each model row's embedded `availability` map with a rendered `minPlanName` + `slug`/`vendor` row, which `page_models()` did not recognise — so every plan looked like it served no models, indistinguishable from a genuinely empty plan. `/docs/plans/max` dropped its table and `/docs/plans/provider` became the Provider API reference, so both now carry no rows at all; that silence is now an expected, recorded shape (`NO_MODEL_ROWS`) which shouts if either starts carrying rows again, rather than an exception that aborted the whole audit before any other check ran. The availability ladder itself grew twice — `individual-ultra` above max, and `-v1` variants beside their base — and `minimum_plan()` was still pinned to the old five keys, so a new tier would have been read as *false*; it now requires every key it knows and treats "absent" as unknown rather than unavailable.
40
+
41
+ Separately, **the changelog was never actually being read.** Its content is server-rendered, so `flight_chunks()` yielded an 832-character empty shell while the plain markup carried every release — a parser waiting on a Markdown or JSON endpoint would report no entries and no error. `changelog_entries()` now strips tags and parses the `[date] // v<version> <product> FEAT…` structure, so the check reads **what** shipped independently of **whether** the version moved: the catalog and plan pages legitimately move without a CLI release. What's New carries a different, version-less shape and is kept as raw prose for review rather than forced through that parser.
42
+
43
+
44
+ - Reuse serialized bodies and prepared images during pre-stream account rotation; bound ordinary transient failures to three retries while retaining separate usage-window and transport budgets.
45
+ - Scope learned and catalog-published output limits, billing access, and protocol choices to `apiBase`. Disk catalog cache version 3 records its gateway and rejects unscoped or mismatched files.
46
+ - **The consecutive-header-timeout streak was shared across every concurrent session on the process, not scoped to one.** `CommandCodeAdapter` is registered once and reused for every agent, but `headerTimeoutStreak` lived as a single instance field rather than the `WeakMap<agent, …>` pattern the new transient/transport retry budgets use. Two failure modes followed from that: a success on ANY session cleared every other session's in-progress streak (the escalation to a terminal diagnosis after `MAX_CONSECUTIVE_HEADER_TIMEOUTS` almost never fired under concurrency), and, since the streak also compares a body fingerprint, two sessions repeatedly timing out on different request shapes kept resetting each other's count back to 1 (same effect, opposite mechanism). The field is now `headerTimeoutStreaks`, a `Map` keyed by `GenerateOptions.sessionId` (not `agent`: `GenerateOptions` carries no agent reference) with FIFO eviction past 200 entries; a session-less call keeps the old single-slot behavior via a fixed fallback key. `tests/adapter.test.ts` pins independent per-session accumulation and the session-less fallback.
47
+
48
+ - **Claude sessions were billed at the full input rate on every turn — the Messages route never asked for a cache.** Anthropic's prompt caching is opt-in per content block: a prefix is stored only when a `cache_control` marker names it, unlike the automatic prefix caching the OpenAI-shaped routes get. The adapter set exactly one marker on the whole plugin, on the CLI route's system section. The Messages route — the only route the entire Claude family answers on — sent **no marker at all**, so every turn re-prefilled the whole replayed history at the undiscounted input price. A 10-turn `claude-sonnet-5-5` session measured 2026-09-29 billed **$0.5648**, and each of its ten requests matched the undiscounted formula to the cent (`37898×$2/M + 1312×$10/M = $0.088916` against a logged `$0.0889`), which is what establishes the zero rather than any display bug — one cached token anywhere in the session would have moved the total. `mapMessagesUsage()` was already reading `cache_read_input_tokens` and `cache_creation_input_tokens` correctly, so the counters were faithfully reporting a zero the request had asked for.
49
+
50
+ The body now carries **three ephemeral breakpoints** — the `system` block, the LAST tool declaration, and the last block of the last `user` message — for **$0.1869 on that same session, a 66.9% reduction**, with no other field changed. The last user turn is the rolling boundary: DSH resends the full history each turn, so a marker there grows with the conversation, while a `system`-only marker would leave every turn's history uncached. `messagesMessageCodec` renders tool results as `role: 'user'`, so a tool loop's trailing turn is marked too; `thinking` and `tool_use` blocks never are, since a breakpoint there caches a prefix the next turn invalidates. `system` becomes a one-block array on this route only — the same shape the CLI route has always sent, and the only way a string field could carry a marker. The CLI and OpenAI routes are untouched, and `tests/adapter.test.ts` pins both.
51
+
52
+ **The placements are copied from the official client, not invented.** Command Code ships
53
+ [`pi-commandcode-provider`](https://github.com/CommandCodeAI/pi-commandcode-provider), which calls this
54
+ same `/provider/v1/messages` endpoint through `@earendil-works/pi-ai` and hand-writes no marker at all:
55
+ that library's `anthropic-messages` transport marks precisely these three positions, and reads
56
+ `pricing.cache_read` / `pricing.cache_write` from the model list to price the result. Both artifacts were
57
+ checked against their registry-published sha512 digests (`pi-ai` 0.87.1, `command-code` 1.68.0, both
58
+ matching) rather than taken on trust, and `command-code` 1.68.0 separately shows the CLI marks only its own
59
+ system section and never touches this endpoint — which is why the CLI-route cache regressions in
60
+ [docs/issue-64-cache-review.md](../docs/issue-64-cache-review.md) say nothing about this one. **This could
61
+ not be confirmed against a live Command Code response** — the account had no remaining quota — so the
62
+ claim that the gateway honours the field rests on the official provider depending on it for its own cost
63
+ display, not on a measured `cache_read_input_tokens`. The downside is bounded either way: a gateway that
64
+ ignored the field would leave price and shape exactly as they are, and one that honoured it without a hit
65
+ would bill the first turn's write at $2.5/M against $2/M, with every later turn hitting inside the
66
+ 5-minute window.
67
+
68
+ - **A lone UTF-16 surrogate half in any outgoing string is now removed before it reaches the wire.** A surrogate half is not a character — it comes from a truncated slice, a lossy decode, or a stray `\uD83D` escape — and `JSON.stringify` does **not** reject it: it serializes to the escape `"\ud83d"` and leaves the process as a string no UTF-8 consumer can round-trip. Whether that is fatal depends entirely on the receiving decoder, and the official provider for pi strips these from every outgoing string on this same endpoint, which is the evidence this change rests on. The strip is applied through the `JSON.stringify` replacer at the **single point a request body is serialized**, so all three transports and every field — system text, message content, tool schemas, tool results — are covered at once, rather than enumerating the fields that can carry text and missing one. **This is defensive, not a reproduced crash**: measured locally, `JSON.stringify` accepts a lone surrogate without complaint and the bytes stay valid UTF-8, so the failure it guards against is a strict upstream decoder, which could not be exercised without quota.
69
+
70
+ Two details are load-bearing. **Paired surrogates must survive** — a well-formed astral character IS a surrogate pair, so a naive range filter would delete every emoji; `tests/adapter.test.ts` pins that both a lone half and an emoji are handled correctly. And the **exact** pattern needs lookaround, which V8 cannot turn into a fast scan: on a 412 KB request body it costs 1.40 ms against a bare `JSON.stringify` of 0.59 ms. A flagless range pre-test gates it, because real prompts are ASCII-plus-CJK with no surrogates at all, bringing the ordinary case to 0.77 ms. The pre-test deliberately uses a **non-global** regex: a global one is stateful, and testing with it would advance `lastIndex` so consecutive calls on the same string alternated between true and false.
71
+
72
+ - **A model that refuses `max_tokens` now costs one wasted request, not a failed turn — and the failure is no longer blamed on the context.** Issue #71: `claude-sonnet-5-5` sends `max_tokens: 131072` (the global default, which is 128 × 1024) against a real ceiling of 128 000, and the Messages endpoint refuses it by name. Two things went wrong, and the first one hid the second. The refusal text contains the word `max_tokens`, which `CLI_CONTEXT_OVERFLOW_PATTERN` matches, so it was classified `CONTEXT_WINDOW_EXCEEDED` — the harness then compacted a session that was not oversized, resent the **identical** request, was refused identically, and told the user to start a new conversation that could not possibly work. `OUTPUT_LIMIT_REJECTION` was already claiming those shapes for the HTTP path; the fix here extends the same ordering to `streamErrorToLlmError()`, which is the path issue #71 actually reported (`Command Code stream error: …`, a 200 body carrying an in-stream error frame, not a 400).
73
+
74
+ The refusal is now **retried once at the ceiling the endpoint states**: `streamRequest()` wraps `streamAttempt()` and, when a failure arrives before the first chunk reaches the host, parses Y out of `max_tokens: X > Y`, records it and re-sends. Both shapes are covered because the recovery is identical for each. What makes it pay off is the record — `learnedOutputLimits` is process-wide and only ever tightens, so the turn that *is* refused still leaves every later turn for that model built correctly, without spending a 400 to find out again. A refusal arriving after a delta has been delivered is not retried (the answer is already partly on screen; a second request would duplicate it), and a second refusal within one turn surfaces, so this is one downshift per turn and not a loop. `streamAttempt()` now takes the smaller of the catalog row and the learned value, so a warm catalog cannot mask a limit the gateway has already stated.
75
+
76
+ - **The Messages route no longer halves every Opus answer to avoid a refusal it can now predict.** With the ceiling known, the `claude-*` fallback of 64 000 that predates the table is no longer needed for the family it was written for. `MODEL_OUTPUT_TOKEN_LIMITS` is now **generated** by `scripts/sync-output-limits.mjs` from models.dev rather than hand-maintained from whatever 400 a user happened to hit, and it covers **74 of the 84 catalog ids** — not only the Claude and GPT families, but the open-weight models too, because Command Code sells those through their **vendor's own API** and the vendor's published ceiling is the one worth writing. The script resolves it in order: the vendor's own catalog entry (matched on the normalized id tail, since Command Code namespaces `Qwen/Qwen3.8-Max` while the vendor files `qwen3.8-max`), the same entry with a `free` suffix dropped — both spellings occur, `inclusionai/ling-3.0-flash-sante:free` and `poolside/laguna-s-2.1-free` — and finally unanimity across every provider carrying the id.
77
+
78
+ An earlier pass at this took the opposite line: it wrote a value only where every provider agreed, and left all 48 open-weight models to runtime learning. That was over-cautious in a way that cost real capability. A cross-provider aggregation disagrees precisely because each host configures its own deployment — `moonshotai/Kimi-K3` carries nine `output` values from 8000 to 1048576 across 26 providers — so unanimity was never going to arrive for exactly the models that needed an answer, and "take the minimum" would have cut Kimi to 8000 tokens. The vendor figure is the correct one to start from, and when the gateway is stricter than the vendor the runtime correction above handles it: a ceiling that is too high costs one round trip, while a conservative guess costs half of every answer for as long as the table stands. The 10 models no vendor publishes yet (`Qwen/Qwen3.8-27B`, `deepseek/deepseek-v4.1-flash`, `moonshotai/Kimi-K2.5`, `zai-org/GLM-5.2-Fast`, `thinkingmachines/inkling-small`, two `tencent/hy*`, three id-only rows) stay unlisted and are learned the same way.
79
+
80
+ - **Messages asked a non-reasoning model to reason.** `thinking: { type: 'adaptive' }` was sent unconditionally, but that switch exists to be driven by `output_config.effort` — the endpoint's own wording is "Use `thinking.type.adaptive` AND `output_config.effort` to control thinking behavior". `claude-haiku-4-5-20251001` has neither: no effort levels, and the official CLI's model table marks it neither `reasoning_effort` nor `reasoning:!0`, so it does not reason. It was being handed half of a pair that controls nothing. The switch now travels only when `KNOWN_EFFORTS` publishes levels for the model (reusing the lookup `stream()` already performs), leaving nine of the ten Claude models untouched. **This one could not be confirmed against a live response**: every request for that model stops at 403 `MODEL_NOT_IN_PLAN` on a GOAT-plan tier, before generation, so it rests on the official model table plus the endpoint's field validation — the switch is accepted there, it is simply meaningless. Related, and measured: the switch does **not** suppress thinking (six live requests across `adaptive` present/absent × `temperature` present/absent all produced a thinking block), so an apparent difference between two probe runs was the `max` vs `xhigh` effort rather than the switch.
81
+
82
+ - **Messages never sends `temperature` — adaptive thinking refuses any other value.** The body builder copied the other two transports' `temperature: options.temperature ?? 0.3`, and the endpoint answers `` `temperature` may only be set to 1 when thinking is enabled or in adaptive mode ``. **Every real request was refused with HTTP 400 while all 722 unit tests passed**: the failure only exists on the wire, which is what `scripts/verify-messages-live.mjs` now exists to catch. Omission is the only field state that satisfies both the thinking switch and a caller that asked for a temperature, so Claude loses sampling control on this route because the endpoint removed it. `tests/adapter.test.ts` pins the absence across `temperature` values 0 / 0.3 / 1 / 2.
83
+
84
+ - **A `max_tokens` rejection no longer triggers an expensive, useless compaction.** The context-overflow pattern matches a bare `max_tokens`, so both a ceiling refusal and a plain validation error on that field were reported as `CONTEXT_WINDOW_EXCEEDED` — which hands the turn to `dsh-compaction-basic`, and the harness then compacts a session that was never oversized, at full price on a long context, and is refused identically on the retry. `OUTPUT_LIMIT_REJECTION` now claims the request-side shapes first and returns a bilingual `PROVIDER_HTTP_ERROR` quoting the endpoint's own numbers, so the user learns which setting to lower instead of watching a compaction that cannot help.
85
+
86
+
7
87
  ## [0.12.0] - 2026-09-29
8
88
 
9
89
  ### Added
package/README.md CHANGED
@@ -35,7 +35,7 @@ See [Screenshots](#screenshots) below for what the UI looks like.
35
35
 
36
36
  ## Install
37
37
 
38
- This release supports **dsh 0.2.0-rc.1 and nothing else** — the plugin's peer
38
+ This release supports **dsh 0.2.0-rc.2 and nothing else** — the plugin's peer
39
39
  range is that one version, and its compatibility record names it alone:
40
40
 
41
41
  ```sh
@@ -70,7 +70,7 @@ Fresh pnpm 10 marketplace generations are supported directly. Do not add a separ
70
70
  Update with the same tag you installed with:
71
71
 
72
72
  ```sh
73
- dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest # dsh 0.2.0-rc.1
73
+ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest # dsh 0.2.0-rc.2
74
74
  dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 # older dsh (0.5.0 line, unmaintained)
75
75
  ```
76
76
 
@@ -144,7 +144,7 @@ cmd login # writes ~/.commandcode/auth.json
144
144
  cwd: !!js process.cwd()
145
145
  ```
146
146
 
147
- **Engine version.** The plugin is maintained against exactly one engine: **dsh 0.2.0-rc.1**. Its `@deepseek-ai/dsh-*` peer range is `^0.2.0-rc.1` (semver resolves that to 0.2.0-rc.1 alone), and `dsh.compatibility.dshReleases` records that single release. On an older engine the settings page, the message envelope, or the request-image budget will not line up — install the last release that supported your engine (see [Install](#install)) instead of forcing this one.
147
+ **Engine version.** The plugin is maintained against exactly one engine: **dsh 0.2.0-rc.2**. Its `@deepseek-ai/dsh-*` peer range is `^0.2.0-rc.2` (semver resolves that to 0.2.0-rc.2 alone), and `dsh.compatibility.dshReleases` records that single release. On an older engine the settings page, the message envelope, or the request-image budget will not line up — install the last release that supported your engine (see [Install](#install)) instead of forcing this one.
148
148
 
149
149
  ## Usage dashboard
150
150
 
package/README.zh-CN.md CHANGED
@@ -33,7 +33,7 @@
33
33
 
34
34
  ## 安装
35
35
 
36
- 本版本**只支持 dsh 0.2.0-rc.1**:插件的 peer 范围就是这一个版本,兼容性记录里也只列它。
36
+ 本版本**只支持 dsh 0.2.0-rc.2**:插件的 peer 范围就是这一个版本,兼容性记录里也只列它。
37
37
 
38
38
  ```sh
39
39
  dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@latest
@@ -62,7 +62,7 @@ dsh plugin --profile web add @mars-sea/dsh-commandcode-provider@0.11.15
62
62
  用与安装时相同的 tag 更新:
63
63
 
64
64
  ```sh
65
- dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest # dsh 0.2.0-rc.1
65
+ dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@latest # dsh 0.2.0-rc.2
66
66
  dsh plugin --profile web update @mars-sea/dsh-commandcode-provider@0.9.1 # 更早的 dsh(0.5.0 线,不再维护)
67
67
  ```
68
68
 
@@ -136,7 +136,7 @@ cmd login # 写入 ~/.commandcode/auth.json
136
136
  cwd: !!js process.cwd()
137
137
  ```
138
138
 
139
- **引擎版本要求。** 插件只针对一个引擎维护:**dsh 0.2.0-rc.1**。它的 `@deepseek-ai/dsh-*` peer 范围是 `^0.2.0-rc.1`(按 semver,这只会解析到 0.2.0-rc.1),`dsh.compatibility.dshReleases` 也只记录这一个版本。在更早的引擎上,设置页、消息封装或请求图片预算总有一处对不上——请改装支持你所用引擎的最后一个版本,而不是硬装这一个。
139
+ **引擎版本要求。** 插件只针对一个引擎维护:**dsh 0.2.0-rc.2**。它的 `@deepseek-ai/dsh-*` peer 范围是 `^0.2.0-rc.2`(按 semver,这只会解析到 0.2.0-rc.2),`dsh.compatibility.dshReleases` 也只记录这一个版本。在更早的引擎上,设置页、消息封装或请求图片预算总有一处对不上——请改装支持你所用引擎的最后一个版本,而不是硬装这一个。
140
140
 
141
141
  ## 用量面板
142
142
 
package/lib/client.js CHANGED
@@ -2262,7 +2262,7 @@ window.__ModuleLoader__.load({
2262
2262
  }
2263
2263
  //#endregion
2264
2264
  //#region package.json
2265
- var version = "0.12.0";
2265
+ var version = "0.12.1";
2266
2266
  var repository = {
2267
2267
  "type": "git",
2268
2268
  "url": "git+https://github.com/Mars-Sea/dsh-commandcode-provider.git"
package/lib/index.d.ts CHANGED
@@ -6,8 +6,30 @@ import { WebRuntime, WebSearchProvider, WebSearchRequest, WebSearchResult } from
6
6
  import { Context } from "@deepseek-ai/cordis";
7
7
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
8
8
  import { CommandDefinition } from "@deepseek-ai/dsh-commands";
9
+ //#region src/request-timing.d.ts
10
+ type Phase = 'credentials' | 'images' | 'body' | 'serialization' | 'headers';
11
+ type Outcome = 'finished' | 'error' | 'aborted' | 'cancelled';
12
+ interface RequestTimingSummary {
13
+ version: 1;
14
+ startedAt: number;
15
+ model: string;
16
+ outcome: Outcome;
17
+ errorCode?: string;
18
+ totalMs: number;
19
+ phasesMs: Record<Phase, number>;
20
+ firstByteMs?: number;
21
+ firstContentMs?: number;
22
+ attempts: Array<{
23
+ protocol: string;
24
+ bodyBytes: number;
25
+ headersMs: number;
26
+ status?: number;
27
+ }>;
28
+ }
29
+ type RequestTimingSink = (summary: RequestTimingSummary) => void | Promise<void>;
30
+ //#endregion
9
31
  //#region src/adapter.d.ts
10
- declare const COMMAND_CODE_CLI_VERSION = "1.68.0";
32
+ declare const COMMAND_CODE_CLI_VERSION = "1.72.1";
11
33
  declare const DEFAULT_API_BASE = "https://api.commandcode.ai";
12
34
  /**
13
35
  * The output budget this bundle asks for, and the ceiling it will never exceed.
@@ -42,8 +64,18 @@ declare const DEFAULT_GENERATE_MAX_TOKENS = 131072;
42
64
  declare const DEFAULT_MAX_OUTPUT_TOKENS = 131072;
43
65
  /** How long the picker's plan-filter billing facts stay cached before refetching. */
44
66
  declare const BILLING_ACCESS_TTL_MS: number;
45
- /** Endpoint protocol selected for one generate call. */
46
- type CommandCodeProtocol = 'cli' | 'openai';
67
+ /**
68
+ * Endpoint protocol selected for one generate call.
69
+ *
70
+ * `messages` is the Anthropic Messages surface at
71
+ * `{apiBase}/provider/v1/messages`, the only Provider API route the Claude
72
+ * family answers (posted to `/chat/completions` they refuse with `400 Model
73
+ * "<id>" must be called via /provider/v1/messages`). `openai` is the
74
+ * documented Chat Completions surface; `cli` is Command Code's private
75
+ * `/alpha/generate` transport, kept for Go-plan keys (the one plan without
76
+ * Provider API access) and as the `upgrade_required` fallback.
77
+ */
78
+ type CommandCodeProtocol = 'cli' | 'openai' | 'messages';
47
79
  /**
48
80
  * Head-of-request timeout: how long to wait for the first response byte.
49
81
  *
@@ -181,6 +213,8 @@ interface AccountRotationContext {
181
213
  }
182
214
  /** Everything the adapter needs beyond the request itself. */
183
215
  interface CommandCodeAdapterDeps<C extends CommandCodeConnectionOptions = CommandCodeConnectionOptions> {
216
+ /** 可选的数值耗时接收器;不接收提示词、凭据或错误正文。 */
217
+ onRequestTiming?: RequestTimingSink;
184
218
  /** Resolve the current connection facts (fresh per request, settings-aware). */
185
219
  options: () => C;
186
220
  /**
@@ -326,6 +360,7 @@ interface CommandCodeUsageReport {
326
360
  declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = CommandCodeConnectionOptions> extends LlmAdapter {
327
361
  private readonly deps;
328
362
  private catalog;
363
+ private catalogState;
329
364
  private readonly fetchImpl;
330
365
  private readonly resolveAttachments;
331
366
  /**
@@ -336,24 +371,32 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
336
371
  private readonly billingAccess;
337
372
  private readonly billingAccessInflight;
338
373
  private readonly protocolCache;
374
+ /** Namespace account-derived facts by gateway without exposing the key. */
375
+ private accountCacheKey;
339
376
  /**
340
377
  * Consecutive header timeouts for the last request shape, counted ACROSS
341
378
  * `stream()` calls: dsh-llm-retry re-invokes this same instance, so this is
342
- * the only place a streak can live. One slot rather than a map is the point —
343
- * the claim is "consecutive", and a single record cannot grow.
344
- */
345
- private headerTimeoutStreak;
379
+ * the only place a streak can live. Keyed by `sessionId` (not by `agent`:
380
+ * `GenerateOptions` carries no agent reference, only `sessionId`) so
381
+ * concurrent sessions on this one shared adapter instance cannot clear or
382
+ * inflate each other's count. A call with no `sessionId` (a one-shot,
383
+ * session-less caller) falls back to {@link GLOBAL_HEADER_TIMEOUT_KEY},
384
+ * matching the old single-slot behavior for exactly that case. Bounded by
385
+ * {@link MAX_HEADER_TIMEOUT_STREAK_ENTRIES} with FIFO eviction (`Map`
386
+ * preserves insertion order) since the key space is per-session rather than
387
+ * per-account/model, so it is not naturally small like `protocolCache`.
388
+ */
389
+ private readonly headerTimeoutStreaks;
390
+ private static readonly MAX_HEADER_TIMEOUT_STREAK_ENTRIES;
346
391
  constructor(deps: CommandCodeAdapterDeps<C>);
347
392
  /**
348
393
  * Grade one failed attempt: a headers timeout that repeats on an unchanged
349
394
  * payload eventually leaves the retryable whitelist.
350
395
  *
351
396
  * `providerRetryPolicy()` cannot express this itself — dsh-llm's
352
- * `NormalRetryPolicyConfig` carries a single scalar `maxRetries` next to the
353
- * code list, so there is no per-code limit to lower. Escalating from here is
354
- * the only way to keep `RATE_LIMIT` / `SERVER` / `TRANSPORT` retryable (each
355
- * carries a real "try again later" signal) while stopping the one code that,
356
- * repeated unchanged, only burns the user's session.
397
+ * `NormalRetryPolicyConfig` carries one `maxRetries` for every listed code.
398
+ * Escalating here stops repeated identical header timeouts with a specific
399
+ * diagnosis; the agent listener separately caps ordinary transient errors.
357
400
  *
358
401
  * @returns the error to surface: unchanged for any other failure, the
359
402
  * original timeout below the threshold, and a terminal one at it.
@@ -368,12 +411,11 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
368
411
  */
369
412
  providerInfo(provider: string): LlmProviderInfo;
370
413
  /**
371
- * Near-unbounded retry for transient failures only (`mode: 'normal'` with
372
- * an explicit 1000-attempt cap — opencode-style persistence without the
373
- * unbounded loop): `RATE_LIMIT`/`SERVER`/`TIMEOUT`/`TRANSPORT`/
374
- * `EMPTY_RESPONSE` retry up to 1000 times with waits doubling from 500 ms
375
- * and capping at 15 minutes (±10% jitter), so an exhausted 5-hour window
376
- * recovers in-session instead of failing after two tries. Permanent failures
414
+ * The official executor owns backoff and provider reset waits. Its high
415
+ * route-wide cap lets a confirmed `RATE_LIMIT` window recover in-session;
416
+ * our request-error listener limits `SERVER`/`TIMEOUT`/`EMPTY_RESPONSE`/
417
+ * `THROTTLED` to three retries and `TRANSPORT` to its separate budget.
418
+ * Waits double from 500 ms and cap at 15 minutes (±10% jitter). Permanent failures
377
419
  * (an invalid key's `INVALID_CREDENTIAL`, `UNSUPPORTED_CONTENT`, plan
378
420
  * rejections) are absent from the whitelist and surface immediately instead
379
421
  * of looping. Waits the pool/adapter attach as `providerRetryAfterMs` are
@@ -397,7 +439,9 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
397
439
  * requirement — the meter throws when the counts differ.
398
440
  */
399
441
  imageRequestPricing(_provider: string, model: string): LlmImageRequestPricing | undefined;
400
- /** Refresh the catalog (live fetch, cache fallback) and return it. */
442
+ /** 地址或缓存文件变化时丢弃旧目录,防止旧网关的路由声明影响新请求。 */
443
+ private currentCatalogState;
444
+ /** 共享一次刷新;调用者取消只结束自己的等待,不取消其他调用者的请求。 */
401
445
  private loadCatalog;
402
446
  listModels(provider: string, opts?: {
403
447
  unfiltered?: boolean;
@@ -466,25 +510,36 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
466
510
  /** Fresh cached billing tier weight for a key, or undefined when not known. */
467
511
  private cachedBillingTierWeight;
468
512
  /** Cached protocol decision for a key, or undefined when expired/unknown. */
469
- private cachedProtocolUseCli;
513
+ private cachedProtocol;
470
514
  private rememberProtocol;
471
515
  /**
472
- * Choose the initial protocol for one request. A fresh protocol cache entry
473
- * wins; otherwise a cached (not network-fetched) billing tier of Go is
474
- * treated as CLI-only. Unknown accounts default to Provider API and fall
475
- * back only after an `upgrade_required` rejection.
516
+ * True when the catalog routes `model` to `/provider/v1/messages`.
517
+ *
518
+ * The catalog's own `supported_endpoints` is authoritative (it is what
519
+ * upstream publishes per model); `requiresMessagesEndpoint()` — the
520
+ * `claude-*` prefix rule — is the fallback for a catalog entry that carries
521
+ * no route list, which is the shape every pre-1.55 cache file has.
522
+ */
523
+ private routesToMessages;
524
+ /**
525
+ * Choose the initial protocol for one request.
476
526
  *
477
- * Models the Provider API serves ONLY through `/provider/v1/messages` (the
478
- * Claude family — `requiresMessagesEndpoint()`) always take the CLI
479
- * transport, on any tier and even under a forced `'openai'` preference:
480
- * that option is documented as "prefer the Provider API, still fall back",
481
- * and the 400 these models answer with is not a preference to be honoured
482
- * but a hard refusal. `/alpha/generate` routes every one of them, so this
483
- * costs nothing.
527
+ * Models the Provider API serves only through `/provider/v1/messages` (the
528
+ * Claude family) take the Messages transport on any tier and even under a
529
+ * forced `'openai'` preference: that option is documented as "prefer the
530
+ * Provider API, still fall back", and the 400 these models answer with on
531
+ * Chat Completions (`Model "<id>" must be called via /provider/v1/messages`)
532
+ * is a hard refusal, not a preference to honour.
484
533
  *
485
534
  * That decision is deliberately NOT written to `protocolCache`, which is
486
- * keyed by API key alone: remembering it would pin the whole ACCOUNT to the
487
- * CLI transport and drag every other model off it until the entry expired.
535
+ * keyed by gateway and API key: it depends on the model, not just the account, so
536
+ * remembering it would pin the whole ACCOUNT to one transport and drag
537
+ * every other model along (issue #46).
538
+ *
539
+ * Otherwise a fresh protocol cache entry wins, then a cached (not
540
+ * network-fetched) billing tier of Go — the one plan without Provider API
541
+ * access. Unknown accounts default to Chat Completions and fall back to the
542
+ * CLI transport only after an `upgrade_required` rejection.
488
543
  */
489
544
  private resolveProtocol;
490
545
  /**
@@ -535,6 +590,26 @@ declare class CommandCodeAdapter<C extends CommandCodeConnectionOptions = Comman
535
590
  resetAt: number;
536
591
  } | undefined>;
537
592
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
593
+ /**
594
+ * One turn, with at most one output-ceiling downshift.
595
+ *
596
+ * The ceiling a model accepts is not published anywhere the request can be
597
+ * built from (see `modelOutputTokenLimit`), and the endpoint states it only
598
+ * by refusing. That refusal is worth one retry rather than a failed turn: the
599
+ * harness's own recovery for it — compact the context — cannot help, because
600
+ * nothing about the context is wrong, and resending the identical request
601
+ * would be refused identically (issue #71). Reading the stated ceiling off
602
+ * the rejection turns one wasted turn into one, for good: it is recorded
603
+ * process-wide, so the next request for this model is built correctly
604
+ * without spending the round trip again.
605
+ *
606
+ * Only BEFORE the first chunk reaches the host. Once a delta has been
607
+ * delivered the answer is already partly on screen, and a second request
608
+ * would duplicate it — so from that point the error stands and says what is
609
+ * actually wrong. One downshift per turn, and a second refusal surfaces.
610
+ */
611
+ private streamRequest;
612
+ private streamAttempt;
538
613
  }
539
614
  //#endregion
540
615
  //#region src/accounts.d.ts
@@ -821,7 +896,7 @@ declare const KNOWN_EFFORTS: Readonly<Record<string, readonly string[]>>;
821
896
  declare const KNOWN_IMAGE_MODELS: ReadonlySet<string>;
822
897
  /**
823
898
  * Models WITHOUT a zero-data-retention upstream, per the official CLI's own
824
- * registry (`command-code@1.68.0` `dist/cli.mjs`): `modelSupportsZdr(id)` is
899
+ * registry (`command-code@1.72.1` `dist/cli.mjs`): `modelSupportsZdr(id)` is
825
900
  * exactly `!nonZdrSet.has(canonicalize(id))`, and `knownModelSupportsZdr`
826
901
  * carries the same membership in the sibling route table — the UNION of both
827
902
  * is this set. Reading only the sibling route table would drop `meituan/
@@ -882,7 +957,7 @@ declare const KNOWN_THINKING_MODELS: ReadonlySet<string>;
882
957
  * `/docs/plans/max` and `/docs/resources/pricing-limits`). Each plan's model
883
958
  * list is a superset of the one below it: Go ⊂ GOAT ⊂ Pro ⊂ Provider/Max.
884
959
  * Models absent from every plan list (Claude Opus/Fable, Fugu Ultra) are
885
- * Provider-tier. Re-verified at command-code@1.68.0 (2026-09-29): 84 catalog
960
+ * Provider-tier. Re-verified at command-code@1.72.1 (2026-09-30): 86 catalog
886
961
  * ids at 53/62/76/84 cumulative, a strict superset chain — every release since
887
962
  * 1.49.0 has been additive with no tier move, and per-entry tags below name the
888
963
  * release that added each row.
@@ -911,7 +986,7 @@ declare function compareByPlan(a: {
911
986
  /**
912
987
  * Subscription plan table, synced from the official CLI bundle's plan maps
913
988
  * (located by the `"individual-go"` key in `dist/cli.mjs`, re-verified unchanged
914
- * through command-code@1.68.0): subscription `planId` prefix → display name and
989
+ * through command-code@1.72.1): subscription `planId` prefix → display name and
915
990
  * the plan's monthly credit total. This is the account's own subscription
916
991
  * (from `/alpha/billing/subscriptions`) — distinct from {@link KNOWN_PLANS},
917
992
  * which maps catalog models to their minimum tier.
@@ -1798,9 +1873,9 @@ interface Config {
1798
1873
  offloadSeenImagesForCache?: boolean;
1799
1874
  /**
1800
1875
  * Transport failures one request absorbs before the failure is surfaced;
1801
- * defaults to 5. The route's retry policy is near-unbounded on purpose
1802
- * (1000 attempts, waits doubling to 15 minutes) because that shape is for
1803
- * the failures a provider asks to have retried; a transport failure is not
1876
+ * defaults to 5. The route's policy allows long waits for confirmed usage
1877
+ * windows, while ordinary transient errors have a separate three-retry
1878
+ * budget. A transport failure is not
1804
1879
  * one of those, and after ~15.5 s of grace the wait is pure stall, so it is
1805
1880
  * capped here (issue #39). 0 surfaces every transport failure immediately.
1806
1881
  */