dsh-multi-provider-usage
A DSH plugin that shows the
quotas of OpenCode GO, Command Code and GLM Code Plan in one sidebar
widget. It is modelled on
@astervolans/dsh-opencode-go-usage
— same dual-half package, same-origin proxy, same sidebar slot, same Plugins
panel settings page — extended from one provider to three.
The package name is
dsh-multi-provider-usagebecause the unscopeddsh-provider-usagename is already taken on npm by a different plugin. The HTTP routes (/provider-usage/*) and chat command (/provider-usage) stay short and stable.
Features

- Three providers, one column. All enabled providers are stacked vertically; each header carries a health dot, the provider's primary percentage and its top-up balance. Click a provider header to collapse/expand that provider's plan windows, quota bars and time bars; the collapse state is remembered locally, so any subset can stay folded.
- OpenCode GO — rolling (5 h), weekly and monthly windows. Prefers the live
dsh-opencode-goplugin's active account through itsopencodeGoUsagehost service, then falls back to the officialGET /v1/usageendpoint. - Command Code — monthly credit pool, 5-hour window and weekly window from
the official billing/usage endpoints (
/alpha/billing/*,/alpha/usage/summary). - GLM Code Plan — 5-hour token window, weekly token window and monthly MCP
tool limit from Z.ai / BigModel
/api/monitor/usage/quota/limit. - Top-up balances — OpenCode GO shows the OpenCode Zen prepaid balance and
Command Code shows its purchased + free "Extra Credits" balance, both as a
badge next to the account/plan and in the
/provider-usagetext. The OpenCode console token is auto-discovered fromOPENCODE_CONSOLE_TOKEN, the OpenCode CLI auth file, or its localopencode.db; the token never leaves the host. - Turn usage card — when a turn finishes and the conversation used OpenCode
GO, Command Code or GLM Code Plan, a floating card in the top-right shows that
provider's latest quota windows, balance and balance change for the turn
(turn-start snapshot vs. the refreshed balance). It listens to the current
session's
turn/endandassistant/messageprovider metadata and ignores turns that used only other providers. The card auto-dismisses after 12 s and has a close button. - Same-origin proxy — the browser only talks to
/provider-usage/usageand/provider-usage/settings; API keys stay in the host process. - Failure isolation — one broken provider never hides the other two. The
route answers
200while any provider succeeds and502only when all enabled providers fail. - Bundle settings page — sidebar Plugins panel → this bundle's card: enable/disable each provider, edit credential refs and endpoints, choose the GLM region, and see non-network detection status. Save applies immediately.
/provider-usagechat command — prints every enabled provider's numbers as text.- Time bars — under each quota bar, gray is time already elapsed and blue is time remaining, matching the reference widget's geometry.
x-opencode-sessionfix — injects the real per-conversation harness session id into OpenCode GO chat requests at runtime, without patching DSH.- Chinese and English UI through the DSH locale service.
Install
From a checkout:
dsh plugin --profile desktop add /path/to/dsh-provider-usage
or, for the web profile:
dsh plugin --profile web add /path/to/dsh-provider-usage
The package declares dsh.bundle.patch, so the CLI reconciles it into the
profile's bundle layer stack automatically. The widget and client bundle are
part of the profile's client-modules graph, so restart DSH once after
installing (a running host does not rebuild its window.__DSH_BOOT__ manifest
for a newly linked package).
For a local link: dependency pnpm prints a peer-dependency warning
(@deepseek-ai/schemastery, @deepseek-ai/dsh). That is expected: the linked
checkout resolves schemastery from its own node_modules in development; a
normal npm install resolves the peers from the DSH runtime.
Credentials
All values are resolved through the DSH credentials domain (the process
environment, $DSH_HOME/.credentials.yaml and .env layers). They are never
returned by the settings route and never reach the browser.
| Provider | Default ref | Resolution order | Fallback source |
|---|---|---|---|
| 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 |
| Command Code | COMMANDCODE_API_KEY |
commandCodeApiKeyEnv, COMMANDCODE_API_KEY, then the CLI auth file |
~/.commandcode/auth.json (apiKey) |
| GLM Code Plan | ZAI_API_KEY |
glmApiKeyEnv, ZAI_CODING_CN_API_KEY, ZAI_API_KEY, ZHIPU_API_KEY, GLM_API_KEY, BIGMODEL_API_KEY |
— |
Set a ref in Plugins → dsh-multi-provider-usage → settings (or in the
auto-generated Settings → Plugins → Plugin configuration form). A ref is a
name such as OPENCODE_GO_API_KEY; the secret itself lives in DSH's credential
store.
OpenCode console balance
The GO quota API does not include the account's prepaid balance. That comes from
GET https://opencode.ai/console/api/billing/status, which needs the OpenCode
console OAuth token (the GO API key returns 403 there). The host resolves it
in this order:
opencodeConsoleApiKeyEnv(OPENCODE_CONSOLE_TOKENby default), resolved through DSH credentials;~/.local/share/opencode/auth.json($XDG_DATA_HOME/opencode/auth.jsonwhen set), provideropencode;- the
credentialtable of~/.local/share/opencode/opencode.db(read vianode:sqlitewhen available; this is where current OpenCode releases keep the OAuth grant).
The optional opencodeConsoleAuthFile overrides the auth file (and its
directory is used for opencode.db), opencodeConsoleOrgId supplies the
x-org-id header, and opencodeConsoleEnabled: false turns the whole balance
read off. Balance failures never hide the GO quota: the provider stays ok with
balance: null and a meta.balanceError, and the widget hides the missing
optional credential instead of showing a false error.
OpenCode GO integration and fallback
When opencodeGoSource is opencode-go (the default), the host probes:
- a live loader entry named
opencode-go/dsh-opencode-go; - that entry is not disabled;
ctx.get("opencodeGoUsage")exposesread();read()returns validrolling/weekly/monthlydata;- the active account is read from that entry's
apiKeyEnv+accounts[].
If any step fails, the host reads the built-in endpoint and reports a fallback
object with one of: not-installed, entry-disabled, service-unavailable,
read-failed, invalid-response. The widget shows a Built-in badge with the
reason and message in its tooltip. opencodeGoSource: "builtin" pins the HTTP
path and never touches the other plugin.
Plan windows
| Provider | Windows | Window length |
|---|---|---|
| OpenCode GO | rolling, weekly, monthly |
5 h, 7 d, ~30 d |
| Command Code | five-hour, weekly, monthly |
5 h, 7 d, real billing period |
| GLM Code Plan | five-hour, weekly, mcp |
5 h, 7 d, monthly |
Percentages are used percentages and are clamped to [0, 100] for display.
Command Code monthly utilization is
totalCost / (totalCost + monthlyCredits); purchased and free credits are
reported separately in meta.monthly and do not change the plan bar, while
their sum is the visible top-up balance (balance.extra). The
5-hour/weekly bars use windowLimits.fiveHour / weekly (used, cap,
resetAt, exceeded).
A GLM account without a Coding Plan answers HTTP 200 with
{ "success": false, "msg": ... }; the adapter surfaces that message and the
widget shows the provider in an error state.
Configuration
The loader reads the exported Config schema; every field is .volatile(), so
the settings page and the auto-generated form hot-apply.
| Field | Default | Meaning |
|---|---|---|
opencodeGoEnabled |
true |
Show OpenCode GO |
opencodeGoSource |
"opencode-go" |
opencode-go or builtin |
opencodeGoApiKeyEnv |
"OPENCODE_GO_API_KEY" |
Credential ref |
opencodeGoBaseUrl |
"https://opencode.ai/zen/go" |
Gateway base |
opencodeConsoleEnabled |
true |
Read the Zen prepaid balance |
opencodeConsoleApiKeyEnv |
"OPENCODE_CONSOLE_TOKEN" |
Console token ref |
opencodeConsoleOrgId |
"" |
Optional x-org-id |
opencodeConsoleAuthFile |
"" |
Optional auth.json path; empty auto-discovers |
opencodeConsoleDbFile |
"" |
Optional opencode.db path; empty derives it |
opencodeConsoleBaseUrl |
"https://opencode.ai" |
Console origin |
commandCodeEnabled |
true |
Show Command Code |
commandCodeApiKeyEnv |
"COMMANDCODE_API_KEY" |
Credential ref |
commandCodeBaseUrl |
"https://api.commandcode.ai" |
API base |
commandCodeAuthFile |
"" |
Empty = ~/.commandcode/auth.json |
glmEnabled |
true |
Show GLM Code Plan |
glmApiKeyEnv |
"ZAI_API_KEY" |
Credential ref |
glmBaseUrl |
"" |
Optional base override |
glmRegion |
"auto" |
auto, zai, bigmodel, custom |
cacheMs |
30000 |
Per-provider host cache TTL |
updateCheck |
true |
npm update check (24 h) |
injectSessionHeader |
true |
OpenCode GO session-header fix |
defaultProvider |
"auto" |
Provider pinned first in the column (auto, opencode-go, command-code, glm) |
glmRegion: "custom" requires glmBaseUrl; auto probes
https://api.z.ai first and then https://open.bigmodel.cn.
HTTP API
GET /provider-usage/usage
?refresh=1 (or ?force=1) bypasses the host cache.
{
"ok": true,
"enabled": ["opencode-go", "command-code", "glm"],
"defaultProvider": "auto",
"providers": [
{
"id": "opencode-go",
"name": "OpenCode GO",
"short": "GO",
"status": "ok",
"account": { "ref": "OPENCODE_GO_API_KEY", "id": null, "name": "" },
"plan": null,
"source": "builtin",
"requested": "opencode-go",
"fallback": {
"from": "opencode-go",
"reason": "not-installed",
"message": "dsh-opencode-go is not installed, or its profile entry is inactive"
},
"balance": {
"amount": 5.01495352,
"currency": "USD",
"labelKey": "balance.zen",
"source": "opencode-db",
"details": { "billingMode": "prepaid", "mode": "pay-as-you-go" }
},
"windows": [
{
"key": "rolling",
"labelKey": "window.rolling",
"hintKey": "window.rolling.hint",
"percent": 0,
"status": "ok",
"resetsAt": "2026-11-05T13:50:46.363Z",
"windowMs": 18000000
}
],
"meta": {}
}
],
"update": { "current": "0.1.0", "available": false, "latest": null },
"sessionHeader": { "active": true, "count": 0, "diag": {} }
}
Failed providers have "status": "error", code, error and windows: [];
their siblings are still returned. A body with providers: [] means no provider
is enabled.
GET /provider-usage/settings
Returns the settings descriptor (namespace, revision, value, base,
user) plus a non-network detection report:
{
"ok": true,
"namespace": "dsh-multi-provider-usage",
"revision": 0,
"value": { "opencodeGoEnabled": true },
"detection": {
"defaultProvider": "auto",
"enabled": ["opencode-go", "command-code", "glm"],
"providers": {
"opencode-go": { "configured": true, "entryFound": false, "fallback": { "...": "..." } },
"command-code": { "configured": true, "source": "auth-file" },
"glm": { "configured": true, "region": "auto" }
}
}
}
503 means the host has no settings service or the entry has no volatile
fields; the settings page explains how to fix it.
POST /provider-usage/settings
Only the fields in the table above are accepted; anything else is ignored.
expectedRevision gets 409 on a concurrent change.
{
"patch": { "commandCodeEnabled": false, "defaultProvider": "glm" },
"expectedRevision": 0
}
Cross-origin writes are refused with 403. The usage route is read-only.
How it is put together
| Half | File | Role |
|---|---|---|
| host (Node) | lib/index.js |
Config schema, routes, command, settings/detection, cache, session fix |
| host adapters | lib/providers.js |
Canonical window shape and the three vendor adapters |
| console balance | lib/opencode-console.js |
OpenCode console token discovery + billing status |
| session fix | lib/opencode-session.js |
globalThis.fetch wrapper + llm/stream ALS context |
| browser | lib/client.js |
Hand-authored window.__ModuleLoader__.load({ id, factory }) bundle: widget, settings page, locale dictionaries |
| bundle patch | cordis.patch.yml |
Inserts the profile entry |
The browser bundle is composed by DSH's official client-modules scan from
dsh.client + exports["./client"]; it registers into
sidebar.footer.action and plugins.bundle.config, exactly like the reference
plugin.
The session fix wraps globalThis.fetch once and only adds
x-opencode-session when the request URL starts with the gateway base carried
by the current llm/stream call. The base comes from
llm-pi-ai.providers.<route>.baseURL, falling back to opencodeGoBaseUrl.
Compatibility
- DSH
0.1.7and above, including the0.2.0line; verified on0.2.1-rc.5. @deepseek-ai/schemastery^3.18.4(volatile fields / settings page).- Node.js ≥ 18 (global
fetch,AbortSignal.timeout). dsh-opencode-gois optional; its absence is a normal fallback.- Command Code's CLI auth file is optional if
COMMANDCODE_API_KEYis set. - The turn usage card uses the
shell.overlayslot plus thesessions/uiSessionclient services; a runtime without them loses only the card. - A DSH runtime that does not expose
plugins.bundle.configloses only the settings page; the sidebar widget and/provider-usagekeep working. Set the config fields in the profile patch or the auto-generated form instead.
Development
npm install
npm test
The tests are hermetic: a fake globalThis.fetch and a stub Cordis context, no
network and no real keys. They cover the schema, all three adapters, OpenCode
GO service integration/fallback, aggregation and error isolation, caching and
forced refresh, settings conflicts/whitelisting, the client bundle contract,
top-up balance discovery (credential and local auth.json) and failure
isolation, and the session-header injection. 60 tests, all offline.
License
MIT.
No comments yet. Be the first to write one.