Skip to content

Add an OpenCode usage collector to the agents panel - #12635

Closed
Leanesse wants to merge 1 commit into
omacom:quattrofrom
Leanesse:opencode-usage-collector
Closed

Leanesse wants to merge 1 commit into
omacom:quattrofrom
Leanesse:opencode-usage-collector

Conversation

@Leanesse

@Leanesse Leanesse commented Sep 20, 2026 •

Copy link
Copy Markdown

Why

The agents panel is one of the nicest parts of Omarchy, and OpenCode is one of the supported default agents (omarchy default agent opencode), but the usage panel has no OpenCode tab. If you have moved entirely to OpenCode (for example OpenCode Go with GLM), the panel keeps showing a stale Claude Code tab with a sign-in warning and there is no way to see what OpenCode actually consumes.

Omarchy already has the right architecture for this: the collectors and the agents panel are decoupled, and the plugin README documents that adding an agent is just shipping a collector. This PR fills the gap for OpenCode following exactly that contract.

What the collector does

  • Local stats — reads opencode's local message store. Current opencode versions keep every message in session_message (~/.local/share/opencode/opencode.db); older stores have the legacy message table with the same data inlined in the JSON. The collector detects which shape exists (and never reads both — migrated rows would count twice). Every assistant message with token usage feeds the tokens-by-day and tokens-by-model charts; every user message counts as one prompt.
  • Limits — when the machine is signed into OpenCode Go, the 5-hour rolling, weekly, and monthly dollar-window percentages come straight from Go's official GET https://opencode.ai/zen/go/v1/usage endpoint (Bearer auth). The record carries the same limit entries the claude collector produces, so the panel shows meters and "Resets in" times with no panel changes. Probe results are cached the way the claude collector caches its probe (15s reuse window; kept as the answer of record on failure, and only until each window rolls over).
  • Credential handling — the Go API key is read at runtime from the same auth.json opencode writes. It never lands in a record, a cache, or the printed JSON; it only ever appears in the Authorization header of the probe. One practical note: the endpoint answers plain Python-urllib agents with 403 while ordinary clients pass, so the probe sends a custom User-Agent.
  • Hero — the plan line reads Go when a live subscription is present, API otherwise (pay-per-token through other providers). An OpenCode mark ships as assets/opencode.svg plus a -light twin for light surfaces; the mark comes from opencode's MIT-licensed packages/identity/mark*.svg, with the filled backgrounds stripped to match the other marks.
  • Self-hiding stays intact — a machine that has never run OpenCode has no record, so the module behaves exactly like before.

Provider enablement defaults to true in the manifest, so the tab only appears when OpenCode was actually used.

How I tested

./test/all passes except 5 pre-existing environment failures that reproduce on a pristine checkout of quattro (config-test, locate-test, omarchy-kernel-migration-test, snapper-test, unowned-system-paths-test — they expect an omarchy-pkgs checkout and system dirs a clean machine does not have). On my machine the record is correct against a live OpenCode Go subscription; the screenshots below are from a real system running this collector.

Happy to adjust anything — record contract details, cache windows, naming, or the mark assets.

Screenshots

Agents panel showing OpenCode with Go limits

@llstrk

llstrk commented Sep 25, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

The collector's structure fits the agents-panel contract, but on stores written by current OpenCode clients it reports no local usage, and the Go meters misread low percentages. Five issues follow, the first two with the widest effect. All five also apply to the current quattro tip, which does not change the collectors or the agents plugin.

Verified (source review plus synthetic runs of the collector):

  • A machine with no OpenCode data and no auth.json gets no stats and no limits, so the tab stays hidden.
  • When session_message is populated, it is parsed correctly: prompts, sessions, reasoning folded into output, model labels, day buckets. Malformed and token-less rows are skipped.
  • opencode-go is preferred over other keys. The key appears only in the Authorization header, never in stdout or any cache file.
  • Window labels, resetsAt and the record fields match what Main.qml/Panel.qml consume and what the Claude collector produces.
  • The SVG marks match upstream packages/identity/mark.svg and mark-light.svg (MIT) with the background removed.

1. Local stats skip the message table that current OpenCode clients write

collect() (bin/omarchy-agent-usage-opencode:232-258) runs PRAGMA table_info(session_message) and picks a table from those columns alone:

Legacy store (OpenCode v1.2.0 to v1.14.33): only `message` exists
  table_info(session_message) -> no rows -> neither branch -> {}      (the `message` fallback is unreachable)

Current store (v1.14.34+, checked at v1.18.32)
  message          <- TUI, `opencode run`, default desktop sidecar (V1 prompt routes -> projector)
  session_message  <- only the V2 API (/api/session/:id/prompt); cleared by several migrations
  session_message has `type` -> scan session_message only -> 0 prompts, 0 tokens

In OpenCode v1.18.32, the V1 prompt path persists user and assistant messages through SessionV1.Event.MessageUpdated, and packages/core/src/session/projector.ts:260-272 writes them to message. session_message is written only by V2 session events. No migration copies message rows into session_message, and migrations such as 20260622170816_reset_v2_session_state run DELETE FROM session_message. In synthetic runs, a legacy-only store and a v1.18.32-shaped store (populated message, empty session_message) both produced zero stats and a hidden tab. The Claude collector's opencode scan of the same stores found its Anthropic row.

Impact: For typical OpenCode use (TUI, desktop app or opencode run), the new tab shows no local usage. Without a Go key the tab never appears. With Go it shows meters over empty charts.

