Add OpenCode Zen/Go local usage to usage panel - #6526
Conversation
Adds an OpenCode provider to the model-usage bar plugin that merges local usage from Pi session transcripts (Zen/Go split by provider tag) with the opencode CLI database, with no remote balance or limit touches. The panel gains a Both/Zen/Go sub-toggle (keys 1/2/3) that filters the daily chart and model breakdown to the selected route; older untagged CLI sessions count toward the merged view only. Route data travels through cross-device sync snapshots. Ships a scanner, provider adapter, icon assets, docs, and a scanner test.
…oute The OpenCode sub-toggle now lists Zen first, then Go, then Both, defaults to Zen (with ordinal fallback when a route has no data), and the hero plan line reads API for Zen, Subscription for Go, and Subscription + API for the merged view.
- Reset no longer yanks a user-selected Both view back to Zen on every scan; Both (the merged view) always exists, and single-route views are judged against the displayed (synced-aware) route set. - Displayed active routes now come from synced stats when sync is on, so the Zen/Go toggle works on machines with no local OpenCode usage. - Per-route merge shares the whole-provider merge helper and carries the full record shape (todayPrompts, todaySessions, totalPrompts, activeDates), fixing prompts/sessions showing 0 on synced devices. - Route chips ordered Zen / Go / Both per the README and commit message; route ids live in a single catalog on the provider. - Scanner test derives fixture dates dynamically (the scanner only buckets the trailing 7 days), matching the codex test pattern.
Pi records a provider tag per message, but the scanner only routed the opencode family - messages tagged fireworks, anthropic, openai-codex or anything else were dropped entirely, so a day spent in a non-opencode provider showed 0 tokens in the merged view. The scanner now reads any line carrying a provider tag and adds usage from providers outside the OpenCode family to the merged (both) view only, never to a zen/go route bucket. Also expects the rg prefilter to match any provider instead of the three opencode spellings.
Reverts the merged OpenCode view to OpenCode-only providers: messages Pi tags with any other provider (fireworks, anthropic, openai-codex, ...) are dropped again instead of counting toward the merged view. The rg prefilter matches only the opencode provider spellings, and route-less messages skip token parsing entirely.
|
Screenshot of the OpenCode view: Tried to mirror approach from Claude, Codex, and Fireworks (via PR #6488). |
There was a problem hiding this comment.
🟡 Not ready to approve
The database query cannot read actual OpenCode tokens, and route-selection edge cases can hide available usage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds OpenCode Zen/Go local usage scanning, route filtering, synchronization, and panel presentation.
Changes:
- Scans Pi transcripts and the OpenCode database.
- Adds Zen, Go, and merged usage views.
- Extends synchronization, documentation, assets, and tests.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
test/shell.d/model-usage-opencode-scanner-test.sh |
Tests scanner aggregation and route separation. |
shell/plugins/model-usage/scripts/opencode_usage_scanner.py |
Scans Pi and OpenCode usage. |
shell/plugins/model-usage/README.md |
Documents OpenCode behavior. |
shell/plugins/model-usage/providers/OpenCode.qml |
Adds the provider adapter. |
shell/plugins/model-usage/Panel.qml |
Adds route selection and presentation. |
shell/plugins/model-usage/manifest.json |
Enables and describes OpenCode. |
shell/plugins/model-usage/Main.qml |
Integrates provider state and synchronization. |
shell/plugins/model-usage/assets/opencode.svg |
Adds the dark-surface logo. |
shell/plugins/model-usage/assets/opencode-light.svg |
Adds the light-surface logo. |
Review details
- Files reviewed: 7/9 changed files
- Comments generated: 6
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
- kill and reap the opencode child when the db query times out so refreshes cannot pile up processes - show the Zen/Go/Both route switch as soon as any attributed route has data, so the merged view stays reachable when one route coexists with un-attributed CLI usage - re-home the OpenCode sub-view (opencodeSubViewReset) when synced aggregate data changes, not only when the local scanner's route set changes - document that only OpenCode-tagged Pi transcripts feed the panel, and correct the Panel comment naming the merged view the default (the real default is Zen)
There was a problem hiding this comment.
🟡 Not ready to approve
Database aggregation misattributes resumed-session usage and omits reasoning tokens.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:251
- The daily buckets group each session's cumulative token total by
time_created. If an existing OpenCode session is resumed on a later day, all newly consumed tokens remain attributed to the original creation day, so “today” and the seven-day chart are guaranteed to be wrong for resumed sessions. Aggregate usage records by their own timestamps (for example, from message/part data) and count distinct session IDs per day instead.
"SELECT date(time_created/1000,'unixepoch','localtime') AS day, "
"count(*) AS sessions, "
"sum(tokens_input) AS input, sum(tokens_output) AS output, "
"sum(tokens_cache_read) AS cache_read, sum(tokens_cache_write) AS cache_write "
"FROM session GROUP BY day ORDER BY day"
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:250
- The session schema also exposes
tokens_reasoning, but this aggregation omits it and later totals only input/output/cache fields. Reasoning-heavy OpenCode sessions will therefore be under-reported. Include the reasoning sum in the query and fold it into the token total (or expose a dedicated bucket).
"sum(tokens_input) AS input, sum(tokens_output) AS output, "
"sum(tokens_cache_read) AS cache_read, sum(tokens_cache_write) AS cache_write "
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
…reasoning tokens The CLI-db scan bucketed each session's cumulative token totals under time_created, so a session resumed on a later day misattributed its usage to the creation day, and tokens_reasoning was omitted entirely. Aggregate assistant messages by their own time_created, counting distinct session IDs per day, and fold reasoning tokens into the day/message totals.
|
Addressed both suppressed review points in Resumed-session attribution — the CLI-db scan previously bucketed each session's cumulative SELECT date(m.time_created/1000,'unixepoch','localtime') AS day,
count(DISTINCT m.session_id) AS sessions,
sum(json_extract(m.data,'$.tokens.input')) AS input,
...
FROM message m
WHERE json_extract(m.data,'$.role') = 'assistant'
GROUP BY day ORDER BY dayVerified against a live opencode DB: every session has message rows, and full-history message sums match the Reasoning tokens — Both changes are covered by the updated scanner test (fixture now includes a reasoning component, merged-day total updated accordingly); the full scanner suite passes 8/8. |
There was a problem hiding this comment.
🟡 Not ready to approve
CLI reasoning tokens are double-counted, and sessions spanning multiple dates inflate the total session count.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:125
- OpenCode stores
tokens.reasoningas the reasoning-token detail withintokens.output, so adding both inflates every CLI day containing reasoning. This is the same subset relationship handled by the Codex scanner. Compute the total from input, output, and cache fields only; the fixture's merged expectation should consequently be 4,450 rather than 4,700.
# Reasoning tokens are part of the billed total, so they fold into the
# day's messageCount just like input/output/cache do.
total = input_tokens + output_tokens + reasoning_tokens + cache_read + cache_write
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:302
- These synthetic keys are scoped by day, so one OpenCode session with assistant messages on two dates is counted twice in
totalSessions, unlike Pi sessions and the other scanners, which deduplicate by the real session identity. Return/group bym.session_idand use that ID as the key while still aggregating tokens per day.
session_keys = ["cli:" + day + ":" + str(index) for index in range(sessions)]
both.add_db_day(day, session_keys, input_tokens, output_tokens, reasoning_tokens, cache_read, cache_write)
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
The CLI-db scan keyed each day's sessions by an index, so one session with assistant messages on two dates counted twice in totalSessions. Aggregate distinct session ids per day and use them as the keys, keeping per-day token attribution.
There was a problem hiding this comment.
🟡 Not ready to approve
Native OpenCode database metadata is discarded, preventing CLI usage from appearing in Zen/Go route views.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:256
- Current OpenCode assistant rows carry
providerIDandmodelIDinm.data, but this aggregation discards both by grouping only on the day. As a result, all native OpenCode CLI tokens are added only toboth; CLI-only Zen/Go users get no route switch, and the per-route/model views omit their usage. Select and group by those fields, route recognized providers into Zen/Go while keeping untagged legacy rows merged-only, and extend the fixture for both cases.
"SELECT date(m.time_created/1000,'unixepoch','localtime') AS day, "
"group_concat(DISTINCT m.session_id) AS session_ids, "
"sum(json_extract(m.data,'$.tokens.input')) AS input, "
"sum(json_extract(m.data,'$.tokens.output')) AS output, "
"sum(json_extract(m.data,'$.tokens.reasoning')) AS reasoning, "
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
The CLI-db scan grouped assistant messages by day only, so all opencode CLI usage landed in the merged view and CLI-only users never got a route switch. Each message carries providerID (opencode = Zen, opencode-go = Go) and modelID, so group by route+model, feed zen/go buckets alongside both, and attribute CLI tokens to their model. BYOK providers (anthropic, openai, llama.cpp) and legacy rows without a provider tag stay merged-only.
There was a problem hiding this comment.
🟡 Not ready to approve
OpenCode reasoning tokens are omitted from the per-model breakdown, producing inconsistent totals.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
shell/plugins/model-usage/scripts/opencode_usage_scanner.py:142
- Reasoning tokens are included in
messageCount,todayTotalTokens, andtodayTokensByModel, but omitted frommodelUsage. As a result, the panel's “Tokens by model” totals undercount OpenCode CLI usage (the fixture's Zen model shows 3,500 while the day shows 3,750) and can rank models incorrectly. Fold reasoning into the output bucket, since the shared model schema has no separate reasoning field.
bucket["inputTokens"] += input_tokens
bucket["outputTokens"] += output_tokens
bucket["cacheReadInputTokens"] += cache_read
bucket["cacheCreationInputTokens"] += cache_write
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
modelUsage speaks a 4-field schema (input/output/cache) with no reasoning slot, but add_db_day left the separate reasoning counter out of it while day/today totals included it, so Tokens by model undercounted CLI usage. Fold reasoning into the output bucket so per-model totals match day rows; the sync merge and panel already sum the same 4 fields.
There was a problem hiding this comment.
🟡 Not ready to approve
Sync initialization can replace the documented Zen default with Both before remote route data loads.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
shell/plugins/model-usage/Main.qml:200
- When sync is enabled on a machine with no local routed usage, the local scan can run before the first aggregate arrives and reset the initial
zenvalue toboth. Once synced Zen data arrives, this early return preservesboth, so the panel does not use the documented Zen default. Track whetherbothwas explicitly selected, or defer the no-route fallback until the initial sync completes.
function opencodeSubViewReset() {
var routes = root.displayActiveRoutes()
// "both" (the merged view) always exists, so it survives any scan;
// single-route views survive while their route has data in the displayed set.
if (root.opencodeSubView === "both" || routes.indexOf(root.opencodeSubView) >= 0)
return
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
An empty local scan (sync-enabled box before the first aggregate arrives, or usage that appears after a quiet first scan) reset the documented Zen default to both, and the early return then pinned it there forever even after Zen route data arrived. Track whether the user explicitly picked the current view, so a fallback to both is never mistaken for a choice.
There was a problem hiding this comment.
🟡 Not ready to approve
Automatic route fallback can become incorrectly sticky, and test timestamps are timezone-dependent.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
test/shell.d/model-usage-opencode-scanner-test.sh:91
- Like the Zen fixture, this fixed
11:00Ztimestamp can resolve outside localDAY(for example, the next day in UTC+14), making the Go route and merged-today assertions timezone-dependent. Emit an offset-aware timestamp for localDAYbefore writing it.
{"type":"session","version":3,"id":"go-ses","timestamp":"${DAY}T11:00:00Z","cwd":"/demo"}
shell/plugins/model-usage/Main.qml:208
- An automatic fallback does not clear
opencodeSubViewExplicit. For example, after a user selects Go, if all attributed route data disappears this function changes the view tobothbut leaves the flag true; when route data returns, line 204 then treats that fallback as an explicit Both choice and never restores the Zen/Go default. Clear the flag whenever this function changes an invalid view automatically.
if (root.opencodeSubView === "both" && root.opencodeSubViewExplicit) return
if (routes.indexOf(root.opencodeSubView) >= 0) return
if (routes.indexOf("zen") >= 0) root.opencodeSubView = "zen"
else if (routes.indexOf("go") >= 0) root.opencodeSubView = "go"
else root.opencodeSubView = "both"
test/shell.d/model-usage-opencode-scanner-test.sh:87
- This fixture combines a local
DAYwith a fixed UTC time, so it is not always onDAY: at UTC-11,10:00Zis the previous local date, and at UTC+14 it is the next date. The “today” totals therefore fail solely based on the machine timezone. Emit an offset-aware timestamp for a local time onDAYinstead.
This issue also appears on line 91 of the same file.
{"type":"session","version":3,"id":"zen-ses","timestamp":"${DAY}T10:00:00Z","cwd":"/demo"}
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
opencodeSubViewReset now clears the explicit-flag when re-homing an invalid view automatically, so a fallback to Both after route data vanishes is never mistaken for a user choice and the Zen default is restored once route data returns. The scanner test fixtures used fixed UTC timestamps that drift onto yesterday/tomorrow in extreme timezones (UTC-11, UTC+14); they now emit offset-aware local stamps (%:z) that stay on the local day everywhere.
There was a problem hiding this comment.
🟡 Not ready to approve
The database test stub does not validate the SQL contract, allowing scanner query regressions to pass unnoticed.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
test/shell.d/model-usage-opencode-scanner-test.sh:28
- The fake CLI returns fixture rows for every
dbinvocation, so this test never validates the SQL contract it is meant to cover. A regression to the wrong table or token JSON path would still pass even though real CLI usage disappears. Make the stub reject unexpected query text/arguments (or execute the query against a fixture database) so the scanner’s message-table query is exercised.
cat >"$TEST_HOME/bin/opencode" <<EOF
#!/bin/bash
if [[ \$1 == "db" ]]; then
- Files reviewed: 7/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Summary
~/.pi/agent/sessionstranscripts (zen/go split by provider tag) and the opencode CLI session database for per-route tokensTesting
omarchy-shell omarchy.model-usage refreshNotes
OpenCode Go usage isn't available via API, so this is just showing OpenCode and Pi sessions: the panel reads only transcripts and sessions recorded on this machine, never the live Zen/Go balance from the web console (due to lack of public API availability).