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

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,115 @@ 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.2] - 2026-10-01
8
+
9
+ ### Added
10
+
11
+ - 支持 Go 套餐官方每模型月度额度;零值保留为零,缺失值不推算,额度不改变接口访问权限。
12
+
13
+ ### Changed
14
+
15
+ - **上游同步:command-code@1.72.1 → 1.73.0。** 已按 npm `latest` tarball(提取前做 sha512 校验)、`/provider/v1/models`、定价页内嵌 availability、CLI 注册表的 effort/ZDR/套餐表与渲染后 changelog 重新核对。**本次只有版本常量变化**:目录仍是同样 86 个模型,`KNOWN_EFFORTS`、`KNOWN_PLANS`、`KNOWN_IMAGE_MODELS`、`KNOWN_THINKING_MODELS`、`KNOWN_PEAK_PRICING`、`KNOWN_NON_ZDR_MODELS`(23 项)与 `KNOWN_SUBSCRIPTION_PLANS` 全部零差异,因此没有任何快照被改动。1.73.0 实际带来的是云沙箱 RPC 层——新增 `/alpha/sandbox/input`、`/alpha/sandbox/transcript` 两个 alpha 端点,以及 `--experimental --rpc` 下的一个 JSON-RPC dispatcher(`turn/start`、`turn/steer`、`turn/interrupt`、`modelAndEffort`),本插件从不调用。适配器真正使用的通道与 1.72.1 完全一致:`/alpha/generate` 调用点、`/provider/v1/*` 端点全集、以及所有请求头(`x-command-code-version`、`x-cmd-zdr` 等)均无增删。其前的 1.72.2 → 1.72.4 是 `IMP` 修复串;1.72.0 的用户可见新增是 `cmd mods enable|disable <name>` 与 `/rc`(从 Telegram/Discord 远程控制会话)。注意官方 changelog 页当时最新仍为 1.72.4——npm 先于网页发布,该版本自身条目尚未渲染。
16
+
17
+ - **`/alpha/generate` 补齐官方客户端的两个请求字段。** `permissionMode` 现在发送 `"standard"`(CLI 内部默认值是 `"default"`,但其 `toWirePermissionMode()` 会在上线前把缺省/默认归一化为 `"standard"`,且该值经真实服务探测确认为合法枚举成员),`x-taste-learning` 由 `"true"` 改为 `"false"`(取自一个独立第三方 Command Code 集成的观测值,未对真实服务做过效果验证)。两者都只是请求形状对齐:`permissionMode` 已在 issue #64 评审的"根因边界"一节用同一传输测过并排除为缓存回归的成因;`x-taste-learning` 的新值同样未经实测确认会改变任何响应。未改变运行时行为。
18
+
19
+ - **账号卡片与侧边栏统计的"当前使用"徽章,现在与实际请求路由共用同一套自愈判断。** `resolveKey()`(真实请求路径)对显式选中的账号有专门的复活探测:一次 429 留下的 `unknown` 标记,会在下一次真实请求时按 60 秒节流主动探测其真实窗口,发现没超限就立刻恢复。但展示路径此前直接调用裸 `selectActiveAccount()`——一个纯同步过滤函数,从不探测——而清除 `unknown` 标记的唯一途径就是 `resolveKey()` 内部的探测。结果是:只要用户没有实际发消息,一个早已可复活的标记会一直显示"已切换"。现在把两段探测逻辑抽成共享私有方法 `reviveExplicit()` / `probeAllMarked()`,并新增 `pool.activeAccount()` 供展示层复用(issue #51 的后续报告)。`activeAccount()` 与 `resolveKey()` 的刻意差异:不消耗 `tried` 预算、不区分具体模型、全部账号不可用时返回 `undefined` 而非抛错。反馈中另一半"选的是有额度账号却突然 400、会话中断"是否同一根因仍待真实零余额账号复现,不在本次修复范围。
20
+
21
+ ### Fixed
22
+
23
+ - 限制错误响应正文的时间与大小,避免错误请求永久挂起;登录回调后的验证继续受总期限与取消约束。
24
+ - 修复 OpenAI 独立尾部用量丢失,以及 Messages 部分用量覆盖输入/缓存统计和过早终止。
25
+ - 价格同步遇到未知套餐额度或交叉校验失败时停止生成与写入,避免删除完整旧快照中的额度。
26
+ - 秒制超时设置保留毫秒精度;超时建议只提供高于当前值且在页面上限内的期限。
27
+ - 保存期间的新草稿不再被成功回调清除;移除账户先原子提交关联配置,再清理凭据,清理失败可跨页面刷新重试。
28
+ - 冷生成使用同网关的磁盘目录计算模型输出上限,不额外联网;DSH 对等依赖改用精确版本以匹配单一宿主支持约定。
29
+ - 移除按单次请求提示词大小动态收紧 `max_tokens` 的客户端预判(`requestContextBudget()`):该机制会随对话变长把实际发送的输出预算越压越小,是长对话"输出被提前截断"的直接原因;现场 A/B 实测确认,去掉预判后真正超窗口的请求仍能被上游拒绝原文正确识别为超出上下文窗口并走既有压缩重试,不需要客户端预先猜测(issue #74,一并回退了 issue #73 为同一机制加的空响应改判分支)。
30
+ - 设置页 RPC 报错不再可能显示为 `[object Object]`:跨进程边界丢失 `Error` 原型链时,改为读取错误对象里的 `message` 字段(issue #74)。
31
+
32
+ - 网页显示终端覆盖后的有效模型选择;保存时一次更新白名单与覆盖,显示全部处理继承隐藏项,冲突保留草稿。终端保存显式选择,避免清除覆盖后重新继承相反值。
33
+ - 页面及用量面板辅助字号至少 12px;重试次数错误说明与 0~50 次限制一致。通用 HTTP 失败显示中文说明和诊断码,避免回显上游正文。
34
+ - 移除发布包中无对应文件的 `./src/*` 子路径;持续集成加入真实宿主加载、隔离安装和实际打包入口校验,打包检查不再额外运行准备脚本。
35
+
36
+ ## [0.12.1] - 2026-09-30
37
+
38
+ ### Added
39
+
40
+ - 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.
41
+
42
+ - Optional numeric request timing through `DSH_COMMANDCODE_TIMING`, including request phases, attempts, and first content without request text or credentials.
43
+
44
+ - **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.
45
+
46
+ 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.
47
+
48
+ 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`.
49
+
50
+ ### Changed
51
+
52
+ - **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.
53
+
54
+ - **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.
55
+
56
+ **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`.
57
+
58
+ **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.
59
+
60
+ **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.
61
+
62
+ **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).
63
+
64
+ ### Fixed
65
+
66
+ - **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.
67
+ - **`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".
68
+ - **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.
69
+
70
+ 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.
71
+
72
+
73
+ - 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.
74
+ - 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.
75
+ - **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.
76
+
77
+ - **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.
78
+
79
+ 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.
80
+
81
+ **The placements are copied from the official client, not invented.** Command Code ships
82
+ [`pi-commandcode-provider`](https://github.com/CommandCodeAI/pi-commandcode-provider), which calls this
83
+ same `/provider/v1/messages` endpoint through `@earendil-works/pi-ai` and hand-writes no marker at all:
84
+ that library's `anthropic-messages` transport marks precisely these three positions, and reads
85
+ `pricing.cache_read` / `pricing.cache_write` from the model list to price the result. Both artifacts were
86
+ checked against their registry-published sha512 digests (`pi-ai` 0.87.1, `command-code` 1.68.0, both
87
+ matching) rather than taken on trust, and `command-code` 1.68.0 separately shows the CLI marks only its own
88
+ system section and never touches this endpoint — which is why the CLI-route cache regressions in
89
+ [docs/issue-64-cache-review.md](../docs/issue-64-cache-review.md) say nothing about this one. **This could
90
+ not be confirmed against a live Command Code response** — the account had no remaining quota — so the
91
+ claim that the gateway honours the field rests on the official provider depending on it for its own cost
92
+ display, not on a measured `cache_read_input_tokens`. The downside is bounded either way: a gateway that
93
+ ignored the field would leave price and shape exactly as they are, and one that honoured it without a hit
94
+ would bill the first turn's write at $2.5/M against $2/M, with every later turn hitting inside the
95
+ 5-minute window.
96
+
97
+ - **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.
98
+
99
+ 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.
100
+
101
+ - **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).
102
+
103
+ 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.
104
+
105
+ - **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.
106
+
107
+ 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.
108
+
109
+ - **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.
110
+
111
+ - **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.
112
+
113
+ - **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.
114
+
115
+
7
116
  ## [0.12.0] - 2026-09-29
8
117
 
9
118
  ### 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-*` peers use the exact version `0.2.0-rc.2`, and `dsh.compatibility.dshReleases` records that single release. Adjacent prereleases and stable patch versions have not been verified. 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
 
@@ -301,3 +301,7 @@ MIT — see [LICENSE](./LICENSE). Portions ported from [pi-commandcode-provider]
301
301
  **Settings page** — the account list with live quota, model visibility, privacy switches and connection options:
302
302
 
303
303
  <img src="assets/screenshots/settings-page.png" alt="Command Code settings page with the account usage card" width="640">
304
+
305
+ ### Package entry points
306
+
307
+ The package exports the plugin root, `/client`, `/locale/*.json`, and `/package.json`. Source remains in the repository; the unpackaged `./src/*` export was removed. `npm run test:pack` checks declared entry points against the actual packed file list.
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-*` 对等依赖使用精确版本 `0.2.0-rc.2`,`dsh.compatibility.dshReleases` 也只记录这一个版本。相邻预发布版与稳定补丁版尚未验证。在更早的引擎上,设置页、消息封装或请求图片预算总有一处对不上——请改装支持你所用引擎的最后一个版本,而不是硬装这一个。
140
140
 
141
141
  ## 用量面板
142
142
 
@@ -292,3 +292,8 @@ MIT —— 见 [LICENSE](./LICENSE)。部分内容移植自 [pi-commandcode-prov
292
292
  **设置页** —— 带实时额度的账户列表、模型显示、隐私开关与连接参数:
293
293
 
294
294
  <img src="assets/screenshots/settings-page.png" alt="Command Code 设置页面(含账户用量卡片)" width="640">
295
+
296
+ ### 发布入口
297
+
298
+ 发布包提供插件根入口、`/client` 网页入口、`/locale/*.json` 语言包及 `/package.json` 元数据。
299
+ 源码只保存在仓库中;未发布的 `./src/*` 不再列入导出声明。`npm run test:pack` 根据实际打包文件核对入口。