Suggested change: Read message as the primary source, using the v1 JSON fields (role, time.created, modelID, providerID, tokens.*), as the Claude and Codex collectors already do. Add session_message rows where present, keyed by message id. In v1.18.32 the two write paths are separate, so a union does not double count. Once message is read, OpenCode usage on Anthropic or OpenAI providers appears in both that provider's tab and the OpenCode tab. The panel never sums tabs, but a README sentence would clarify the attribution.

2. A Go window at 1% is shown as 100%

probe_limits() line 341 treats all values as fractions unless one of them is > 1. The Go endpoint returns whole-number percents (Math.floor(Math.min(100, …)) in OpenCode's packages/console/core/src/subscription.ts). The Claude collector uses >= 1 for exactly this case.

Go API percent (rolling, weekly, monthly) Collector output Panel (from Panel.qml source)
1, 0, 0 1.0, 0.0, 0.0 Session 100%, alarm state (≥ 0.9)
0, 1, 1 0.0, 1.0, 1.0 Weekly and Monthly 100%
2, 1, 0 (control) 0.02, 0.01, 0.0 correct

Impact: A light user sees full quota meters, the fullest window becomes the headline, and the bar icon turns urgent.

Suggested change: Divide by 100 unconditionally for this endpoint.

3. --limits-only reuses the whole cached record for up to 900 s

main() (lines 422-448) caches the complete record in opencode-scan.json, including limits and retryAdvised, and returns it for 900 s under --limits-only. collect_limits() never runs on a cache hit. The Claude collector caches only the local scan and always calls collect_limits().

t0        normal run, endpoint unreachable   -> no limits, retryAdvised
t0 + 30s  --limits-only (panel retry), endpoint healthy -> cached record, 0 probes, retryAdvised still set
          ... repeats every 30 s until the scan cache expires or a normal refresh runs

Reproduced with a stubbed endpoint. A later change from 40% to 90% usage was also not picked up by --limits-only. The panel runs --limits-only when opened and for its 30 s retry (from Main.qml/Panel.qml source).

Impact: A network failure at login can leave Go limits missing for up to 15 minutes instead of about 30 s. Opening the panel can show limits up to 15 minutes old.

Suggested change: Cache only the local scan, as the Claude collector does, and always call collect_limits(). SCAN_REUSE_SECONDS and LIMITS_ONLY_REUSE_SECONDS are defined but unused, and main() hardcodes 20 and 900.

4. Cached Go windows hide a rejected or removed key

When the probe returns 401 or 403, or the Go key is removed from auth.json, collect_limits() (lines 383-413) returns the cached windows that have not reset yet. It skips setting usageStatusText and authHelpText, and the probe cache is never cleared. main() then sets tierLabel to Go because limits are non-empty (line 440). In synthetic runs with an open cache, 401 and 403 both produced Go, three frozen meters and empty status text. Without a cache, the same responses showed "OpenCode limits unavailable" with the matching help text.

Impact: After a key is revoked or a Go subscription ends, the panel keeps showing "Go" and last-known meters with no warning, up to about a month for the monthly window. The public endpoint source treats 403 as a definite "OpenCode Go subscription required" answer.

Suggested change: On 401/403, set usageStatusText and authHelpText even while showing cached windows, or drop the fallback on 403. Derive Go from a fresh result or the saved key rather than from cached limits alone.

5. A Zen-only key is used to probe Go

access_token() (lines 269-285) falls back to any auth.json entry whose name contains "opencode". OpenCode stores a Zen key under opencode and a Go key under opencode-go (packages/tui/src/component/dialog-provider.tsx, packages/opencode/src/auth/index.ts). In OpenCode's public endpoint source, a key without a Go subscription gets 403. With a stubbed 403, the record showed "OpenCode limits unavailable" and "The saved API key has no OpenCode Go subscription behind it."

Impact: A Zen-only user sees an "OpenCode limits unavailable" warning, which contradicts the README text added here: it says users without Go get no limits section. While finding 1 stands, such a user's tab is usually hidden anyway.

Suggested change: Probe only with opencode-go, or treat a 403 on a fallback key as "no Go" without a warning.

Optional test improvement: The Claude, Codex and Fireworks collectors each have scanner or limits tests under test/shell.d/, and this collector has none. A scanner case with populated message rows and a limits case with percent: 1 reproduce findings 1 and 2 in the synthetic runs above, so they would catch those regressions.


Review information

Test scope: Source review of the collector, the agents panel (Main.qml, Panel.qml) and the sibling collectors at the PR head, with the later quattro tip checked for changes to these paths. OpenCode upstream source read at release v1.18.32, with earlier releases checked for the schema history. The Go usage endpoint's public source was read as well. The collector ran only in isolated synthetic fixtures: synthetic SQLite stores built from upstream DDL, fake keys and a stubbed endpoint. No OpenCode binary, live Go account or network request was used, so the author's live-system result was not reproduced. Panel effects come from QML source, not desktop rendering. Responses for keys routed through OpenCode's inference proxy (for example oc_sk_ keys) are not established by public source. ./test/all was not run.

AI process: Opus 5.5 Medium coordination and synthesis, independent Opus 5.5 Xhigh and GPT 6 Sol Xhigh technical assessments with targeted follow-up questions, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@dhh

dhh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thanks! OpenCode usage is coming in through #14040, which counts only opencode's own providers so nothing doubles with the Claude and Codex collectors. Closing in favor of that.

@dhh dhh closed this Oct 2, 2026
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