Skip to content

Add OpenCode Zen/Go local usage to usage panel - #6526

Open
chipkoziara wants to merge 12 commits into
basecamp:quattrofrom
chipkoziara:opencode-zen-and-go-usage-panel
Open

Add OpenCode Zen/Go local usage to usage panel#6526
chipkoziara wants to merge 12 commits into
basecamp:quattrofrom
chipkoziara:opencode-zen-and-go-usage-panel

Conversation

@chipkoziara

Copy link
Copy Markdown

Summary

  • add OpenCode as another model-usage provider in the agent menu bar modal, with a Zen API / Go subscription usage split
  • scan ~/.pi/agent/sessions transcripts (zen/go split by provider tag) and the opencode CLI session database for per-route tokens
  • within the OpenCode view, add ability to shift between Zen / Go / Both to see token usage across each (defaulting to Zen)
  • The included usage from Pi will only be from OpenCode Zen/Go; Pi messages tagged with any other provider (fireworks, anthropic, openai-codex, ...) are excluded

Testing

  • bash test/shell.d/model-usage-opencode-scanner-test.sh
  • bash test/shell.d/model-usage-codex-scanner-test.sh
  • live scan against real Pi sessions + panel refresh via omarchy-shell omarchy.model-usage refresh

Notes

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).

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.
Copilot AI review requested due to automatic review settings August 3, 2026 00:47
@chipkoziara

Copy link
Copy Markdown
Author

Screenshot of the OpenCode view:
image

Tried to mirror approach from Claude, Codex, and Fireworks (via PR #6488).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to 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.

Comment thread shell/plugins/model-usage/scripts/opencode_usage_scanner.py Outdated
Comment thread shell/plugins/model-usage/scripts/opencode_usage_scanner.py
Comment thread shell/plugins/model-usage/Panel.qml
Comment thread shell/plugins/model-usage/Main.qml
Comment thread shell/plugins/model-usage/README.md Outdated
Comment thread shell/plugins/model-usage/Panel.qml Outdated
- 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)
Copilot AI review requested due to automatic review settings August 3, 2026 01:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Copilot AI review requested due to automatic review settings August 3, 2026 01:43
@chipkoziara

Copy link
Copy Markdown
Author

Addressed both suppressed review points in a3e44af2:

Resumed-session attribution — the CLI-db scan previously bucketed each session's cumulative tokens_* totals under the session's time_created, so a session resumed on a later day misattributed its usage to the creation day. The query now aggregates assistant messages by each message's own time_created:

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 day

Verified against a live opencode DB: every session has message rows, and full-history message sums match the session table's totals exactly across all five token columns, so nothing is lost by switching sources — only the per-day attribution changes (sessions that resumed on a later day now land on their actual usage days).

Reasoning tokenstokens_reasoning is now summed in the query and folded into the day/message token totals (the panel's model-usage buckets don't expose a separate reasoning category, so it counts toward the total like input/output/cache).

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.reasoning as the reasoning-token detail within tokens.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 by m.session_id and 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.
Copilot AI review requested due to automatic review settings August 3, 2026 01:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 providerID and modelID in m.data, but this aggregation discards both by grouping only on the day. As a result, all native OpenCode CLI tokens are added only to both; 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.
Copilot AI review requested due to automatic review settings August 3, 2026 02:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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, and todayTokensByModel, but omitted from modelUsage. 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.
Copilot AI review requested due to automatic review settings August 3, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 zen value to both. Once synced Zen data arrives, this early return preserves both, so the panel does not use the documented Zen default. Track whether both was 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.
Copilot AI review requested due to automatic review settings August 3, 2026 02:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:00Z timestamp can resolve outside local DAY (for example, the next day in UTC+14), making the Go route and merged-today assertions timezone-dependent. Emit an offset-aware timestamp for local DAY before 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 to both but 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 DAY with a fixed UTC time, so it is not always on DAY: at UTC-11, 10:00Z is 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 on DAY instead.

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.
Copilot AI review requested due to automatic review settings August 3, 2026 02:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 db invocation, 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants