@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 +80 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/lib/client.js +1 -1
- package/lib/index.d.ts +114 -39
- package/lib/index.js +1335 -191
- package/lib/index.js.map +1 -1
- package/package.json +40 -37
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
46
|
-
|
|
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.
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
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
|
|
353
|
-
*
|
|
354
|
-
* the
|
|
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
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* `
|
|
375
|
-
* and
|
|
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
|
-
/**
|
|
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
|
|
513
|
+
private cachedProtocol;
|
|
470
514
|
private rememberProtocol;
|
|
471
515
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
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
|
|
478
|
-
* Claude family
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
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
|
|
487
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
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
|
*/
|