@dan-ai-studio/dshopencodego 0.1.43 → 0.1.45

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.
Files changed (44) hide show
  1. package/README.en.md +48 -28
  2. package/README.md +47 -27
  3. package/lib/client.js +47 -1
  4. package/lib/index.js +288 -123
  5. package/lib/types/adapter.d.ts +26 -3
  6. package/lib/types/catalog/go-doc.d.ts +9 -5
  7. package/lib/types/catalog/index.d.ts +22 -7
  8. package/lib/types/catalog/service.d.ts +44 -1
  9. package/lib/types/config.d.ts +4 -3
  10. package/lib/types/conversion/replay.d.ts +30 -1
  11. package/lib/types/models.d.ts +14 -0
  12. package/lib/types/usage/meter.d.ts +89 -2
  13. package/package.json +20 -21
  14. package/lib/types/adapter.js +0 -384
  15. package/lib/types/catalog/constants.js +0 -18
  16. package/lib/types/catalog/contract.js +0 -147
  17. package/lib/types/catalog/gateway.js +0 -59
  18. package/lib/types/catalog/go-doc.js +0 -192
  19. package/lib/types/catalog/index.js +0 -377
  20. package/lib/types/catalog/json-response.js +0 -73
  21. package/lib/types/catalog/metadata.js +0 -278
  22. package/lib/types/catalog/protocol.js +0 -87
  23. package/lib/types/catalog/reading.js +0 -73
  24. package/lib/types/catalog/service.js +0 -45
  25. package/lib/types/config.js +0 -91
  26. package/lib/types/conversion/context.js +0 -212
  27. package/lib/types/conversion/index.js +0 -7
  28. package/lib/types/conversion/replay.js +0 -242
  29. package/lib/types/conversion/stream.js +0 -221
  30. package/lib/types/go-limits.js +0 -91
  31. package/lib/types/index.js +0 -198
  32. package/lib/types/models.js +0 -86
  33. package/lib/types/remotes.js +0 -29
  34. package/lib/types/session-header.js +0 -49
  35. package/lib/types/update/check.js +0 -138
  36. package/lib/types/update/contract.js +0 -64
  37. package/lib/types/update/index.js +0 -10
  38. package/lib/types/update/service.js +0 -39
  39. package/lib/types/update/version.js +0 -16
  40. package/lib/types/usage/contract.js +0 -106
  41. package/lib/types/usage/index.js +0 -8
  42. package/lib/types/usage/meter.js +0 -65
  43. package/lib/types/usage/service.js +0 -74
  44. package/lib/types/usage/windows.js +0 -84
