Skip to content

feat(opencode): add rolling/weekly/monthly quota display via console scraping - #250

Open
achappell wants to merge 9 commits into
janekbaraniewski:mainfrom
achappell:feat/opencode-subscription-rpc
Open

feat(opencode): add rolling/weekly/monthly quota display via console scraping#250
achappell wants to merge 9 commits into
janekbaraniewski:mainfrom
achappell:feat/opencode-subscription-rpc

Conversation

@achappell

@achappell achappell commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds OpenCode Zen quota display (rolling 5h / weekly / monthly usage percentages, console balance, billing info) to the dashboard, sourced from the OpenCode console (browser-session auth), plus a handful of correctness fixes found while getting this working with multiple OpenCode accounts on the same machine.

Provider/subsystem touched: internal/providers/opencode, internal/providers/perplexity, internal/providers/claude_code, internal/telemetry.

What's included

  • internal/providers/opencode: new console_rpc.go client that scrapes the OpenCode Go usage page (HTML + embedded Seroval script) for billing and rolling/weekly/monthly usage-percent data, with an RPC-based fallback. Adds a monthly_usage_pct metric and a blue-themed dashboard widget.
  • Browser-session-only accounts (no Zen API key) are now supported — Fetch() no longer requires an API key when a browser cookie is configured.
  • Cross-account correctness: fixed several bugs that only show up with two OpenCode accounts on one machine (e.g. work + personal, different browsers):
    • Session storage no longer gets clobbered across sibling accounts (loadStoredSession reads directly from credentials.json instead of refreshing from whichever browser polled last).
    • A telemetry provider-scope fallback (internal/telemetry/usage_view.go) no longer leaks one account's local usage/message/token data onto a sibling account that shares the same provider ID but has no local telemetry of its own.
    • The same cross-account session-clobbering fix is mirrored in internal/providers/perplexity — it had the identical bug (same fixed cookie domain, multiple accounts, different browsers).
  • Correctness of the quota data itself:
    • A missing/expired session (both the page scrape and billing-RPC fallback failing) no longer gets silently swallowed and reported as $0.00 balance — it now surfaces as a real auth error.
    • A legitimate 0% usage reading (quota window just reset) is no longer indistinguishable from "not found" and dropped from the dashboard.
    • An account with neither a valid API key nor a working console session no longer falsely reports OK status.
  • Unrelated fix bundled in (found while testing the above): internal/providers/claude_code — the Keychain lookup for Claude Desktop's Chromium Safe Storage password used a stale account name ("Claude") that no longer matches current Claude Desktop builds ("Claude Key"), which silently broke the 5h/7d usage-percent metrics for Claude Code. Now tries both.
  • Monthly gauge was silently hidden: DefaultDashboard caps gauge rendering to 2 lines unless a widget opts into more. OpenCode's gauge priority lists three usage-window percentages (5h / 7d / monthly), so the 2-line default meant monthly_usage_pct was always bumped by the two shorter windows even though it was already being fetched correctly. Raised GaugeMaxLines to 3 (matches the existing ollama provider's precedent) so all three render.

User-visible changes

  • New dashboard metrics for OpenCode: rolling_usage, weekly_usage, monthly_usage_pct, console_balance, monthly_limit, plus billing attributes (subscription plan, payment method) — all three usage-window gauges (5h/7d/monthly) are now visible on the tile, not just the first two.
  • OpenCode accounts configured with only a browser session (no OPENCODE_API_KEY) now work end-to-end instead of requiring a key.
  • Claude Code's 5-hour/7-day usage-percent gauges will populate again for users on current Claude Desktop builds (previously silently fell back to Cache Hit % / cost-only display).
  • No config schema changes — existing browser_cookie account config is reused as-is.

Testing

  • make fmt vet test run locally. All green except three pre-existing, environment-dependent failures unrelated to this PR (hermes/zed TestProvider_Fetch_MissingDB, which pick up real local DB files, and an opencode auth_scope assertion that depends on real local credentials rather than a proper test double) — confirmed via git stash that all three exist on main before this branch's changes.
  • New/updated unit tests: console_rpc_test.go (Seroval/HTML parsing, including a 0%-usage regression test), provider_test.go (browser-session-only auth flow, double-failure error propagation, status fallback), usage_view_test.go (cross-account telemetry leak regression).
  • Manually verified end-to-end against two real OpenCode accounts (different browsers) and a real Claude Code / Claude Desktop install.
  • GaugeMaxLines change is a widget-config constant, not new logic — the underlying monthly_usage_pct data path was already covered by existing/added tests; verified the rendering fix visually via make demo-style TUI capture rather than a new unit test.

