Skip to content

Add an OpenCode agent usage collector - #13548

Closed
bitbonsai wants to merge 3 commits into
omacom:quattrofrom
bitbonsai:opencode-agent-usage
Closed

bitbonsai wants to merge 3 commits into
omacom:quattrofrom
bitbonsai:opencode-agent-usage

Conversation

@bitbonsai

@bitbonsai bitbonsai commented Sep 28, 2026 •

Copy link
Copy Markdown

OpenCode Go usage in the agents panel

The agents panel covers Claude Code, Codex, and Fireworks but not OpenCode, even though opencode is already a default agent (omarchy agent launches it, and omarchy-restart-opencode exists). This adds omarchy-agent-usage-opencode, built like the Codex collector.

Commit 1 scans local usage: pi and omp JSONL sessions on the opencode and opencode-go providers first, then opencode's own message store for sessions the CLI recorded itself, falling back to session_message when a build has migrated. It fills the same fields as the other collectors (today, last week, active days, per-model) and adds the OpenCode mark plus a scanner test.

Commit 2 reads the Zen Go meters from https://opencode.ai/zen/go/v1/usage: rolling (5h), weekly, monthly. The key comes from OPENCODE_API_KEY, then ~/.local/share/opencode/auth.json, then ~/.pi/agent/auth.json (pi spends the same subscription and stores it under the same provider ids). It adds a limits test and mentions the subscription in manual/17-ai.md and the plugin README.

Commit 3 hardens the collector. The Go probe is cached for 15 seconds and cached windows are dropped once they reset, so opening and closing the panel does not repeat the request. A failed probe keeps the last good numbers, 401 and 403 get separate messages, and {providerID, modelID} model objects plus the GLM acronym are handled.

Plain Zen (/zen/v1, the opencode provider) is pay-per-use and has no meters. Only /zen/go/v1/usage exists; every other usage and balance path returns 404. Without a key, local stats still show.

Tests: test/shell.d/agent-usage-opencode-scanner-test.sh (5 checks) and test/shell.d/agent-usage-opencode-limits-test.sh (12 checks) both pass. ./test/all reports only the pre-existing environment failures (omarchy-pkgs checkout, omasnap, browser focus, theme file).

Worth knowing before review:

  • The Zen usage endpoint is undocumented. If that blocks the PR, commit 2 can go and local stats can stay.
  • The endpoint is behind Cloudflare, which rejects urllib's default signature with a 1010 before the request arrives. The collector sends its own User-Agent.
  • Reading ~/.pi/agent/auth.json is the last-resort key source and can be dropped if it's unwelcome.
  • opencode 1.18 still writes chat messages to message, so session_message is only a fallback. Verified with a live opencode run.

Sums token usage on the opencode and opencode-go providers into the agents
panel's record contract: pi and omp JSONL transcripts first, because they can
spend the subscription without the opencode CLI ever running, then opencode's
own message database for sessions the CLI recorded itself.

The scanner and cache follow the Codex collector, so the record carries the
same today, last-week, active-day, and per-model fields the panel already
renders.
Read the rolling, weekly, and monthly meters from the Zen gateway's usage
endpoint. The key comes from OPENCODE_API_KEY, then opencode's own credential
store, then pi's: pi spends the same subscription and records it under the
same provider ids.

A transport failure tells the panel to retry shortly, and a missing key still
leaves the local token stats in place.
@bitbonsai

bitbonsai commented Sep 28, 2026 •

Copy link
Copy Markdown
Author

@dhh this is a third OpenCode collector PR, and the first two are still open, so I'd rather sort out the overlap than have it merged blind.

Three open PRs touch the same files:

What this PR does differently: one collector covers opencode and opencode-go; local stats prefer pi and omp sessions, since those can spend the subscription without the opencode CLI running, and read opencode's own message store with a session_message fallback; the Zen Go meters come from /zen/go/v1/usage and are cached for 15 seconds so repeated panel opens do not re-probe.

I checked #12635's session_message-first scan against a live opencode 1.18 run: assistant messages with token usage land in message, not session_message, so that ordering reads zero on the current release. This PR reads message first and only falls back.

I'm fine reworking this onto whichever direction you want, including turning it into a user-installable collector if #12353 lands first.

Read opencode's current `message` store first and fall back to
`session_message`, so a build that migrated does not silently read zero and a
store that has both cannot double count a session.

Cache the Go probe for 15 seconds, drop cached windows once they reset, and
keep the last good numbers when a probe fails, so opening and closing the
panel does not send a request every time. Tell a rejected key apart from a key
with no Go subscription behind it, accept the {providerID, modelID} model
object, and keep GLM capitalized.
@dadofsambonzuki

Copy link
Copy Markdown

Independent live verification from a machine that actually runs this, plus one finding on the plan label.

Verified on a live account, not fixtures

Ran your collector unchanged (a856e644) against a real ~/.local/share/opencode (Go subscription, opencode CLI history) and the live endpoint:

