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 +21 -0
- package/README.md +427 -2
- package/README.zh.md +377 -0
- package/cordis.patch.yml +15 -0
- package/docs/widget.png +0 -0
- package/lib/auto-update.js +570 -0
- package/lib/client.js +2226 -0
- package/lib/index.js +1290 -0
- package/lib/opencode-console.js +556 -0
- package/lib/opencode-session.js +139 -0
- package/lib/providers.js +919 -0
- package/lib/util.js +27 -0
- package/package.json +64 -4
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
|
-
#
|
|
1
|
+
# dsh-multi-provider-usage
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+

|
|
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.
|