No docs changes — no new config keys were introduced.

@janekbaraniewski
janekbaraniewski force-pushed the feat/opencode-subscription-rpc branch from 1fbe312 to 595d12e Compare July 15, 2026 08:59
@janekbaraniewski

Copy link
Copy Markdown
Owner

Thanks for this, and sorry for the long silence.

Could you rebase onto main? This now conflicts on internal/providers/claude_code/usage_api.go67fc0b8 (OAuth usage-API fallback) and d37d68b (refactor unifying usage-API auth) have both landed there since you opened this. Your last CI run was fully green, but it predates those commits, so it isn't current signal any more.

One request to make review easier: this bundles several changes that aren't really about OpenCode quota display — the cross-account session-clobbering fix, the claude_code Keychain key-name fix, and the GaugeMaxLines bump. Those look independently useful and would likely land much faster as their own small PRs. Would you be up for splitting them out?

@achappell

Copy link
Copy Markdown
Contributor Author

Yep, can do!

@achappell

Copy link
Copy Markdown
Contributor Author

Good call — split out the two cleanly-separable pieces:

Both are self-contained diffs against current main, not this branch, so they can land independently of the OpenCode quota work here.

On the third item — the GaugeMaxLines bump — it's not actually separable: it's one option inside the providerbase.DefaultDashboard(...) block this PR adds for OpenCode, and OpenCode has no pre-existing Dashboard config to bump from. Pulling it out would mean shipping an empty PR that sets a knob on a Dashboard block that doesn't exist yet on main. I've left it here since it's genuinely part of this feature (it's what makes the third gauge line — monthly usage — actually render).

Once #299 and #300 merge, I'll rebase this branch — since this PR already contains the identical fix commits, git will dedupe them against the merged main and the diff here will shrink down to just the OpenCode-specific console-scraping / browser-session-only-auth / gauge-display work. I've also just pushed a merge with current main so this is unblocked from the earlier CONFLICTING state in the meantime.

@janekbaraniewski

Copy link
Copy Markdown
Owner

Reviewed. The substance here is good, and the cross-account bugs you found are real — I confirmed the telemetry provider-scope leak independently while reviewing #300.

The blocker is overlap, not quality. This branch contains the same fixes as two of your own smaller PRs:

Splitting those out was the right call, and it means the review order should be #299#300 → this one. Both of those are now green and queued to merge (I resolved a perplexity.go conflict on #300 where main had grown a test against the seam this branch removes; the resolution keeps a stubbable loadStoredSession seam so both intents survive).

Once they land, this branch's remaining unique content is the part I actually want to review on its own merits:

  • the console_rpc.go page-scrape + Seroval parse with RPC fallback
  • browser-session-only accounts (no Zen API key)
  • the correctness fixes around the quota data itself — the swallowed auth error reported as $0.00, a legitimate 0% reading being dropped as "not found", and a false OK with neither key nor session
  • GaugeMaxLines raised to 3 so monthly_usage_pct renders

What I'd like from you: after #299 and #300 merge, rebase this on main and drop the now-duplicated commits. That should also clear the current perplexity.go conflict for free, since the resolution will already be in main. If you'd rather I do the rebase, say so and I'll push it to your branch — you have maintainer edits enabled.

One reviewable note for when it comes back: the scrape of the Go usage page (HTML + embedded Seroval) is inherently fragile against a console redesign. The RPC fallback helps, but I'd want the failure mode to be an explicit degraded status rather than a silent zero — which is exactly the class of bug you already fixed here for the auth case, so I think we agree on the principle. Worth a test that a shape change surfaces as AUTH/ERROR and not as valid-looking data.

Thanks for splitting out the small fixes — that's what made them mergeable this week.

…isplay

- Add workspace discovery via workspaces RPC (def39973...)
- Add subscription.get RPC (7abeebee...) for rolling 5h and weekly usage %
- Fetch billing + subscription concurrently in enrichFromConsole
- New metrics: rolling_usage, weekly_usage with reset timestamps
- Dashboard widget: blue theme, gauge priority for quota metrics
- Regex fallback parsing matching CodexBar's strategy
- 8 new tests covering round-trips, fallbacks, and workspace parsing
The individual RPC endpoints (subscription.get, billing.get) have
content-hash IDs that rotate on every backend deploy. The Go usage
page at /workspace/{id}/go embeds both billing.get and
lite.subscription.get data in a Seroval script blob that's stable
across deploys.