package/README.en.md CHANGED
@@ -27,11 +27,14 @@ Prerequisites: a DSH `0.2.0` release (`0.2.0-rc.1` or later; see "Compatibility"
27
27
  dsh plugin --profile web add @dan-ai-studio/dshopencodego@<version>
28
28
  ```
29
29
 
30
- Confirm the composition picked it up:
30
+ **After installing, confirm it actually loaded** — `--dump-config` cannot prove that:
31
31
 
32
- ```sh
33
- dsh --profile web --dump-config | grep dshopencodego
34
- ```
32
+ 1. The startup output should carry the plugin's own registration line, `dshopencodego: route "opencode-go" registered as OpenCode Go` (with `enabled: false` it is `dshopencodego: disabled by configuration; …`). No line means no load — when the compatibility gate refuses the plugin, the same output stream carries `Plugin … is incompatible with dsh …`.
33
+ 2. A profile without HMR reports `restart-required` (`restartReason: no-hmr`) after the install: the plugin takes effect only after you **restart DSH**.
34
+ 3. Where the logs are: a failed startup writes the full diagnostic to `$DSH_HOME/logs/startup-<timestamp>-<uuid>.log` and prints that path; a failed `dsh plugin` run prints `dsh: plugin command failed; diagnostics: <path>` (a file under the profile's `.plugin-manager/logs/operation-*/pnpm.log`).
35
+ 4. Whether the session header really goes out: `node tools/verify/live-check.mjs --check-only` is a **zero-cost** local pre-flight that prints the `x-opencode-session` and `x-session-affinity` this run would carry; dropping `--check-only` and adding `--allow-live` sends **one** minimal request instead (it costs money; rules in `tools/verify/README.md`), after which the script asserts and prints `OK x-opencode-session: <session id>`.
36
+
37
+ > `dsh --profile web --dump-config | grep dshopencodego` is **not** load evidence: a plugin row the gate denied still appears in the dump (`packages/boot/app-boot/README.md`). It only proves the row was written into the composition.
35
38
 
36
39
  ### Option 2: GitHub Release
37
40
 
@@ -83,27 +86,35 @@ Configuration lives in the profile's `cordis.patch.yml`:
83
86
  config:
84
87
  enabled: true # false withdraws the route only
85
88
  apiKeyEnv: OPENCODE_GO_API_KEY # default
86
- baseURL: https://opencode.ai/inference/go/v1 # default (OpenCode Go endpoint, 37 subscription models)
87
- # or https://opencode.ai/inference/v1 (Console main endpoint, 86 pay-as-you-go/free models)
88
- refreshMinutes: 60 # live catalog TTL
89
+ baseURL: https://opencode.ai/inference/go/v1 # default (OpenCode Go endpoint)
90
+ # http(s) only, and no query or fragment; writing ?x=1 is refused when the profile is saved
91
+ # or https://opencode.ai/inference/v1 (Console main endpoint, pay-as-you-go/free models)
92
+ refreshMinutes: 60 # live catalog TTL, 1 ~ 10080 (7 days)
89
93
  streamIdleTimeoutMs: 300000 # stream idle timeout: longest silence between events (ms)
90
94
  maxRequestImageBytes: 6291456 # whole-request image payload cap (after base64, bytes)
91
95
  requestImagePixelBudget: 1440000 # total-pixel budget for one image
92
96
  requestImageMaxBytes: 400000 # encoded-byte target for one image (before base64)
93
- modelVisibility: # per-model switches (deprecated default off)
97
+ modelVisibility: # per-model switches; unlisted models follow the default rule (see "The model list in the settings page")
94
98
  glm-5: true
95
99
  modelLimits: # per-model capacity overrides
96
100
  kimi-k3:
97
101
  contextWindow: 262144
102
+ maxTokens: 32768 # optional: this model's output cap
103
+ some-legacy-model: null # a whole-entry null: both fields explicitly fall back to the catalog values
98
104
  modelProtocols: # last resort: per-model protocol override
99
- some-new-model: openai-responses
105
+ some-new-model: openai-responses # the only legal values are anthropic-messages / openai-completions / openai-responses
100
106
  retryPolicy: # optional: this route's retry policy, executed by dsh-llm-retry
101
- mode: normal
102
- maxRetries: 2
107
+ mode: normal # or always (retry every failure; maxRetries is ignored)
108
+ maxRetries: 2 # default 5
109
+ retryableCodes: [RATE_LIMIT, TIMEOUT] # optional: retry only these codes (defaults RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT / EMPTY_RESPONSE)
103
110
  backoff:
104
- initialDelayMs: 500
111
+ initialDelayMs: 500 # default 500
112
+ maxDelayMs: 10000 # default 10000, ceiling 2147483647
113
+ jitterRatio: 0.1 # default 0.1, range 0 ~ 1
105
114
  ```
106
115
 
116
+ **Validated on save**: those constraints are checked when the profile is **written**, not at the first request — `baseURL` must be http(s) with no query or fragment; `refreshMinutes` ranges from 1 to 10080; every `modelLimits` field may be `null`, meaning "select the catalog value explicitly" (`modelLimits.<id>: null` sends both fields back); `modelProtocols` accepts only those three values, and a wrong one makes that model appear as *configuration missing* (switch off, not selectable) rather than falling back to a guess; `retryPolicy` is a union (`normal` reads `maxRetries`/`retryableCodes`/`backoff`, `always` reads only `backoff`) and unknown keys are rejected.
117
+
107
118
  The API key comes from the Harness credential store (reference name `OPENCODE_GO_API_KEY`), or from `export OPENCODE_GO_API_KEY=...`.
108
119
 
109
120
  **Stream timeout**: `streamIdleTimeoutMs` bounds the **longest silence between two stream events**, not the total request duration — a long-thinking model that keeps producing events is never cut off. Default 5 minutes; the ceiling is `2147483647` (about 24.8 days). Exceeding it raises `TIMEOUT`, which is retryable.
@@ -154,45 +165,52 @@ The "OpenCode Go" section shows the live gateway catalog (the count follows the
154
165
  - **Input modalities**: a separate line lists the input modes models.dev declares (text / image / audio / video / PDF). These are the **model's own metadata**, not what the Harness can forward — the Harness sends text and images natively; audio/video/PDF travel as attachments and tool reads. The line is absent when the document declares none.
155
166
  - filtering (by name/id, enabled-only, show-deprecated — **deprecated hidden by default**), sorting (newest first, monthly requests, input price, context, name, enabled first); the list is the only view, one model per row.
156
167
 
157
- **Default switches**: with no explicit `modelVisibility` entries at all, the **top five models by published monthly request estimate** are enabled (deprecated, unconfigurable, and training-"contributor" models never qualify). The first explicit entry switches the whole list to explicit values. The Host picker and the settings page compute this from one rule, so they never disagree.
168
+ **Default switches**: `modelVisibility` is **per model** — an id listed with `true`/`false` follows that explicit value, and **ids you do not list still follow the default rule**: the **top five models by published monthly request estimate** are enabled (deprecated, unconfigurable, and training-"contributor" models never qualify) and the rest stay off. That top-five set is recomputed unconditionally on every catalog refresh, whether or not any explicit entry exists. The Host picker and the settings page compute this from one rule, so they never disagree. **The consequence**: once you write any explicit entry, a model the gateway adds later is **not** automatically visible unless it lands in the top five — add `id: true` for it explicitly.
158
169
 
159
170
  ## Usage
160
171
 
161
172
  - **Quota windows**: `GET /usage` reports 5-hour / weekly / monthly percentages with reset times and rate-limit flags; a failed read keeps the last reading and marks it stale, and an account or endpoint change invalidates it.
162
173
  - **Actual spend**: the in-conversation button reports calls and tokens (input/output/cache-read) this process spent since it started, from the usage the provider returns on each call. The gateway publishes no per-model tokens, so the numbers are labelled for what they are instead of faking a per-model quota.
174
+ - **Estimated cost**: models.dev per-million-token rates multiplied by the tokens the provider returned — an **estimate, not a bill**. On the subscription endpoint (Go) it is indicative only; it is only reconcilable on the metered Console endpoint. The rate is captured per call, so a catalog refresh never reprices tokens already spent; when the catalog publishes no separate cache rate, cached tokens are estimated at the **input** rate (an upper bound). **A model with no published rate shows "unpriced" with its name — never `0`, which would read as free.** Very small amounts render as `<$0.0001`.
175
+ - **Reasoning tokens**: the thinking tokens a provider reports are a **subset of `outputTokens`**, so they are shown on their own line but **excluded from the total** (adding them would count the same tokens twice). A model that reports no breakdown hides the line — that means "no data", not `0`.
163
176
 
164
177
  ## Compatibility
165
178
 
166
- **Supported**: DSH `0.2.0` releases (`0.2.0-rc.1` and later) and the `0.2.1` line. Every `@deepseek-ai/dsh-*` peer dependency declares `>=0.2.0-rc.1 <0.3.0`.
179
+ **Supported**: DSH `0.2.0` releases (`0.2.0-rc.1` and later) and the `0.2.1` line. `engines.dsh` and every `@deepseek-ai/dsh-*` peer dependency declare `>=0.2.0-rc.1 <0.3.0-0`.
167
180
 
168
- **`0.2.0` and earlier are no longer supported**: support for the `0.1.7` line and older was removed in v0.1.36 — the runtime seam across that range differs from the one this plugin is built against, and continuing to declare it would only let the gate admit an untested combination. (`0.1.8` and `0.1.9` were never published.)
181
+ **Anything before `0.2.0` is no longer supported**: support for the `0.1.7` line and older was removed in v0.1.36 — the runtime seam across that range differs from the one this plugin is built against, and continuing to declare it would only let the gate admit an untested combination. (`0.1.8` and `0.1.9` were never published.)
169
182
 
170
- **The mechanism (not a hard pin)**: before mounting a plugin, DSH reads its `package.json` `peerDependencies` and semver-checks each against the **running DSH version** (prereleases participate):
183
+ **The mechanism (not a hard pin)**: before mounting a plugin, DSH reads its `package.json` declarations and semver-checks the **running DSH version** against them (prereleases participate, i.e. `includePrerelease`). There are two levels, first match wins:
171
184
 
172
- - all satisfied → the plugin loads;
173
- - any mismatch → **that plugin row is disabled with a printed reason** (`Plugin … is incompatible with dsh …`), leaving other plugins untouched;
185
+ 1. **`engines.dsh` takes precedence and decides alone**: when a plugin declares it, only that range is consulted and `peerDependencies` plays no part at all (DSH source `packages/boot/app-boot/src/plugin-compatibility.ts` returns as soon as it finds `engines.dsh`).
186
+ 2. Only when a plugin declares **no** `engines.dsh` are the `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer ranges checked, one by one.
187
+
188
+ The verdict:
189
+
190
+ - satisfied → the plugin loads;
191
+ - not satisfied → **that plugin row is disabled with a printed reason** (`Plugin … is incompatible with dsh …`), leaving other plugins untouched;
174
192
  - to take the risk anyway, grant an **exact-version exemption**: `dsh plugin --profile <profile> allow-version <pkg>@<version> --dsh-version <exact> --accept-risk` (applies only to that package and that exact runtime version); `dsh plugin --profile <profile> version-exemptions` lists what a profile holds.
175
193
 
176
- The `engines.dsh` field is informational for readers and package managers; **DSH's compatibility gate reads only `peerDependencies`**. `@deepseek-ai/cordis` is declared separately as `4.0.4 || 4.0.5-alpha.1 || 4.0.5-alpha.2 || 4.0.5` — the runtimes used by DSH `0.2.0-rc.1`/`rc.2`, `0.2.1-alpha.1`/`alpha.2`, `0.2.1-alpha.3`–`alpha.5`, and `0.2.1-rc.2` respectively (`4.0.2`/`4.0.3` were dropped along with pre-`0.2.0` support).
194
+ **This plugin declares `engines.dsh`, so that is the range the gate enforces**; the peer ranges are a declaration for readers and package managers, and only take over the verdict if `engines.dsh` is removed. The upper bound is written `<0.3.0-0` rather than `<0.3.0` precisely because the check includes prereleases: `<0.3.0` would judge `0.3.0-rc.1` and its siblings **compatible** and let them load, while `<0.3.0-0` closes the whole `0.3.0` line. `@deepseek-ai/cordis` is declared separately as `4.0.4 || 4.0.5-alpha.1 || 4.0.5-alpha.2 || 4.0.5` — the runtimes used by DSH `0.2.0-rc.1`/`rc.2`, `0.2.1-alpha.1`/`alpha.2`, `0.2.1-alpha.3`–`alpha.5`, and `0.2.1-rc.2` respectively (`4.0.2`/`4.0.3` were dropped along with pre-`0.2.0` support).
177
195
 
178
- **Outside the declared range**: everything before `0.2.0-rc.1`, and `0.3.0+`, are refused. If the seam is compatible, widen the peer range in your own build (never in DSH core), or wait for a plugin release.
196
+ **Outside the declared range**: everything before `0.2.0-rc.1`, and the whole `0.3.0` line (prereleases included), are refused. If the seam is compatible, widen or drop this plugin's **`engines.dsh`** in your own build — widening the peer range does nothing, since it takes no part in the verdict — or wait for a plugin release.
179
197
 
180
198
  ### Version-binding notes
181
199
 
182
200
  | Binding layer | Declared | Enforced by |
183
201
  | --- | --- | --- |
184
- | `peerDependencies` (17 `@deepseek-ai/dsh-*` packages) | all `>=0.2.0-rc.1 <0.3.0` | **DSH's gate, at plugin load time** |
185
- | `@deepseek-ai/cordis` | `4.0.4 \|\| 4.0.5-alpha.1 \|\| 4.0.5-alpha.2 \|\| 4.0.5` | the same gate |
186
- | `engines.dsh` | `>=0.2.0-rc.1 <0.3.0` | informational only; DSH never reads it |
202
+ | `engines.dsh` | `>=0.2.0-rc.1 <0.3.0-0` | **DSH's gate at plugin load time; declaring it skips the peer verdict entirely** |
203
+ | `peerDependencies` (17 `@deepseek-ai/dsh-*` packages) | all `>=0.2.0-rc.1 <0.3.0-0` | takes over only when `engines.dsh` is absent; this plugin declares it, so the peers take no part today |
204
+ | `@deepseek-ai/cordis` | `4.0.4 \|\| 4.0.5-alpha.1 \|\| 4.0.5-alpha.2 \|\| 4.0.5` | the same peer channel |
187
205
  | `engines.node` | `^22.19.0 \|\| >=24.0.0` | the package manager |
188
206
  | bundled `@earendil-works/pi-ai` | `~1.3.0` (within 1.3.x), **compiled into `lib/`** (devDependency) | an independent coupling: a change in pi-ai's request construction changes this plugin's wire behaviour, and its opencode-go builtin catalog is this plugin's fallback data. Bundling leaves the plugin with **no runtime dependencies** for an installer to resolve |
189
207
  | bundled `@deepseek-ai/schemastery` | `~3.18.5-alpha.2`, **compiled into `lib/`** (devDependency) | a build-time dependency |
190
208
 
191
209
  Things to keep in mind when maintaining this:
192
210
 
193
- - **Required versus optional peers**: six of the eighteen are marked `optional` (`dsh-api-remotes`, `dsh-client-locale`, `dsh-client-store`, `dsh-client-ui-model-selection`, `dsh-client-ui-settings`, `dsh-client-ui-slots`). The twelve that actually block loading are `cordis`, `dsh-attachment`, `dsh-brand`, `dsh-client-ui-conversation`, `dsh-client-ui-renderer`, `dsh-credentials`, `dsh-fs`, `dsh-launch-environment`, `dsh-llm`, `dsh-timeout`, `dsh-typert-protocol`, and `dsh-typert-registry`. `dsh-llm` is the heaviest coupling — more than twenty imports across the source.
194
- - **Declared range ≠ verified range**: the declaration is `>=0.2.0-rc.1 <0.3.0`. What was actually verified here, in order, is `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), `0.2.0-rc.2` (the same plus a `prepareCall` generation freeze), `0.2.1-alpha.3` (the first target release), `0.2.1-alpha.4` (T-24), `0.2.1-alpha.5` (T-26), and `0.2.1-rc.2` (T-31, where the runtime moved to cordis `4.0.5`; the upstream contract layer had zero source changes). Other versions inside the range were not verified one by one.
195
- - **The upper bound is a tracking line**: `<0.3.0` means that the moment DSH ships `0.3.0`, this plugin must ship a new release in the same window or every user loses the plugin. After changing a range, `npm test` is the verification (local mock gateway, no network, no tokens) — ranges follow what the interfaces declare, and are never "measured" with live probes.
211
+ - **Required versus optional peers**: six of the eighteen are marked `optional` (`dsh-api-remotes`, `dsh-client-locale`, `dsh-client-store`, `dsh-client-ui-model-selection`, `dsh-client-ui-settings`, `dsh-client-ui-slots`). The other twelve are required at runtime: `cordis`, `dsh-attachment`, `dsh-brand`, `dsh-client-ui-conversation`, `dsh-client-ui-renderer`, `dsh-credentials`, `dsh-fs`, `dsh-launch-environment`, `dsh-llm`, `dsh-timeout`, `dsh-typert-protocol`, and `dsh-typert-registry`. None of the eighteen takes part in the load verdict today (see above: `engines.dsh` decides alone); their value is declaring the dependency for the package manager and for readers. `dsh-llm` is the heaviest coupling — more than twenty imports across the source.
212
+ - **Declared range ≠ verified range**: the declaration is `>=0.2.0-rc.1 <0.3.0-0`. What was actually verified here, in order, is `0.2.0-rc.1` (the dev-dependency baseline: gate verdict + typecheck + full suite), `0.2.0-rc.2` (the same plus a `prepareCall` generation freeze), `0.2.1-alpha.3` (the first target release), `0.2.1-alpha.4` (T-24), `0.2.1-alpha.5` (T-26), and `0.2.1-rc.2` (T-31, where the runtime moved to cordis `4.0.5`; the upstream contract layer had zero source changes). Other versions inside the range were not verified one by one.
213
+ - **The upper bound is a tracking line**: `<0.3.0-0` means that the moment DSH ships `0.3.0-0` (the first prerelease on the `0.3.0` line), this plugin must ship a new release in the same window or every user loses the plugin. `tests/peer-coverage.spec.ts` asserts both ends — the lower bound accepts the installed version, the upper bound rejects `0.3.0-0` — so a drift here fails locally instead of failing on a user's machine. After changing a range, `npm test` is the verification (local mock gateway, no network, no tokens) — ranges follow what the interfaces declare, and are never "measured" with live probes.
196
214
  - **Peer coverage is now guarded both ways**: `dsh-typert-registry`, `dsh-client-ui-conversation`, and `dsh-client-ui-renderer` were imported by the source but absent from `peerDependencies`, invisible to the gate; they are declared now, and `tests/peer-coverage.spec.ts` asserts both directions — anything the source imports must be declared, and anything declared but unreached must be removed (`dsh-settings` went that way: the settings form is actually provided by `dsh-client-ui-settings`).
197
215
  - **Exemptions are per profile**: only a profile that ran `dsh plugin allow-version` has one, and no record means the plugin must stay inside the declared range. `dsh plugin version-exemptions` lists a profile's exemptions.
198
216
 
@@ -217,11 +235,13 @@ Things to keep in mind when maintaining this:
217
235
  npm ci
218
236
  npm run lint # oxlint: static checks (the release workflow runs this too, and fails on it)
219
237
  npm run typecheck # both halves: host and client
238
+ npm run build # build first: emits lib/index.js and lib/client.js
220
239
  npm test # contract tests against a real local HTTP mock gateway
221
- npm run build # emits lib/index.js and lib/client.js
222
240
  ```
223
241
 
224
- The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest). That is **33 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
242
+ **Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
243
+
244
+ The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest). That is **43 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
225
245
 
226
246
  ## Releasing (maintainers)
227
247
 
package/README.md CHANGED
@@ -27,11 +27,14 @@
27
27
  dsh plugin --profile web add @dan-ai-studio/dshopencodego@<版本>
28
28
  ```
29
29
 
30
- 安装后确认组合里出现该插件:
30
+ **安装后确认真实加载**——`--dump-config` 证明不了这一点:
31
31
 
32
- ```sh
33
- dsh --profile web --dump-config | grep dshopencodego
34
- ```
32
+ 1. 启动输出里应出现插件自己的注册日志 `dshopencodego: route "opencode-go" registered as OpenCode Go`(`enabled: false` 时是 `dshopencodego: disabled by configuration; …`)。没有它就没加载——被版本门禁拒绝时,同一条输出流里会打印 `Plugin … is incompatible with dsh …`。
33
+ 2. 没有 HMR 的 profile 安装后会回报 `restart-required`(`restartReason: no-hmr`):**重启 DSH** 后插件才生效。
34
+ 3. 日志位置:启动失败会把完整诊断写到 `$DSH_HOME/logs/startup-<时间戳>-<uuid>.log` 并打印该路径;`dsh plugin` 失败会打印 `dsh: plugin command failed; diagnostics: <路径>`(profile 下的 `.plugin-manager/logs/operation-*/pnpm.log`)。
35
+ 4. 会话头是否真的发出去了:`node tools/verify/live-check.mjs --check-only` 是**零成本**本地预检,会打印本次将会携带的 `x-opencode-session` 与 `x-session-affinity`;去掉 `--check-only`、加 `--allow-live` 才会真发**一次**最小请求(花钱,规则见 `tools/verify/README.md`),脚本随后断言并打印 `OK x-opencode-session: <会话 id>`。
36
+
37
+ > `dsh --profile web --dump-config | grep dshopencodego` **不构成加载证据**:被门禁拒绝的插件行同样会出现在 dump 里(`packages/boot/app-boot/README.md`)。它只能证明"这一行被写进了组合"。
35
38
 
36
39
  ### 方式二:GitHub Release
37
40
 
@@ -83,27 +86,35 @@ DSH 桌面端是 Electron 壳 + 完整的 dsh Web 应用,本插件对它同样
83
86
  config:
84
87
  enabled: true # false 仅撤下路由,插件保持挂载
85
88
  apiKeyEnv: OPENCODE_GO_API_KEY # 默认值
86
- baseURL: https://opencode.ai/inference/go/v1 # 默认值(OpenCode Go 端点,37 个订阅模型)
87
- # 也可指定为 https://opencode.ai/inference/v1(Console 主端点,包含 86 个按量/免费模型)
88
- refreshMinutes: 60 # 目录缓存时长
89
+ baseURL: https://opencode.ai/inference/go/v1 # 默认值(OpenCode Go 端点)
90
+ # 只能是不带 query / fragment 的 http(s) 地址;写 ?x=1 会在保存配置时被拒
91
+ # 也可指定为 https://opencode.ai/inference/v1(Console 主端点,含按量/免费模型)
92
+ refreshMinutes: 60 # 目录缓存时长,1 ~ 10080(7 天)
89
93
  streamIdleTimeoutMs: 300000 # 流空闲超时:两次流事件之间的最长静默(毫秒)
90
94
  maxRequestImageBytes: 6291456 # 整个请求的图片载荷上限(base64 之后,字节)
91
95
  requestImagePixelBudget: 1440000 # 单张图的总像素预算
92
96
  requestImageMaxBytes: 400000 # 单张图编码前的字节目标(base64 之前)
93
- modelVisibility: # 逐模型开关(弃用模型默认关闭)
97
+ modelVisibility: # 逐模型开关;未写到的模型走默认规则(见「设置页里的模型列表」)
94
98
  glm-5: true
95
99
  modelLimits: # 逐模型容量覆盖
96
100
  kimi-k3:
97
101
  contextWindow: 262144
102
+ maxTokens: 32768 # 可选:该模型的输出上限
103
+ some-legacy-model: null # 整条 null:该模型两个字段都显式回退到目录值
98
104
  modelProtocols: # 最后兜底:逐模型协议覆盖
99
- some-new-model: openai-responses
105
+ some-new-model: openai-responses # 合法值只有 anthropic-messages / openai-completions / openai-responses
100
106
  retryPolicy: # 可选:本路由的重试策略(由 dsh-llm-retry 执行)
101
- mode: normal
102
- maxRetries: 2
107
+ mode: normal # 也可用 always(对所有失败重试,maxRetries 被忽略)
108
+ maxRetries: 2 # 默认 5
109
+ retryableCodes: [RATE_LIMIT, TIMEOUT] # 可选:只对这些错误码重试(默认 RATE_LIMIT / SERVER / TIMEOUT / TRANSPORT / EMPTY_RESPONSE)
103
110
  backoff:
104
- initialDelayMs: 500
111
+ initialDelayMs: 500 # 默认 500
112
+ maxDelayMs: 10000 # 默认 10000,上限 2147483647
113
+ jitterRatio: 0.1 # 默认 0.1,取值 0 ~ 1
105
114
  ```
106
115
 
116
+ **保存即校验**:上面这些约束在**写入 profile 时**就检查,不会拖到第一次请求——`baseURL` 必须是 http(s) 且不带 query/fragment;`refreshMinutes` 取值 1 ~ 10080;`modelLimits` 的每个字段都可以写 `null`,语义是"显式选回目录值"(`modelLimits.<模型>: null` 表示两个字段一起回退);`modelProtocols` 只接受那三个合法值,写错会让该模型以"配置缺失"出现(开关关闭、不可选),而不是回退到猜测;`retryPolicy` 是联合类型(`normal` 读 `maxRetries`/`retryableCodes`/`backoff`,`always` 只读 `backoff`),未知字段会被拒绝。
117
+
107
118
  API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),也可以直接 `export OPENCODE_GO_API_KEY=...`。
108
119
 
109
120
  **流超时**:`streamIdleTimeoutMs` 约束的是**两次流事件之间的最长静默**,不是请求总时长——长思考模型只要持续产出事件就不会被误判。默认 5 分钟,上限为 `2147483647`(约 24.8 天)。超时抛出 `TIMEOUT`(可重试类)。
@@ -154,45 +165,52 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
154
165
  - **输入模态**:单独一行列出 models.dev 声明的输入方式(文本 / 图像 / 音频 / 视频 / PDF)。这是**模型自身的元数据**,不等于 DSH 能直传:DSH 的模型输入只原生支持文本与图像,音视频/PDF 走附件与工具读取路径;文档未声明时该行不显示。
155
166
  - 筛选(按名称/ID、仅已启用、显示已弃用——**默认隐藏已弃用**)、排序(新发布优先 / 每月预估次数 / 输入单价 / 上下文 / 名称 / 已启用优先);列表是唯一视图,逐模型纵向排列。
156
167
 
157
- **默认开关规则**:没人显式配置过 `modelVisibility` 时,默认启用**每月预估次数最高的 5 个**(跳过已弃用、无法配置、以及"贡献者版"这类以数据换折扣的模型);**一旦有任一显式条目,全部按显式值走**。同一规则同时作用于模型选择器与设置页,两处不会出现不同答案。
168
+ **默认开关规则**:`modelVisibility` 是**逐模型**的——写了 `id: true`/`id: false` 的模型按显式值走,**没写到的模型仍走默认规则**:默认启用**每月预估次数最高的 5 个**(跳过已弃用、无法配置、以及"贡献者版"这类以数据换折扣的模型),其余关闭。这份 top5 名单每轮目录刷新都会无条件重算,与是否写过显式条目无关。同一规则同时作用于模型选择器与设置页,两处不会出现不同答案。**后果**:一旦写过显式配置,网关新增的模型若不在 top5 内,下次刷新**不会自动可见**,要显式加一条 `id: true`。
158
169
 
159
170
  ## 用量
160
171
 
161
172
  - **额度窗口**:`GET /usage` 返回 5 小时 / 本周 / 本月三个百分比,含重置时间与限流状态;失败时保留上次读数并标注陈旧,账号或端点变化时不保留。
162
173
  - **实际消耗**:会话内按钮显示本进程自启动以来的调用次数与 token(输入/输出/缓存读),数据来自 provider 每次调用回传的 usage。网关不公布分模型 token,因此这里如实标注口径,不伪造分模型配额。
174
+ - **估算花费**:按 models.dev 的每百万 token 单价乘 provider 回传的 token 估算,**不是账单**——订阅端点(Go)下仅供参考,只有按量端点(Console)才有对账意义。单价随每次调用一起记录,目录刷新不会用新价重算已经花掉的 token;目录未单独给出缓存单价时,缓存部分**按输入价估算**(上界)。**没有单价的模型显示「未定价」并列出模型名,绝不以 0 冒充免费**;极小金额显示为 `<$0.0001`。
175
+ - **推理 token**:provider 报告的思考 token 是 `outputTokens` 的**子集**,因此单独显示但**不计入总量**(否则同一批 token 会被算两次)。未上报该项的模型不显示该行——那是「没有数据」,不是 0。
163
176
 
164
177
  ## 兼容性
165
178
 
166
- **支持**:DSH `0.2.0` 系列(`0.2.0-rc.1` 及之后的正式版)与 `0.2.1` 系列。插件在每个 `@deepseek-ai/dsh-*` 的 `peerDependencies` 上声明 `>=0.2.0-rc.1 <0.3.0`。
179
+ **支持**:DSH `0.2.0` 系列(`0.2.0-rc.1` 及之后的正式版)与 `0.2.1` 系列。插件在 `engines.dsh` 与每个 `@deepseek-ai/dsh-*` 的 `peerDependencies` 上声明 `>=0.2.0-rc.1 <0.3.0-0`。
167
180
 
168
181
  **不再支持 `0.2.0` 之前**:`0.1.7` 系列及更早的版本自 v0.1.36 起被移除支持——那段范围内的运行时接口与本插件当前依赖的接口已经不同,继续声明只会让门禁放行一个未经验证的组合。`0.1.8`、`0.1.9` 从未发布。
169
182
 
170
- **机制(不是写死)**:DSH 在挂载插件前会读取插件 `package.json` 的 `peerDependencies`,对**运行时的 DSH 版本**逐个做 semver 判定(含预发布):
183
+ **机制(不是写死)**:DSH 在挂载插件前会读取插件 `package.json` 的声明,对**运行时的 DSH 版本**做 semver 判定(含预发布,即 `includePrerelease`)。判定分两级,先命中先赢:
171
184
 
172
- - 全部满足 → 正常加载;
173
- - 任一不满足 → **该插件行被禁用并打印原因**(`Plugin … is incompatible with dsh …`),其他插件不受影响;
185
+ 1. **`engines.dsh` 优先且独占**:插件只要声明了它,就**只**由它决定,`peerDependencies` 完全不参与判定(DSH 源码 `packages/boot/app-boot/src/plugin-compatibility.ts`:命中 `engines.dsh` 即返回)。
186
+ 2. 插件**没有**声明 `engines.dsh` 时,才逐个检查 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 的 peer 范围。
187
+
188
+ 判定结果:
189
+
190
+ - 满足 → 正常加载;
191
+ - 不满足 → **该插件行被禁用并打印原因**(`Plugin … is incompatible with dsh …`),其他插件不受影响;
174
192
  - 需要冒险时可用**精确版本豁免**:`dsh plugin --profile <profile> allow-version <包>@<版本> --dsh-version <精确版本> --accept-risk`(只对该包与该精确版本生效);`dsh plugin --profile <profile> version-exemptions` 可列出当前 profile 的豁免。
175
193
 
176
- `package.json` 里的 `engines.dsh` 是本插件为读者/包管理器写的信息字段;**DSH 的兼容门禁只读 `peerDependencies`**,请以它为准。`@deepseek-ai/cordis` 单独声明为 `4.0.4 || 4.0.5-alpha.1 || 4.0.5-alpha.2 || 4.0.5`——依次是 DSH `0.2.0-rc.1`/`rc.2`、`0.2.1-alpha.1`/`alpha.2`、`0.2.1-alpha.3`–`alpha.5`、以及 `0.2.1-rc.2` 所用的运行时(`4.0.2`/`4.0.3` 随 `0.2.0` 之前的支持一并移除)。
194
+ **本插件声明了 `engines.dsh`,所以生效的门禁就是它**;peer 范围是给读者与包管理器的声明,只在 `engines.dsh` 被删除之后才会接管判定。上界写成 `<0.3.0-0` 而不是 `<0.3.0`,正是因为判定含预发布:`<0.3.0` 会把 `0.3.0-rc.1` 这类**预发布版判为兼容**并放行,`<0.3.0-0` 才真正封住整条 `0.3.0` 线。`@deepseek-ai/cordis` 单独声明为 `4.0.4 || 4.0.5-alpha.1 || 4.0.5-alpha.2 || 4.0.5`——依次是 DSH `0.2.0-rc.1`/`rc.2`、`0.2.1-alpha.1`/`alpha.2`、`0.2.1-alpha.3`–`alpha.5`、以及 `0.2.1-rc.2` 所用的运行时(`4.0.2`/`4.0.3` 随 `0.2.0` 之前的支持一并移除)。
177
195
 
178
- **声明范围之外**:`0.2.0-rc.1` 之前的所有版本、以及 `0.3.0+` 会被拒绝加载。如果 DSH 侧接口兼容,可自行放宽该插件的 peer 范围并重新构建(不改 DSH 核心);否则请等插件跟进发版。
196
+ **声明范围之外**:`0.2.0-rc.1` 之前的所有版本、以及整条 `0.3.0` 线(含预发布)都会被拒绝加载。如果 DSH 侧接口兼容,可自行放宽或删除该插件的 **`engines.dsh`** 后重新构建(改 peer 范围是无效的——它不参与判定);否则请等插件跟进发版。
179
197
 
180
198
  ### 版本绑定的注意事项
181
199
 
182
200
  | 绑定层 | 声明 | 谁在把关 |
183
201
  | --- | --- | --- |
184
- | `peerDependencies`(17 个 `@deepseek-ai/dsh-*`) | 均为 `>=0.2.0-rc.1 <0.3.0` | **DSH 加载时的真门禁** |
185
- | `@deepseek-ai/cordis` | `4.0.4 \|\| 4.0.5-alpha.1 \|\| 4.0.5-alpha.2 \|\| 4.0.5` | 同一门禁 |
186
- | `engines.dsh` | `>=0.2.0-rc.1 <0.3.0` | 仅信息字段,DSH 不读 |
202
+ | `engines.dsh` | `>=0.2.0-rc.1 <0.3.0-0` | **DSH 加载时的真门禁;声明了它,peer 判定被整体跳过** |
203
+ | `peerDependencies`(17 个 `@deepseek-ai/dsh-*`) | 均为 `>=0.2.0-rc.1 <0.3.0-0` | 仅当 `engines.dsh` 缺席时才接管;本插件声明了它,故当前不参与判定 |
204
+ | `@deepseek-ai/cordis` | `4.0.4 \|\| 4.0.5-alpha.1 \|\| 4.0.5-alpha.2 \|\| 4.0.5` | 同上(peer 通道) |
187
205
  | `engines.node` | `^22.19.0 \|\| >=24.0.0` | 包管理器 |
188
206
  | 已打包依赖 `@earendil-works/pi-ai` | `~1.3.0`(1.3.x 内),**打进产物**(devDependency) | 独立耦合:pi-ai 改请求构造即影响本插件的线上行为;其 opencode-go 内置目录同时是本插件的打底数据。打包后本插件**没有任何运行时依赖**,安装者不必再解析它 |
189
207
  | 已打包依赖 `@deepseek-ai/schemastery` | `~3.18.5-alpha.2`,**打进产物**(devDependency) | 构建期依赖 |
190
208
 
191
209
  维护时需要注意:
192
210
 
193
- - **必选与可选之分**:18 个 peer(17 个 `dsh-*` 加框架 `cordis`)中 6 个标了 `optional`(`dsh-api-remotes`、`dsh-client-locale`、`dsh-client-store`、`dsh-client-ui-model-selection`、`dsh-client-ui-settings`、`dsh-client-ui-slots`);真正卡住加载的是 12 个——`cordis`、`dsh-attachment`、`dsh-brand`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer`、`dsh-credentials`、`dsh-fs`、`dsh-launch-environment`、`dsh-llm`、`dsh-timeout`、`dsh-typert-protocol`、`dsh-typert-registry`。其中 `dsh-llm` 是最重的一处耦合(源码 import 二十余处)。
194
- - **名义范围 ≠ 实测范围**:声明范围是 `>=0.2.0-rc.1 <0.3.0`,本项目实际验证过的基线依次是 `0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)、`0.2.0-rc.2`(同上 + `prepareCall` 代次冻结)、`0.2.1-alpha.3`(首个目标版本)、`0.2.1-alpha.4`(T-24)、`0.2.1-alpha.5`(T-26)与 `0.2.1-rc.2`(T-31,运行时升到 cordis `4.0.5`;上游契约层源码零变更)。范围内的其他版本没有逐个验证过。
195
- - **上界就是跟版线**:`<0.3.0` 意味着 DSH 一旦发布 `0.3.0`,本插件必须同批发新版,否则所有用户加载失败。调整范围后用 `npm test` 验证即可(本地 mock 网关,零网络零 token)——范围以接口声明为准,不要用线上探测来"测"出一个范围。
211
+ - **必选与可选之分**:18 个 peer(17 个 `dsh-*` 加框架 `cordis`)中 6 个标了 `optional`(`dsh-api-remotes`、`dsh-client-locale`、`dsh-client-store`、`dsh-client-ui-model-selection`、`dsh-client-ui-settings`、`dsh-client-ui-slots`);其余 12 个是运行时必需的——`cordis`、`dsh-attachment`、`dsh-brand`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer`、`dsh-credentials`、`dsh-fs`、`dsh-launch-environment`、`dsh-llm`、`dsh-timeout`、`dsh-typert-protocol`、`dsh-typert-registry`。这 18 个当前都**不参与加载判定**(见上:`engines.dsh` 单独决定),它们的价值在于声明依赖并供包管理器与读者使用。其中 `dsh-llm` 是最重的一处耦合(源码 import 二十余处)。
212
+ - **名义范围 ≠ 实测范围**:声明范围是 `>=0.2.0-rc.1 <0.3.0-0`,本项目实际验证过的基线依次是 `0.2.0-rc.1`(开发依赖基线:门禁判定 + typecheck + 全量用例)、`0.2.0-rc.2`(同上 + `prepareCall` 代次冻结)、`0.2.1-alpha.3`(首个目标版本)、`0.2.1-alpha.4`(T-24)、`0.2.1-alpha.5`(T-26)与 `0.2.1-rc.2`(T-31,运行时升到 cordis `4.0.5`;上游契约层源码零变更)。范围内的其他版本没有逐个验证过。
213
+ - **上界就是跟版线**:`<0.3.0-0` 意味着 DSH 一旦发布 `0.3.0-0`(`0.3.0` 线的第一个预发布版),本插件必须同批发新版,否则所有用户加载失败。`tests/peer-coverage.spec.ts` 会同时断言下界接受已安装版本、上界拒绝 `0.3.0-0`,所以这条线漂移会在本地测试里先失败,而不是等用户加载失败。调整范围后用 `npm test` 验证即可(本地 mock 网关,零网络零 token)——范围以接口声明为准,不要用线上探测来"测"出一个范围。
196
214
  - **peer 覆盖已双向守住**:`dsh-typert-registry`、`dsh-client-ui-conversation`、`dsh-client-ui-renderer` 曾被源码 import 却未声明、门禁管不到,现已补进 `peerDependencies`;`tests/peer-coverage.spec.ts` 双向断言——源码用到的必须声明,声明了却没人用的必须删(`dsh-settings` 就这样被移除:设置表单实际由 `dsh-client-ui-settings` 提供)。
197
215
  - **豁免是按 profile 记的**:只有显式执行过 `dsh plugin allow-version` 的 profile 才享有豁免;没有记录就代表必须落在声明范围内。用 `dsh plugin version-exemptions` 可查看当前 profile 的豁免。
198
216
 
@@ -217,11 +235,13 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
217
235
  npm ci
218
236
  npm run lint # oxlint:静态检查(发布流程也会跑,失败即阻断)
219
237
  npm run typecheck # host 与 client 两半
238
+ npm run build # 先构建:产出 lib/index.js 与 lib/client.js
220
239
  npm test # 契约测试:真实 HTTP mock 网关,断言请求头/体、目录阶梯、用量
221
- npm run build # 产出 lib/index.js 与 lib/client.js
222
240
  ```
223
241
 
224
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致)。共 **33 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
242
+ **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
243
+
244
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致)。共 **43 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
225
245
 
226
246
  ## 发布(维护者)
227
247
 
package/lib/client.js CHANGED
@@ -228,13 +228,20 @@ function parseTotals(value) {
228
228
  if (typeof entry !== "number" || !Number.isFinite(entry) || entry < 0) throw new Error(`invalid meter field "${key}"`);
229
229
  return entry;
230
230
  };
231
+ const optionalCount = (key) => row[key] === void 0 ? void 0 : count(key);
232
+ const reasoningTokens = optionalCount("reasoningTokens");
233
+ const estimatedUSD = optionalCount("estimatedUSD");
234
+ const unpricedTokens = optionalCount("unpricedTokens");
231
235
  return {
232
236
  calls: count("calls"),
233
237
  inputTokens: count("inputTokens"),
234
238
  outputTokens: count("outputTokens"),
235
239
  cacheReadTokens: count("cacheReadTokens"),
236
240
  cacheWriteTokens: count("cacheWriteTokens"),
237
- totalTokens: count("totalTokens")
241
+ totalTokens: count("totalTokens"),
242
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens },
243
+ ...estimatedUSD === void 0 ? {} : { estimatedUSD },
244
+ ...unpricedTokens === void 0 ? {} : { unpricedTokens }
238
245
  };
239
246
  }
240
247
  function parseGoMeter(value) {
@@ -350,6 +357,12 @@ var en = {
350
357
  usageCallsUnit: "calls",
351
358
  usageTokensUnit: "tokens",
352
359
  usageCached: "cached read",
360
+ usageReasoning: "reasoning tokens",
361
+ usageSpend: "Estimated spend",
362
+ usageUnpriced: "Unpriced",
363
+ usageSpendPartial: "Unpriced tokens",
364
+ usageUnpricedModels: "No published rate:",
365
+ usageSpendHint: "models.dev rates \xD7 the tokens the provider reported \u2014 an estimate, not a bill. On the subscription (Go) endpoints it is indicative only; only the metered (Console) endpoints make it reconcilable.",
353
366
  usageLocalHint: "The gateway publishes quota percentages only; token counts are what this process actually spent.",
354
367
  usageLoading: "Loading\u2026",
355
368
  updateTitle: "Plugin update",
@@ -440,6 +453,12 @@ var zh = {
440
453
  usageCallsUnit: "\u6B21\u8C03\u7528",
441
454
  usageTokensUnit: "tokens",
442
455
  usageCached: "\u7F13\u5B58\u8BFB\u53D6",
456
+ usageReasoning: "\u63A8\u7406 token",
457
+ usageSpend: "\u4F30\u7B97\u82B1\u8D39",
458
+ usageUnpriced: "\u672A\u5B9A\u4EF7",
459
+ usageSpendPartial: "\u672A\u5B9A\u4EF7 token",
460
+ usageUnpricedModels: "\u65E0\u5355\u4EF7\u7684\u6A21\u578B\uFF1A",
461
+ usageSpendHint: "\u6309 models.dev \u5355\u4EF7 \xD7 provider \u56DE\u4F20 token \u4F30\u7B97\uFF0C\u4E0D\u662F\u8D26\u5355\u3002\u8BA2\u9605\u7AEF\u70B9\uFF08Go\uFF09\u4E0B\u4EC5\u4F9B\u53C2\u8003\uFF0C\u6309\u91CF\u7AEF\u70B9\uFF08Console\uFF09\u624D\u6709\u5BF9\u8D26\u610F\u4E49\u3002",
443
462
  usageLocalHint: "\u7F51\u5173\u53EA\u516C\u5E03\u989D\u5EA6\u767E\u5206\u6BD4\uFF1Btoken \u6570\u4E3A\u672C\u8FDB\u7A0B\u5B9E\u9645\u6D88\u8017\u3002",
444
463
  usageLoading: "\u52A0\u8F7D\u4E2D\u2026",
445
464
  updateTitle: "\u63D2\u4EF6\u66F4\u65B0",
@@ -1353,6 +1372,12 @@ function failureOf(error) {
1353
1372
  return { message: "", retainPrevious: false };
1354
1373
  }
1355
1374
  var POLL_MS = 6e4;
1375
+ var MAX_UNPRICED_IDS = 3;
1376
+ function formatUSD(value) {
1377
+ if (value === 0) return "$0.0000";
1378
+ if (value < 1e-4) return "<$0.0001";
1379
+ return value < 1 ? `$${value.toFixed(4)}` : `$${value.toFixed(2)}`;
1380
+ }
1356
1381
  function UsagePill({ directory, ...rest }) {
1357
1382
  const state = (0, import_react2.useSyncExternalStore)(directory.subscribe, directory.getSnapshot, directory.getSnapshot);
1358
1383
  return state.current?.provider === "opencode-go" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ActiveUsage, { ...rest }) : null;
@@ -1420,6 +1445,8 @@ function ActiveUsage({
1420
1445
  };
1421
1446
  }, [open]);
1422
1447
  const label = windows === void 0 ? `Go \xB7 ${failure === void 0 ? "\u2026" : t("usageUnavailable")}` : `Go \xB7 ${t("usageRolling")} ${windows.rolling.percent}% \xB7 ${t("usageWeekly")} ${windows.weekly.percent}%` + (failure === void 0 ? "" : ` \xB7 ${t("usageStale")}`);
1448
+ const unpriced = (meter?.models ?? []).filter((entry) => entry.estimatedUSD === void 0);
1449
+ const unpricedModels = unpriced.slice(0, MAX_UNPRICED_IDS).map((entry) => entry.model).join(", ") + (unpriced.length > MAX_UNPRICED_IDS ? ` +${unpriced.length - MAX_UNPRICED_IDS}` : "");
1423
1450
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: pill_default.root, ref: root, children: [
1424
1451
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1425
1452
  "button",
@@ -1473,6 +1500,25 @@ function ActiveUsage({
1473
1500
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("usageCached") }),
1474
1501
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: meter.totals.cacheReadTokens.toLocaleString(getLocale?.()) })
1475
1502
  ] }),
1503
+ meter.totals.reasoningTokens !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: pill_default.row, children: [
1504
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("usageReasoning") }),
1505
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: meter.totals.reasoningTokens.toLocaleString(getLocale?.()) })
1506
+ ] }),
1507
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: pill_default.row, children: [
1508
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("usageSpend") }),
1509
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: meter.totals.estimatedUSD === void 0 ? t("usageUnpriced") : formatUSD(meter.totals.estimatedUSD) })
1510
+ ] }),
1511
+ meter.totals.unpricedTokens !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: pill_default.hint, children: [
1512
+ t("usageSpendPartial"),
1513
+ " ",
1514
+ meter.totals.unpricedTokens.toLocaleString(getLocale?.())
1515
+ ] }),
1516
+ unpriced.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: pill_default.hint, children: [
1517
+ t("usageUnpricedModels"),
1518
+ " ",
1519
+ unpricedModels
1520
+ ] }),
1521
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: pill_default.hint, children: t("usageSpendHint") }),
1476
1522
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: pill_default.hint, children: t("usageLocalHint") })
1477
1523
  ] }),
1478
1524
  updatedAt !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: pill_default.hint, children: [