Skip to content

feat(opencode-go): show subscription quota on the dashboard - #3337

Open
ntdatt812 wants to merge 1 commit into
decolua:masterfrom
ntdatt812:feat/opencode-go-quota-3334
Open

feat(opencode-go): show subscription quota on the dashboard#3337
ntdatt812 wants to merge 1 commit into
decolua:masterfrom
ntdatt812:feat/opencode-go-quota-3334

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes #3334.

opencode-go connections render nothing on the quota dashboard while ollama, deepseek and antigravity render progress bars. The provider is a paid subscription with real rolling/weekly/monthly windows, so there is something to show.

The diagnosis in #3334 is the reporter's. I checked each of its claims against this codebase and against the upstream route source rather than taking them on trust — three hold, one does not, and the upstream source settled several details the issue could only guess at.

What the upstream route actually returns

The endpoint is first-party but undocumented. Its implementation is anomalyco/opencodepackages/console/app/src/routes/zen/go/v1/usage.ts (added in anomalyco/opencode#16513), and it decides the whole contract:

{ "usage": {
    "rolling": { "status": "ok",           "percent": 12,  "resetsAt": "<ISO>" },
    "weekly":  { "status": "ok",           "percent": 47,  "resetsAt": "<ISO>" },
    "monthly": { "status": "rate-limited", "percent": 100, "resetsAt": "<ISO>" }
}}

Four things follow from reading it, none of which are in the issue:

  • percent is already a floored, clamped 0..100 integerusagePercent: Math.floor(Math.min(100, (usage / limit) * 100)) in console/core/src/subscription.ts. So used is the percent and total is 100; my clamp is belt-and-braces, not a scale conversion.
  • resetsAt is an ISO string, new Date(Date.now() + resetInSec * 1000).toISOString() — handled by the shared parseResetTime.
  • 403 is not an auth failure. The route answers 403 EntitlementError "OpenCode Go subscription required." when the key authenticates but the workspace has no Go plan. Folding 403 into the 401 branch — as the proposed patch does — tells a user with a perfectly good key to go reissue it. They are split here.
  • All three windows are always emitted, so "skip an absent window" never fires in practice. The guard stays anyway, because the payload shape has already changed once since the route merged; an unreadable window is skipped rather than rendered as a 0% bar that reads as "quota untouched".

That last point also fixes labels. I originally wrote Weekly (7d) / Monthly (30d), copying the issue. Both are wrong: analyzeWeeklyUsage resets on a calendar week boundary (getWeekBounds) and analyzeMonthlyUsage on the subscription anniversary (getMonthlyBounds(now, timeSubscribed)), not rolling spans. The rolling window's length is server-side plan config and never appears in the payload, so (5h) was a guess too. Labels are now plain Rolling / Weekly / Monthly — each row already renders its own countdown from resetAt, which is the honest version of the same information.

Error bodies are {type:"error", error:{type, message}}, so the message is unwrapped rather than echoed as a raw JSON envelope.

The four claims in the issue

1. Registry features block — confirmed, and it is the actual blocker. USAGE_SUPPORTED_PROVIDERS / USAGE_APIKEY_PROVIDERS are derived in src/shared/constants/providers.js by filtering the registry on features.usage / features.usageApikey. Without them /api/usage/[connectionId] returns "Usage not available for this connection" before any fetch happens, and ProviderLimits never calls for the connection.

2. No fetcher in misc.js — confirmed.

3. No USAGE_HANDLERS entry — confirmed. getUsageForProvider would return "Usage API not implemented for opencode-go".

4. "The default case does not pass remainingPercentage, causing the progress bars to fail to render" — this one is not right. Both render paths fall back to the same arithmetic:

  • QuotaTablegetRemainingPercentage(quota)calculatePercentage(used, total) when remainingPercentage is absent.
  • ProviderLimitCard recomputes Math.round(((total - used) / total) * 100) and does not read remainingPercentage at all.

For a used = percent, total = 100 row both yield 100 - used — exactly what remainingPercentage carries, including at the used = 0 and used = 100 edges. I proved it rather than reasoning about it: deleting the case "opencode-go": line makes the field-forwarding test fail while "renders the remaining percentage the provider reported" still passes. The bars would have rendered correctly without any frontend change.

I kept the frontend hunk anyway — it is one line, it puts the provider with deepseek/ollama/kimi where it belongs, and it keeps the row correct if the shape ever stops being a straight percentage — but it is grouped for intent, not a fix, and the comment and commit message now say so. Note also that the diff in the issue is written against the built bundle (([e,t])=>{…}), which is why it names ProviderLimits/index.js; the real switch (provider) is in ProviderLimits/utils.js.

One more deviation

The proposal hardcodes the URL inside misc.js. This repo keeps usage endpoints in transport.usage.url and reads them through U(id) in services/usage/shared.js — that is how glm, glm-cn and vercel-ai-gateway work — so it is declared next to the other transports, with a guard for the case where it is absent.

What I could not verify

I have no OpenCode Go subscription, so I have never seen a live 200 body. The shape above is read off the upstream source and cross-checked against four independent third-party clients that consume the same endpoint, but it is not a captured response. Every failure path therefore degrades to a {message} instead of throwing or rendering a wrong bar: unknown shape → "No quota windows reported", non-JSON → "response was not JSON", transport error → the error text. Usage fetchers are display-only and off the routing path, so the worst case of a shape drift is a message where bars would be.

If a maintainer or the reporter can paste one redacted 200 body, I will tighten the parser to it.

Verification

15 tests in unit/opencode-go-usage-3334.test.js, following the existing deepseek-usage.test.js structure: the registry flags land in both derived lists, the endpoint comes from the registry, the request is a GET with Authorization: Bearer, the three windows map with the remainder derived, labels assert no span, an unreadable window is skipped, an out-of-range percent clamps, an empty payload returns a message, a missing key never calls out, 401 reads as an invalid key, 403 reads as a missing subscription and explicitly not as an invalid key, an upstream error message is unwrapped from its envelope, and a non-JSON body plus a transport failure both return messages. Two parseQuotaData tests close it: one that the field is forwarded, one that the rendered percentage is right.

Mutation-checked, since a passing test proves nothing about whether it bites:

mutation result
drop features from the registry registry test fails — expected [...] to include 'opencode-go'
fold 403 back into the 401 branch only the 403 test fails
drop case "opencode-go": field test fails, render test still passes — the evidence for claim 4 above

Full suite (npx vitest run unit translator): 1808 passed / 93 failed, failing set byte-identical to master's, comm empty in both directions. npx eslint reports no issues on the three changed files.

opencode-go connections rendered nothing on the quota page: the registry
carried no features block, so USAGE_SUPPORTED_PROVIDERS / USAGE_APIKEY_PROVIDERS
never listed the provider and /api/usage/[connectionId] answered "Usage not
available" before any fetch. misc.js had no fetcher and USAGE_HANDLERS had no
entry either.

The usage endpoint is declared as transport.usage.url and read through U(),
the way glm and vercel-ai-gateway already do, rather than hardcoded in the
fetcher. Behaviour follows the upstream route
(anomalyco/opencode packages/console/app/src/routes/zen/go/v1/usage.ts):

- 401 is an invalid key, but 403 is an EntitlementError — the key is fine and
  the account has no Go plan. Reporting that as an expired key would send the
  user off to reissue a working one.
- Error bodies are {type:"error",error:{message}}; surface the message instead
  of echoing the JSON envelope.
- percent is a server-floored 0..100 and resetsAt an ISO string, so used is the
  percent, total is 100, and resetAt goes through parseResetTime.
- Labels carry no duration: only the rolling window is a fixed span (its length
  is plan config and absent from the payload), weekly resets on a calendar week
  boundary and monthly on the subscription anniversary, so "7d"/"30d" would be
  wrong. Each row already renders its own countdown from resetAt.

opencode-go joins the deepseek case in parseQuotaData so remainingPercentage is
forwarded and no absolute `remaining` is (the UI reads `remaining` as a 0-100
percentage). For a used=percent/total=100 row the default branch computes the
same number, so this is for intent, not a rendering fix.

Diagnosis by the reporter in decolua#3334.
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.

Feature Request / Bug: Support Quota Tracking and Display for opencode-go Provide

1 participant