Repository navigation
Conversation
Collects local token stats from opencode's own database (assistant messages on the opencode-go provider) and the authoritative plan limits from GET /zen/go/v1/usage — the same rolling 5-hour, weekly, and monthly percentages the opencode.ai dashboard shows. Reads the key from OPENCODE_GO_API_KEY or the opencode-go entry of ~/.local/share/opencode/ auth.json, and accepts both the deployed response shape and the merged PR shape defensively. Probes are cached for 15s and kept as the answer of record on transient failure, with retryAdvised for the panel.
The official opencode-go glyph as a white mark for dark surfaces and a dark twin for light ones, following the codex.svg convention.
Fixture opencode database rows (today, older, wrong-provider, zero-token), both endpoint response shapes, percent scaling, rate-limited windows, key precedence, and failure paths keep local stats.
The hero meta line is the plan, never the product: claude shows its subscription tier and codex its planType there. "OpenCode Go" duplicated the Go plan, so the display name is now just OpenCode and tierLabel keeps the Go plan. Tests pin both fields.
The client swaps never exercise the real transport, so GoUsageClient.probe is now driven against a stubbed urllib.request.urlopen, the way the claude limits test stubs it: the request path and its bearer, accept, and user-agent headers are asserted, the deployed response shape is decoded, and a rejected key, a server status, and a transport failure each map to their distinct GoUsageError.
The per-value heuristic rendered 1% as a full meter and 0.5% as half of one: any value above 1 was divided by 100 while its neighbors below 1 were not, so one payload could mix scales. Detect the scale across the whole payload the way the claude collector does — any value reaching 1 means percentages — and normalize every window with it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A probe failure fell back to the cached limits forever, so a window whose reset time had long passed kept a dead meter on the panel, and a rejected API key hid behind that silent fallback with no status at all. Filter the cache through the same window-expiry check the claude collector uses, flag auth and transport failures on the error itself, and answer a rejected key with a "Sign-in rejected" status and the remedy. A missing key now keeps still-open windows under "Waiting for auth" too, matching claude's fallback handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The database holds every historical message, JSON included, so the scan now filters in SQL (LIKE gates plus json_valid/json_extract with providerID opencode-go) instead of parsing tens-of-MB blobs in Python. The stats cache gains a versioned envelope stamped with the scan date, negative-age and cross-day misses, and never persists a scan cut short by a database error; cache-layer failures degrade to a direct scan instead of a crash.
A versioned, dated envelope on first scan, corrupt-cache recovery, limits-only reuse with a force bypass, dated and future-dated misses, malformed opencode rows that must not abort the scan, an interrupted scan that must never be cached, and an unwritable cache root that falls back to a direct scan.
9583007 to
8b7bae6
Compare
opencode v2 resolves the opencode-go provider through the `opencode` (OpenCode Console) integration and moved usage to /inference/go, so all three stored keys went dead and the panel told a signed-in user to run `opencode auth login`. Try every way in and let the endpoint pick: OPENCODE_API_KEY, the SQLite opencode-go key, the auth.json key, then the Console OAuth session on its own endpoint carrying the workspace header — and only when that id is known, since a wrong one is a hard 403. An expired session now says so instead of claiming the sign-in was rejected.
8b7bae6 to
8e82bf3
Compare
Automated AI review
Verified: the collector reads V1 Four issues remain. The first affects which database is read. The others affect local token totals and how limits behave when the endpoint fails. Bare A leftover
|
| V2 rows | OpenCode stats.ts |
Collector |
|---|---|---|
Fork copies (projector.ts:130-222: new IDs, same data and time_created) |
Excluded: fork_session_id IS NULL OR message.time_created >= session.time_created (stats.ts:126-148) |
Counted again, because deduplication is by ID only. Synthetic fork: 2200 tokens vs 1100 |
compaction rows with model and tokens |
Added through UsageRecorded events (stats.ts:285-319) |
Dropped: the V2 query reads type = 'assistant' only (:210-216). Synthetic case: 110 counted, 52000 compaction tokens ignored |
OpenCode source cited from v2.0.14.
Impact: on V2, every fork adds its copied history again on the original days, and compaction usage is missing. The V1 path does count V1's compaction message, which is stored as an assistant message.
Suggested change: join session_v2 and apply the same fork filter as stats.ts, and include compaction rows that carry tokens for an opencode-go model.
429 and 5xx responses start a 30-second re-probe loop
probe() marks HTTP 429, any status of 500 or above, and empty or unparseable payloads as retryable. collect_limits then writes retryAdvised: true (:542-577, :736-748). The PR's README describes this as intended ("transport and transient server failures ask the panel to retry"). The panel side is built for a narrower case. Main.qml documents retryAdvised as the signal from a collector that "could not reach its limits endpoint at all", and answers it with a one-shot --limits-only rerun 30 seconds later, re-armed by each new record. The Claude collector sets the flag only for transport failures (bin/omarchy-agent-usage-claude:711-740). This collector's own comment at lines 733-736 also says a server that answered with an error status should not be pestered.
Impact: during a sustained rate limit or outage, the endpoint is probed about every 30 seconds instead of on the normal 15-minute refresh. Under a 429, this works against the rate limit.
Suggested change: set retryAdvised only for transport failures, or add a backoff for 429 and 5xx responses.
Open cached windows are lost on a forced refresh or after several refused credentials
The README says "A failed probe keeps open cached windows" (shell/plugins/agents/README.md:94-95). Two paths do not do that:
- Forced refresh: the panel's refresh button passes
--force, andfallback = [] if force else ...(:711) skips the cache entirely. Offline or during a 5xx, pressing refresh blanks the meters. This matches the README's "--forcebypasses both caches", but the Claude collector handles it differently: a forced run skips only the reuse window and still shows open cached windows when the probe fails. - Several credentials: each refused candidate (401 or entitlement 403) overwrites the saved
refusedresult (:737-739), and only the last one's fallback reaches the record (:759-760). If an earlier refused credential has open cached windows and the last one has none, no limits are shown. A later transport failure has the same effect (:740-747). The result depends on candidate order.
Impact: the meters show "limits unavailable" in cases where the cache still holds valid windows.
Suggested change: load cached windows before checking force, as the Claude collector does, and keep the first non-empty refused fallback instead of the last. Alternatively, narrow the README wording.
Optional test improvement: the first collector call in test/shell.d/agent-usage-opencode-go-scanner-test.sh clears OPENCODE_API_KEY but not OPENCODE_DB. In an isolated run with OPENCODE_DB pointed at a synthetic database, that first assertion failed. With a real database exported, the test would read local credentials and could send them to opencode.ai. Adding OPENCODE_DB="" to that call closes the gap.
Optional test improvement: the 15-second reuse assertion also passes with reuse disabled, because a failed probe falls back to the same cached window. Asserting the fake client's request count would tell the two apart.
Documentation: the edited paragraph in manual/17-ai.md is re-wrapped at about 80 columns. The repository's AGENTS.md asks for full lines in manual/.
Review information
Test scope: Source review of the pinned head against OpenCode source (tags v2.0.14 and v1.18.32, and the public Console server code for /zen/go/v1/usage). The PR test and synthetic scenario probes ran in a network-free sandbox with synthetic databases and credentials. No live endpoint, real OpenCode installation or panel UI was tested. The Console path /inference/go/v1/usage and its x-opencode-org-id header do not appear in the public OpenCode source that was inspected, so they rest on the author's live validation.
AI process: Opus 5.5 Medium coordination and synthesis, independent Opus 5.5 Xhigh and GPT 6 Sol Xhigh technical assessments with targeted cross-checks of each other's claims, Opus 5.5 Medium editorial check.
Opt out: To stop receiving these reviews, reply to this comment saying so.
…ed servers opencode-v2.db only comes from a build whose channel was literally "v2"; current releases write opencode.db, so a stale v2 file shadowed the live database. Prefer OPENCODE_DB, then opencode.db, and keep the v2 name only as a fallback. Also stop marking 429/5xx responses as retryable, which put the panel on a 30-second re-probe loop for a server that had already answered. Document both in the agents README and reflow the manual paragraph to a full line.
A forced refresh discarded the cached windows before probing, so an offline or 5xx probe blanked the meters even though the cache still held open windows. Load the cache in every case and keep it as the answer of record; --force still bypasses the 15-second reuse window. When several credentials are refused, the last one overwrote the saved fallback, so an earlier credential with open windows could be hidden. Keep the first refusal that still has windows.
V2 stores compaction usage in its own rows and copies a source session's messages into a fork under new IDs, so the collector counted copied history again and dropped compaction tokens. Read compaction rows too and count a fork only from its creation time on, matching OpenCode's own statistics.
|
Thanks! Your V2 dedupe, |
Why this is a separate PR
This is a refreshed continuation of #6779, which was closed as similar to #8065 with an invitation
to submit it again explaining the differences.
It preserves the implementation and review feedback from #6779, including the percentage-scale
and stale-cache corrections reviewed by @dhh.
Compared with #8065, this implementation additionally supports current OpenCode V2 installations:
message) and V2 (session_message) schemas.OPENCODE_DB,opencode-v2.db, thenopencode.db.OPENCODE_API_KEY, legacyauth.json, and the Console session.Getting the usage
Local token stats come from opencode's own SQLite database. Account limits come from OpenCode's
usage endpoint, trying each credential in turn and letting the endpoint pick the one it accepts:
OPENCODE_API_KEYopencode-gokey in the SQLite databaseopencode-goentry ofauth.jsonopencode-goprovider throughAPI keys ask
/zen/go/v1/usage; the Console session asks/inference/go/v1/usagewith thex-opencode-org-idheader. An expired session reads as expired, not as a rejected sign-in.Validation
git diff --checkpass.Supersedes #6779.