- Add FetchGoUsagePage() that scrapes the HTML page
- Parse embedded Seroval script for billing + subscription data
- Extract rolling 5h, weekly, and monthly usage percentages
- Fallback to individual RPC calls if page scraping fails
- Add monthly_usage_pct metric (49% on test account)
- Update dashboard widget with monthly gauge
Accounts configured with only browser_session auth were showing
AUTH_REQUIRED because RequireAPIKey() returned before trying the
console enrichment path. Now:
- Skip Zen API probe when no key but browser cookie is configured
- Fall through to enrichFromConsole for quota/billing data
- Message shows 'Auth OK' when browser session succeeds without API key
…nsole failure

- Fetch() no longer defaults to StatusOK when an account has neither a
  validated Zen API key nor a working console/browser session — it now
  surfaces the underlying auth-required status instead.
- enrichFromConsole no longer swallows errors when both the HTML usage-page
  scrape and the billing-RPC fallback fail; it returns the error instead of
  writing zero-valued billing metrics into the snapshot as if they were real.
- Rolling/weekly/monthly usage percentages now use presence flags
  (RollingUsageOK etc.) instead of a `> 0` check, so a legitimate 0% reading
  is no longer indistinguishable from "field not found" and dropped.
- loadStoredSession is now a stubbable var (matching the existing
  loadBrowserSession/newConsoleClient seams), and the console-enrichment
  test now stubs the seam actually on the call path — it was previously
  stubbing the now-unused loadBrowserSession, so it passed only by
  accident against whatever real credentials.json happened to exist.
DefaultDashboard caps gauge rendering to 2 lines unless a widget opts into
more. OpenCode's gauge priority lists three usage-window percentages
(rolling_usage, weekly_usage, monthly_usage_pct), so the 2-line default
meant monthly_usage_pct was always bumped by the two shorter windows even
though it was already being fetched and had real data. Raise
GaugeMaxLines to 3, matching the existing ollama precedent.
The "resets Xh Ym · Y% by reset" annotation claude_code's tiles show was
silently never rendering for opencode, for two stacked reasons:

- gaugeWindowDuration only recognized "5h"/"7d"/"30d"/etc., not the
  "rolling-5h"/"month" window strings core.Metric's own doc comment
  documents as valid — opencode uses exactly those.
- tileGaugeProjectionAnnotation only checked snap.Resets[key] (the bare
  metric key, matching claude_code's convention), never the
  "<key>_reset" suffixed convention copilot and opencode already use.

Also fixes an inconsistency in opencode's own reset-key naming:
monthly_usage_pct's reset was stored as "monthly_usage_reset" instead of
"monthly_usage_pct_reset", unlike rolling_usage/weekly_usage which
correctly followed the "<metric key>_reset" pattern.
computeDisplayInfoRaw only recognized claude_code's metric naming
(usage_five_hour/usage_seven_day) for the "Usage" tag. opencode's
differently-named quota metrics (rolling_usage/weekly_usage/
monthly_usage_pct) fell through to the cost-based today_api_cost branch
whenever local telemetry data was also present, tagging the tile
"Credits" instead of "Usage" even though real quota-window data was
available.
@achappell
achappell force-pushed the feat/opencode-subscription-rpc branch from fff7480 to 5284191 Compare August 31, 2026 19:59
@achappell

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. I’ve addressed the comments
already in this PR by rebasing onto upstream main,
resolving the merge conflicts, hardening the OpenCode Go
usage-page parsing against malformed or changed data, and
adding coverage for those failure cases. I also updated
the provider messaging to refer to OpenCode Go where
appropriate.

While reviewing this more closely, I found a larger
issue: the current implementation treats OpenCode Zen and
OpenCode Go as the same account, even though they are
separate catalogs. In particular, opencode-go
credentials can be collapsed into the opencode account,
model probing uses the Zen endpoint, while console
enrichment reads the Go usage page. That could produce
incorrect behavior for someone using both accounts.

I have a broader fix locally, but haven’t pushed it yet.
It preserves opencode and opencode-go as separate
accounts, uses the appropriate model endpoint for each,
limits console/subscription enrichment to Go, and updates
detection, the TUI, tests, and documentation.

Since that expands the scope considerably, my preference
would be to merge the current workable changes as-is and
handle the Zen/Go account split in a follow-up PR. Does
that sound reasonable, or would you prefer me to include
the larger account-separation fix here?

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