$ time python3 bin/omarchy-agent-usage-opencode
0.571s, exit 0
{"id":"opencode","name":"OpenCode","ready":true,"hasLocalStats":true,
 "tierLabel":"Go","usageStatusText":"","totalPrompts":732,"totalSessions":18,"activeDays":8,
 "limits":[{"label":"Rolling (5h)","percent":0.22,"resetsAt":"2026-09-30T11:20:47.701Z"},
           {"label":"Weekly (7-day)","percent":0.23,"resetsAt":"2026-10-05T00:00:00.000Z"},
           {"label":"Monthly","percent":0.28,"resetsAt":"2026-10-21T07:45:46.000Z"}],
 "modelUsage":{"big-pickle":{"inputTokens":2803206,"outputTokens":324944,"cacheReadInputTokens":48199872,"cacheCreationInputTokens":0},
               "union-alpha":{...}}}

Both of your test files pass here (17 ok and 5 ok, exit 0), in an isolated copy of the tree.

Two things that review the local-stats question raised on #12635: on this machine the message store is what the CLI wrote (739 assistant rows, every one providerID: "opencode"), your scan finds them, and the numbers are consistent across runs and with the cache. Worth noting for anyone comparing collectors: todayPrompts and the 7-day chart are empty here because that DB's last write is 27 Sep — the panel shows all-time totals over an empty week, which is the correct reading of a machine that stopped using the CLI.

The endpoint's percents are integers (19, 22, 27 live), so your fraction() division by 100 is right — the percent <= 1 → treat as fraction guess in some of the sibling PRs misreads a 1% window as 100%.

Also, for the Console split you describe in the README: GET https://opencode.ai/inference/go/v1/usage answers 200 with a plain API key here too, no x-opencode-org-id needed, with the same payload as /zen/go/v1/usage.

The plan label is an assertion, not a reading

tierLabel = "Go" (three call sites) is the one thing in the collector the data does not support:

$ curl -s -H "Authorization: Bearer $KEY" https://opencode.ai/zen/go/v1/usage
{"usage":{"rolling":{"status":"ok","percent":19,"resetsAt":"..."},
          "weekly":{"status":"ok","percent":22,"resetsAt":"..."},
          "monthly":{"status":"ok","percent":27,"resetsAt":"..."}}}

No plan field — and nothing else an API key authenticates against names one. All of these 404: /zen/go/v1/{subscription,plan,account,me,limits,credits,balance}, /zen/v1/usage|credits|balance, /inference/go/v1/{subscription,account,plan,limits}; the console that does know the subscription is session-gated. Go is $10/month and Go Plus is $40/month with the same models and higher per-model allowances, so a Go Plus user gets a panel that names a plan they are not on.

I did not want to open a competing collector, so the fix is a PR onto your branch: bitbonsai#1 (base opencode-agent-usage, MERGEABLE, three files).

  • plan_from_payload() reads a plan from the payload when the endpoint reports one (plan/planType/plan_name/tier/…, top level or under usage/subscription/account), so a future tier appears with no code change.
  • plan_label() resolution: a declared plan in ~/.config/omarchy/agents/opencode.json (the override file the Fireworks collector already reads) → the payload's plan → blank, which the panel renders as "Subscription" rather than naming a plan nobody observed.
  • The probe cache carries the plan with the windows, so a cache hit does not drop it, and a keyless run still takes a declared plan.
  • Five new cases in agent-usage-opencode-limits-test.sh and XDG_CONFIG_HOME sandboxed, so no case can read the operator's own override file and pass by accident.
case your head with #1
endpoint names a plan (Go Plus) "Go" "Go Plus"
config override (Go Plus) "Go" "Go Plus"
no source states a plan "Go" ""

Merging #1 into your branch (or cherry-picking it — it is one commit) keeps your PR as the one upstream sees; say the word if you would rather I take it elsewhere. ./test/shell on the patched tree: 288 scripts, 13 failing, each reproducing byte-identically on your head, so none of them is this change.

@dadofsambonzuki

Copy link
Copy Markdown

A design note that is worth recording here, since it is the same thread as the plan-label change.

The collector currently has to reconstruct per-model usage from client stores (opencode's own database, pi/omp transcripts, and other harnesses that spent the same subscription). That is a workaround: those stores are per-machine and per-client, so they miss usage from other machines and from harnesses whose database you do not know about, and the account-level percentages in /zen/go/v1/usage are the only numbers that are actually authoritative.

There are open feature requests for the endpoint that would replace all of it — anomalyco/opencode#43983 (expose Go usage history through the API key, whose own body notes the local database "does not represent server-side accounting, usage from other machines, or the server-side reconciliation"), #13497 (per-model breakdown and totals) and #44189 (balance in the usage response). The server-side usage table already carries model, provider, tokens, cost and keyID per event, so this is a matter of exposure rather than of data.

If such an endpoint appears, this collector should prefer it and treat the local scans as a fallback for the cases where it is unavailable (no key, no entitlement, no network): one authoritative source, no per-client scans to keep in sync, and no double counting when two stores hold the same session. I have raised the requirement on #43983 with the panel's needs; no change needed here today, just worth agreeing on the direction before more local scanners get written.

@dhh

dhh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thank you! Brought in with your authorship in #14040 as the single OpenCode agent (Zen and Go), with pieces from #12651 folded in and kept limits marked stale after a failed check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants