dsh-multi-provider-usage 0.0.0-stage → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-multi-provider-usage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,428 @@
1
- # Temporary Holding Version
1
+ # dsh-multi-provider-usage
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [DSH](https://github.com/deepseek-ai/deepseek-harness) plugin that shows the
4
+ quotas of **OpenCode GO**, **Command Code** and **GLM Code Plan** in one sidebar
5
+ widget. It is modelled on
6
+ [`@astervolans/dsh-opencode-go-usage`](https://github.com/Astervolans/dsh-opencode-go-usage)
7
+ — same dual-half package, same-origin proxy, same sidebar slot, same Plugins
8
+ panel settings page — extended from one provider to three.
9
+
10
+ > The package name is `dsh-multi-provider-usage` because the unscoped
11
+ > `dsh-provider-usage` name is already taken on npm by a different plugin. The
12
+ > HTTP routes (`/provider-usage/*`) and chat command (`/provider-usage`) stay
13
+ > short and stable.
14
+
15
+ ## Features
16
+
17
+ ![Sidebar widget with the three stacked providers](docs/widget.png)
18
+
19
+
20
+ - **Three providers, one column.** All enabled providers are stacked vertically;
21
+ each header carries a health dot, the provider's primary percentage and its
22
+ top-up balance. Click a provider header to collapse/expand that provider's
23
+ plan windows, quota bars and time bars; the collapse state is remembered
24
+ locally, so any subset can stay folded.
25
+ - **OpenCode GO** — 5-hour, weekly and monthly windows (the API calls the 5-hour field `rolling`). Prefers the live
26
+ `dsh-opencode-go` plugin's active account through its `opencodeGoUsage`
27
+ host service, then falls back to the official `GET /v1/usage` endpoint.
28
+ - **Command Code** — monthly credit pool, 5-hour window and weekly window from
29
+ the official billing/usage endpoints (`/alpha/billing/*`,
30
+ `/alpha/usage/summary`).
31
+ - **GLM Code Plan** — 5-hour token window, weekly token window and monthly MCP
32
+ tool limit from Z.ai / BigModel `/api/monitor/usage/quota/limit`.
33
+ - **Top-up balances** — OpenCode GO shows the OpenCode Zen prepaid balance and
34
+ Command Code shows its purchased + free "Extra Credits" balance, both as a
35
+ badge next to the account/plan and in the `/provider-usage` text. The OpenCode
36
+ console token is auto-discovered from `OPENCODE_CONSOLE_TOKEN`, the OpenCode
37
+ CLI auth file, or its local `opencode.db`; the token never leaves the host.
38
+ When that token is missing, the balance slot becomes a clickable "Set up" chip
39
+ that opens the plugin settings, where the OpenCode GO card renders a
40
+ step-by-step setup guide, instead of silently showing nothing. Command
41
+ Code's tooltip breaks its amount into purchased + free credits; a credits
42
+ response without either pool renders a muted **No top-up** badge.
43
+ - **Auto-update** — on boot and every configured interval the host checks the
44
+ GitHub release feed. A local `link:`/`file:` checkout is fast-forwarded when it
45
+ is on `main`, clean, and a strict fast-forward. An npm / market / tarball copy
46
+ updates itself: the release tarball is unpacked and each packaged file
47
+ (`lib/`, `package.json`, `cordis.patch.yml`, `docs/`) is written to a temp file
48
+ and renamed over the target, so pnpm's hardlinked store is never edited in
49
+ place. Either way the running Host keeps the old module graph, so the
50
+ widget/settings show "updated — restart DSH". The plugin never restarts DSH by
51
+ itself.
52
+ - **Balance-change animation** — the widget polls on its normal interval; when
53
+ a provider's balance differs from the previous poll, the number counts from
54
+ the old value to the refreshed one while a red/green delta chip (for example
55
+ `-0.1`) floats up and fades away. There is no popup and no turn-event
56
+ tracking, so the animation may appear up to one poll interval later.
57
+ - **Low-balance tone** — a balance at or below 5 renders amber and at or below 1
58
+ renders red (thresholds are in the currency unit and apply to USD and CNY
59
+ plans alike), in both the sidebar and the settings page's live-usage sections.
60
+ - **Click-to-retry failures** — an error line in the widget is itself clickable
61
+ (and keyboard-activatable) and forces one fresh upstream read.
62
+ - **Same-origin proxy** — the browser only talks to `/provider-usage/usage` and
63
+ `/provider-usage/settings`; API keys stay in the host process.
64
+ - **Failure isolation** — one broken provider never hides the other two. The
65
+ route answers `200` while any provider succeeds and `502` only when all
66
+ enabled providers fail. Partial endpoint failures keep the provider's numbers
67
+ but turn its health dot amber and name the failed calls in the tooltip.
68
+ - **Settings shortcut** — the widget header's **Settings** button opens this
69
+ bundle's page in the sidebar **Plugins** panel, directly on the configuration
70
+ page below.
71
+ - **Bundle settings page** — sidebar **Plugins** panel → this bundle's card:
72
+ enable/disable each provider, edit credential refs and endpoints, choose the
73
+ GLM region, and see non-network detection status. Save applies immediately.
74
+ Each provider card embeds a **live usage** section — quota windows with
75
+ percentages, time-to-reset and the top-up balance, from the same canonical
76
+ snapshot the widget renders. While the page is open it follows the widget:
77
+ a 30-second poll (when the tab is visible) plus an "Updated HH:MM" stamp,
78
+ a **Refresh usage** control forces a re-read, and every save re-reads with
79
+ `refresh=1`, so a changed credential ref shows its own numbers immediately.
80
+ The GLM card also carries the **referral link** (Z.ai "Invite Friends, Get
81
+ Credits") as a clickable, copyable row. The URL is a bundle constant: it never
82
+ goes through the settings route and a settings save cannot overwrite it.
83
+ - **`/provider-usage` chat command** — prints every enabled provider's numbers
84
+ as text.
85
+ - **Time bars** — under each quota bar, gray is time already elapsed and blue is
86
+ time remaining, matching the reference widget's geometry.
87
+ - **`x-opencode-session` fix** — injects the real per-conversation harness
88
+ session id into OpenCode GO chat requests at runtime, without patching DSH.
89
+ - **Chinese and English UI** through the DSH locale service.
90
+
91
+ ## Install
92
+
93
+ From npm:
94
+
95
+ ```bash
96
+ dsh plugin --profile desktop add dsh-multi-provider-usage
97
+ ```
98
+
99
+ or, for the web profile:
100
+
101
+ ```bash
102
+ dsh plugin --profile web add dsh-multi-provider-usage
103
+ ```
104
+
105
+ From a checkout (for development):
106
+
107
+ ```bash
108
+ dsh plugin --profile desktop add /path/to/dsh-provider-usage
109
+ ```
110
+
111
+ The package declares `dsh.bundle.patch`, so the CLI reconciles it into the
112
+ profile's bundle layer stack automatically. The widget and client bundle are
113
+ part of the profile's client-modules graph, so **restart DSH once** after
114
+ installing (a running host does not rebuild its `window.__DSH_BOOT__` manifest
115
+ for a newly linked package).
116
+
117
+ For a local `link:` dependency pnpm prints a peer-dependency warning
118
+ (`@deepseek-ai/schemastery`, `@deepseek-ai/dsh`). That is expected: the linked
119
+ checkout resolves schemastery from its own `node_modules` in development; a
120
+ normal npm install resolves the peers from the DSH runtime.
121
+
122
+ ## Credentials
123
+
124
+ All values are resolved through the DSH credentials domain (the process
125
+ environment, `$DSH_HOME/.credentials.yaml` and `.env` layers). They are never
126
+ returned by the settings route and never reach the browser.
127
+
128
+ | Provider | Default ref | Resolution order | Fallback source |
129
+ |---|---|---|---|
130
+ | OpenCode GO | `OPENCODE_GO_API_KEY` | `opencodeGoApiKeyEnv`, the `dsh-opencode-go` active-account ref (only when that entry is enabled), `OPENCODE_GO_API_KEY`, `OPENCODE_API_KEY` | `GET {baseUrl}/v1/usage` |
131
+ | Command Code | `COMMANDCODE_API_KEY` | `commandCodeApiKeyEnv`, `COMMANDCODE_API_KEY`, then the CLI auth file | `~/.commandcode/auth.json` (`apiKey`) |
132
+ | GLM Code Plan | `ZAI_API_KEY` | `glmApiKeyEnv`, `ZAI_CODING_CN_API_KEY`, `ZAI_API_KEY`, `ZHIPU_API_KEY`, `GLM_API_KEY`, `BIGMODEL_API_KEY` | — |
133
+
134
+ Set a ref in **Plugins → dsh-multi-provider-usage → settings** (or in the
135
+ auto-generated **Settings → Plugins → Plugin configuration** form). A ref is a
136
+ name such as `OPENCODE_GO_API_KEY`; the secret itself lives in DSH's credential
137
+ store.
138
+
139
+ ### OpenCode console balance
140
+
141
+ The GO quota API does not include the account's prepaid balance. That comes from
142
+ `GET https://opencode.ai/console/api/billing/status`, which needs the OpenCode
143
+ **console** OAuth token (the GO API key returns 403 there). The host resolves it
144
+ in this order:
145
+
146
+ 1. `opencodeConsoleApiKeyEnv` (`OPENCODE_CONSOLE_TOKEN` by default), resolved
147
+ through DSH credentials;
148
+ 2. the `credential` table of `~/.local/share/opencode/opencode.db` (read via
149
+ `node:sqlite` when available; this is where current OpenCode releases keep
150
+ the OAuth grant and rotate it);
151
+ 3. `~/.local/share/opencode/auth.json` (`$XDG_DATA_HOME/opencode/auth.json` when
152
+ set), provider `opencode` — the legacy location kept by older releases.
153
+
154
+ The database is preferred deliberately: OpenCode migrated its grant there but
155
+ often leaves the old `auth.json` behind, and that stale token returns 401. Every
156
+ usable OAuth candidate is tried in turn (at most four), so one expired store
157
+ cannot hide the live one. Channel builds (`opencode-beta.db`, `opencode-dev.db`,
158
+ …) and `$OPENCODE_DB` are discovered too. The billing host comes from the grant's
159
+ `metadata.server` (so a self-hosted console works), falling back to the public
160
+ origin; `opencodeConsoleBaseUrl` overrides it.
161
+
162
+ An OpenCode **API-key** login (`oc_sk_…`, the service-account method) carries no
163
+ console session and is never sent to the billing endpoint. Such a login, a
164
+ missing token, or an expired/revoked grant renders a clickable **Set up** chip
165
+ (instead of a mute dash) that opens this bundle's page in the Plugins panel and
166
+ names the fix: run `opencode auth login` and choose the **OpenCode Console
167
+ account**, or store a console token as `OPENCODE_CONSOLE_TOKEN`.
168
+
169
+ When the balance is not configured (or the console login is fixable), the
170
+ OpenCode GO card in the plugin settings renders a step-by-step **setup guide**
171
+ in place of the static hint. The lead-in names the state-specific fix (nothing
172
+ configured, an API-key login, or an expired sign-in). Step ① shows the
173
+ `opencode auth login` CLI command with a **Copy command** button; step ② is the
174
+ manual path — copy the console token and store it as the `OPENCODE_CONSOLE_TOKEN`
175
+ credential — with a **Copy credential ref** button and a link to the OpenCode
176
+ console website (derived from `opencodeConsoleBaseUrl`, ending in `/console`);
177
+ step ③ offers a **Re-check** button that re-reads the sign-in state and forces
178
+ one upstream usage read, so a fresh login shows the balance without restarting
179
+ DSH (unsaved form edits are never discarded by the re-check). The
180
+ `/provider-usage` command gives the same state-specific guidance as text for
181
+ all three fixable states.
182
+
183
+ The optional `opencodeConsoleAuthFile` overrides the auth file (and its
184
+ directory is used to locate `opencode.db`), `opencodeConsoleOrgId` supplies the
185
+ `x-org-id` header, and `opencodeConsoleEnabled: false` turns the whole balance
186
+ read off. Balance failures never hide the GO quota: the provider stays `ok` with
187
+ `balance: null` and a `meta.balanceError` (plus `meta.balanceSetup` when the
188
+ user can fix it). Any other balance failure renders a muted **Balance —** badge
189
+ whose tooltip carries the error.
190
+
191
+ ### Command Code balance
192
+
193
+ Command Code's quota and balance share one API key and one billing family, so
194
+ they cannot diverge the way OpenCode's do: a missing key (`COMMANDCODE_API_KEY`
195
+ or `~/.commandcode/auth.json`) fails the whole provider with an explicit
196
+ `no API key` message rather than blanking only the balance. Two narrower gaps
197
+ are reported instead of hidden:
198
+
199
+ - when `/alpha/billing/credits` fails, that request carries every window, so the
200
+ provider errors as well; its message now includes the underlying interface and
201
+ status instead of only "no usable quota windows", and `meta.balanceError` lets
202
+ the header show a muted **Balance —** badge carrying the cause;
203
+ - when the credits response succeeds but carries neither `purchasedCredits` nor
204
+ `freeCredits`, `balance` stays `null` with `meta.balanceMissing: true` and the
205
+ header shows a muted **No top-up** badge (a real `0` still renders `$0.00`).
206
+
207
+ ### OpenCode GO integration and fallback
208
+
209
+ When `opencodeGoSource` is `opencode-go` (the default), the host probes:
210
+
211
+ 1. a live loader entry named `opencode-go` / `dsh-opencode-go`;
212
+ 2. that entry is not disabled;
213
+ 3. `ctx.get("opencodeGoUsage")` exposes `read()`;
214
+ 4. `read()` returns valid `rolling` / `weekly` / `monthly` data;
215
+ 5. the active account is read from that entry's `apiKeyEnv` + `accounts[]`.
216
+
217
+ If any step fails, the host reads the built-in endpoint and reports a `fallback`
218
+ object with one of: `not-installed`, `entry-disabled`, `service-unavailable`,
219
+ `read-failed`, `invalid-response`. The widget shows a **Built-in** badge with the
220
+ reason and message in its tooltip. `opencodeGoSource: "builtin"` pins the HTTP
221
+ path and never touches the other plugin.
222
+
223
+ ## Plan windows
224
+
225
+ | Provider | Windows | Window length |
226
+ |---|---|---|
227
+ | OpenCode GO | `rolling`, `weekly`, `monthly` | 5 h, 7 d, ~30 d |
228
+ | Command Code | `five-hour`, `weekly`, `monthly` | 5 h, 7 d, real billing period |
229
+ | GLM Code Plan | `five-hour`, `weekly`, `mcp` | 5 h, 7 d, monthly |
230
+
231
+ Percentages are **used** percentages and are clamped to `[0, 100]` for display.
232
+ Command Code monthly utilization is
233
+ `totalCost / (totalCost + monthlyCredits)`; purchased and free credits are
234
+ reported separately in `meta.monthly` and do not change the plan bar, while
235
+ their sum is the visible top-up balance (`balance.extra`). The
236
+ 5-hour/weekly bars use `windowLimits.fiveHour` / `weekly` (`used`, `cap`,
237
+ `resetAt`, `exceeded`).
238
+
239
+ A GLM account without a Coding Plan answers HTTP 200 with
240
+ `{ "success": false, "msg": ... }`; the adapter surfaces that message and the
241
+ widget shows the provider in an error state.
242
+
243
+ ## Configuration
244
+
245
+ The loader reads the exported `Config` schema; every field is `.volatile()`, so
246
+ the settings page and the auto-generated form hot-apply.
247
+
248
+ | Field | Default | Meaning |
249
+ |---|---|---|
250
+ | `opencodeGoEnabled` | `true` | Show OpenCode GO |
251
+ | `opencodeGoSource` | `"opencode-go"` | `opencode-go` or `builtin` |
252
+ | `opencodeGoApiKeyEnv` | `"OPENCODE_GO_API_KEY"` | Credential ref |
253
+ | `opencodeGoBaseUrl` | `"https://opencode.ai/zen/go"` | Gateway base |
254
+ | `opencodeConsoleEnabled` | `true` | Read the Zen prepaid balance |
255
+ | `opencodeConsoleApiKeyEnv` | `"OPENCODE_CONSOLE_TOKEN"` | Console token ref |
256
+ | `opencodeConsoleOrgId` | `""` | Optional `x-org-id` |
257
+ | `opencodeConsoleAuthFile` | `""` | Optional auth.json path; empty auto-discovers |
258
+ | `opencodeConsoleDbFile` | `""` | Optional opencode.db path; empty derives it |
259
+ | `opencodeConsoleBaseUrl` | `"https://opencode.ai"` | Console origin |
260
+ | `commandCodeEnabled` | `true` | Show Command Code |
261
+ | `commandCodeApiKeyEnv` | `"COMMANDCODE_API_KEY"` | Credential ref |
262
+ | `commandCodeBaseUrl` | `"https://api.commandcode.ai"` | API base |
263
+ | `commandCodeAuthFile` | `""` | Empty = `~/.commandcode/auth.json` |
264
+ | `glmEnabled` | `true` | Show GLM Code Plan |
265
+ | `glmApiKeyEnv` | `"ZAI_API_KEY"` | Credential ref |
266
+ | `glmBaseUrl` | `""` | Optional base override |
267
+ | `glmRegion` | `"auto"` | `auto`, `zai`, `bigmodel`, `custom` |
268
+ | `cacheMs` | `30000` | Per-provider host cache TTL |
269
+ | `updateCheck` | `true` | npm update check (24 h) |
270
+ | `autoUpdate` | `true` | Check GitHub releases; fast-forward a clean checkout, otherwise replace an installed package |
271
+ | `autoUpdateCheckHours` | `6` | Hours between auto-update checks (1–168) |
272
+ | `injectSessionHeader` | `true` | OpenCode GO session-header fix |
273
+ | `providerOrder` | all three ids | Provider display order in the widget; drag the cards in the plugin settings to reorder |
274
+
275
+ `glmRegion: "custom"` requires `glmBaseUrl`; `auto` probes
276
+ `https://api.z.ai` first and then `https://open.bigmodel.cn`.
277
+
278
+ ## HTTP API
279
+
280
+ ### `GET /provider-usage/usage`
281
+
282
+ `?refresh=1` (or `?force=1`) bypasses the host cache.
283
+
284
+ ```json
285
+ {
286
+ "ok": true,
287
+ "enabled": ["opencode-go", "command-code", "glm"],
288
+ "providerOrder": ["opencode-go", "command-code", "glm"],
289
+ "providers": [
290
+ {
291
+ "id": "opencode-go",
292
+ "name": "OpenCode GO",
293
+ "short": "GO",
294
+ "status": "ok",
295
+ "account": { "ref": "OPENCODE_GO_API_KEY", "id": null, "name": "" },
296
+ "plan": null,
297
+ "source": "builtin",
298
+ "requested": "opencode-go",
299
+ "fallback": {
300
+ "from": "opencode-go",
301
+ "reason": "not-installed",
302
+ "message": "dsh-opencode-go is not installed, or its profile entry is inactive"
303
+ },
304
+ "balance": {
305
+ "amount": 5.01495352,
306
+ "currency": "USD",
307
+ "labelKey": "balance.zen",
308
+ "source": "opencode-db",
309
+ "details": { "billingMode": "prepaid", "mode": "pay-as-you-go" }
310
+ },
311
+ "windows": [
312
+ {
313
+ "key": "rolling",
314
+ "labelKey": "window.five-hour",
315
+ "hintKey": "window.five-hour.hint",
316
+ "percent": 0,
317
+ "status": "ok",
318
+ "resetsAt": "2026-11-05T13:50:46.363Z",
319
+ "windowMs": 18000000
320
+ }
321
+ ],
322
+ "meta": {}
323
+ }
324
+ ],
325
+ "update": { "current": "0.1.0", "available": false, "latest": null },
326
+ "sessionHeader": { "active": true, "count": 0, "diag": {} }
327
+ }
328
+ ```
329
+
330
+ Failed providers have `"status": "error"`, `code`, `error` and `windows: []`;
331
+ their siblings are still returned. A body with `providers: []` means no provider
332
+ is enabled. `meta.balanceError` carries a tolerated balance failure (the
333
+ provider still answers `ok`); `meta.balanceSetup` (`no-credential`, `api-key` or
334
+ `expired`) marks a failure the user can fix by signing in again;
335
+ `meta.balanceMissing` marks a Command Code credits response without
336
+ purchased/free pools. `meta.errors` lists partial Command Code endpoint
337
+ failures: the numbers stay, the health dot turns amber, and the tooltip names
338
+ the failed calls. Error cards keep the plan they already resolved.
339
+
340
+ ### `GET /provider-usage/settings`
341
+
342
+ Returns the settings descriptor (`namespace`, `revision`, `value`, `base`,
343
+ `user`) plus a non-network `detection` report:
344
+
345
+ ```json
346
+ {
347
+ "ok": true,
348
+ "namespace": "dsh-multi-provider-usage",
349
+ "revision": 0,
350
+ "value": { "opencodeGoEnabled": true },
351
+ "detection": {
352
+ "providerOrder": ["opencode-go", "command-code", "glm"],
353
+ "enabled": ["opencode-go", "command-code", "glm"],
354
+ "providers": {
355
+ "opencode-go": { "configured": true, "entryFound": false, "fallback": { "...": "..." } },
356
+ "command-code": { "configured": true, "source": "auth-file" },
357
+ "glm": { "configured": true, "region": "auto" }
358
+ }
359
+ }
360
+ }
361
+ ```
362
+
363
+ `503` means the host has no settings service or the entry has no volatile
364
+ fields; the settings page explains how to fix it.
365
+
366
+ ### `POST /provider-usage/settings`
367
+
368
+ Only the fields in the table above are accepted; anything else is ignored.
369
+ `expectedRevision` gets `409` on a concurrent change.
370
+
371
+ ```json
372
+ {
373
+ "patch": { "commandCodeEnabled": false, "providerOrder": ["glm", "opencode-go", "command-code"] },
374
+ "expectedRevision": 0
375
+ }
376
+ ```
377
+
378
+ Cross-origin writes are refused with `403`. The usage route is read-only.
379
+
380
+ ## How it is put together
381
+
382
+ | Half | File | Role |
383
+ |---|---|---|
384
+ | host (Node) | `lib/index.js` | Config schema, routes, command, settings/detection, cache, session fix |
385
+ | host adapters | `lib/providers.js` | Canonical window shape and the three vendor adapters |
386
+ | console balance | `lib/opencode-console.js` | OpenCode console token discovery + billing status |
387
+ | session fix | `lib/opencode-session.js` | `globalThis.fetch` wrapper + `llm/stream` ALS context |
388
+ | browser | `lib/client.js` | Hand-authored `window.__ModuleLoader__.load({ id, factory })` bundle: widget, settings page, locale dictionaries |
389
+ | bundle patch | `cordis.patch.yml` | Inserts the profile entry |
390
+
391
+ The browser bundle is composed by DSH's official client-modules scan from
392
+ `dsh.client` + `exports["./client"]`; it registers into
393
+ `sidebar.footer.action` and `plugins.bundle.config`, exactly like the reference
394
+ plugin.
395
+
396
+ The session fix wraps `globalThis.fetch` once and only adds
397
+ `x-opencode-session` when the request URL starts with the gateway base carried
398
+ by the current `llm/stream` call. The base comes from
399
+ `llm-pi-ai.providers.<route>.baseURL`, falling back to `opencodeGoBaseUrl`.
400
+
401
+ ## Compatibility
402
+
403
+ - DSH `0.1.7` and above, including the `0.2.0` line; verified on `0.2.1-rc.5`.
404
+ - `@deepseek-ai/schemastery` `^3.18.4` (volatile fields / settings page).
405
+ - Node.js ≥ 18 (global `fetch`, `AbortSignal.timeout`).
406
+ - `dsh-opencode-go` is optional; its absence is a normal fallback.
407
+ - Command Code's CLI auth file is optional if `COMMANDCODE_API_KEY` is set.
408
+ - A DSH runtime that does not expose `plugins.bundle.config` loses only the
409
+ settings page; the sidebar widget and `/provider-usage` keep working. Set the
410
+ config fields in the profile patch or the auto-generated form instead.
411
+
412
+ ## Development
413
+
414
+ ```bash
415
+ npm install
416
+ npm test
417
+ ```
418
+
419
+ The tests are hermetic: a fake `globalThis.fetch` and a stub Cordis context, no
420
+ network and no real keys. They cover the schema, all three adapters, OpenCode
421
+ GO service integration/fallback, aggregation and error isolation, caching and
422
+ forced refresh, settings conflicts/whitelisting, the client bundle contract,
423
+ top-up balance discovery (credential and local auth.json) and failure
424
+ isolation, and the session-header injection. 98 tests, all offline.
425
+
426
+ ## License
427
+
428
+ MIT.