An ASCII plan-usage gauge for the OpenCode 2 TUI — a single line in the
prompt footer (or a block in the session sidebar), plus a /usage command.
Zero config: the credential comes from OpenCode's own auth store.
Go 5h ════│8%────── ⟳3h26m · Mon ═══════│44%── · 18d ═══════│42%──
Requires OpenCode 2 (v2.0.18+). This is a V2 CLI plugin: it uses the V2 plugin API, the V2 slot tree (
prompt.footer.status,sidebar.footer) and is configured incli.json. OpenCode 1 will not load it — V1 plugin implementations do not run in V2. Check withopencode --version.
Most plan-usage plugins ask you to go get a credential: paste a browser cookie, copy a workspace id, export an env var. This one doesn't. It reads the key you already connected to OpenCode, so for OpenCode Go it works the moment you install it.
- Zero configuration — no cookie, no workspace id, no config file, no secrets in your config.
- Always visible, zero context cost — one ASCII line appended to the prompt footer row. It never enters the conversation, so it can't pollute the model's context.
- Follows your model — the gauge tracks whichever provider the selected model belongs to.
- Pluggable providers — each billing API is a small adapter that normalizes to one shape. OpenCode Go ships today, plus an opt-in OpenRouter adapter (per-key cap, else account balance); OpenCode Zen, GitHub Copilot, Kiro and others are additive.
- No dependencies, no build step — TypeScript loaded directly by the host. 79 unit tests.
git clone https://github.com/dimalo/opencode-v2-usage-gauge
cd opencode-v2-usage-gauge && npm install # zero runtime deps; node_modules only for devWith options:
{
"plugins": [
{
"package": "opencode-v2-usage-gauge",
"options": { "placement": "sidebar" }
}
]
}Configure it in
cli.json, notopencode.json. Everything this plugin does is TUI-side, and the CLI only learns which plugins to load from the server'sPlugin.Infolist, whose schema (openapi.json) has nooptionsfield (additionalProperties: false). Options configured inopencode.jsontherefore arrive as{}incontext.options;cli.jsonis read by the CLI itself and does deliver them. Listing the plugin in both files loads the TUI entry twice and duplicates every slot claim.
Option B — drop the clone into the global discovery directory:
cp -R opencode-v2-usage-gauge ~/.config/opencode/plugins/opencode-v2-usage-gauge(Files, src/… paths and exports subpaths are NOT accepted for local
directory plugins — the loader resolves server/tui at the directory root.)
Then restart OpenCode (opencode service restart, and restart any open TUI
instances — the TUI entry loads per TUI process).
| Key | Values | Default | Meaning |
|---|---|---|---|
layout |
"single" | "multi" |
"single" |
One compact line, or a title plus one line per window |
showCountdown |
true | false |
true |
Show reset countdowns (⟳ on the line; reset … in multi/dialog) |
placement |
"promptFooter" | "sidebar" | "both" |
"promptFooter" |
Claim the prompt footer row, the session sidebar, or both. sidebar renders the multi-line variant |
maxWidth |
number of cells | 0 (auto) |
Cell budget for the single line. 0 = at most half the row (min 48), because the footer row is shared with the built-in cost/hint items |
providers |
"all" or a list of adapter ids |
["opencode-go"] |
Which providers the gauge tracks. The gauge follows the selected model, so this is a filter on top of "provider has an adapter". openrouter is opt-in — add it to the list or use "all" |
The options hold no secrets — credentials stay in OpenCode's auth store.
An adapter answers two questions for one OpenCode providerID: how to
authenticate, and what its billing API says. Everything else — the gauge, the
width math, the sidebar layout, the dialog — is provider-agnostic.
| Adapter id | Plan | Credential | Status |
|---|---|---|---|
opencode-go |
rolling / weekly / monthly windows | key from OpenCode's auth store | shipped |
openrouter |
per-key credit cap, else account balance (opt-in) | key from OpenCode's auth store | shipped |
openrouter is not in the default providers list: the cap and the balance
are both optional, so for many keys there is no bar to draw and the adapter would
only cost a request. Opt in with "providers": "all" or
"providers": ["opencode-go", "openrouter"].
The gauge is built from GET /api/v1/key, the endpoint OpenRouter's own docs
name for checking "the rate limit or credits left on an API key". It reports the
per-key spending cap (limit, limit_remaining, limit_reset); with
limit > 0 that maps exactly onto the window contract — 88% of $100 drawn as
a bar, labelled by cadence (day / wk / mo).
Without a per-key cap there is no denominator, so no bar is drawn. The
account's remaining credits are then read from GET /api/v1/credits, whose page
is titled "Get remaining credits" and returns the two counters you subtract:
total_credits - total_usage. That is shown as a plain balance 12.53 USD line
— a real number, never a percentage. If /credits does not answer, the key's
lifetime usage is shown instead as a measured amount (spent $17.10 USD · this key). If neither is available, the widget shows nothing at all.
Two honest caveats:
/api/v1/creditsis documented as management-key-only, but answers a normal inference key in practice (verified: HTTP 200 withis_management_key: false). It is used best-effort — if the restriction is ever enforced (403) or the schema drifts, the balance line simply disappears; nothing else breaks.limit_resetis a cadence, not a timestamp, and OpenRouter does not document the instant at which the cap resets — so no reset countdown is shown rather than an invented one. The deprecatedrate_limitobject is ignored.
Zen is pay-as-you-go, and its credit wallet has no API-key endpoint — every
/zen/v1/* usage or balance path returns 404, and the balance is only readable
from the web console behind a browser session. Upstream tracks this as
anomalyco/opencode#44189.
So there is no honest gauge to draw for Zen yet, and the plugin does not
pretend otherwise: a consumed amount is not a percentage, and a budget we cannot
read is not a budget of zero.
The groundwork is in place for when that endpoint ships: PlanUsage.spend
(consumption over a scope) sits beside PlanUsage.balance (remaining) in the
contract, and src/spend.ts is a tested, pure local meter that derives
credits-consumed from OpenCode's own per-message cost, scoped to the session
family. Registering Zen is then one adapter file plus one registry line.
Adding a provider is a single file in src/providers/ plus a line in
src/registry.ts:
export const ZEN_ADAPTER: PlanUsageAdapter = {
id: "opencode-zen",
label: "OpenCode Zen",
planKind: "balance", // windows | balance | both
async credential() { /* bearer | header | cookie */ },
async fetch(credential) { /* → PlanOutcome */ },
};Credentials are a union (bearer / header / cookie) on purpose: API-key
providers and OAuth/console providers need different things, and the interface
should not force a rewrite when the second kind arrives. Pay-as-you-go
providers report balance instead of windows; the gauge omits what a
provider does not have instead of rendering a lie.
opencode --versionreports1.x→ this plugin is OpenCode 2 only. V1 does not load V2 plugins.- Installed via
tui.json/ aplugin(singular) key → move the entry tocli.jsonplugins(V2 auto-migratestui.jsonfor you). - Configured correctly but silent → the gauge follows the selected model. Pick a model from a tracked provider (
/models)./usageworks regardless of the selected model. /usagesays no credentials → connect the provider first (/connect→ OpenCode Go). The plugin never invents or stores credentials.
Honest alternatives, all solving a slightly different problem:
- slkiser/opencode-quota — multi-provider quota and token tracking with toasts and slash commands.
- opencode-go-usage-tui — OpenCode Go quota plus per-model price/limit tables and change tracking (needs a console cookie).
- ColorlessBoy/opencode-go-quota —
/quotadialog with multi-plan key switching. - wiscaksono/opencode-usage — native macOS menu bar app for the same numbers.
npm run typecheck
npm testsrc/parser.ts, src/ansi.ts and src/snapshot.ts are ported from the
sibling pi extension; the layout logic is verbatim, with the accepted window
type widened so provider adapters can feed it. Everything provider-specific
lives in src/providers/.
See CONTRIBUTING.md — including how to add a provider adapter, the "never make the gauge lie, never crash the TUI" rule, and the test gate every PR (and every Dependabot bump) has to pass.
Security: credentials are read in memory and never persisted. Report vulnerabilities via private advisories.
MIT
{ "plugins": ["opencode-v2-usage-gauge"] }