fix(opencode): read pay-as-you-go usage instead of failing with HTTP 500 - #2504
fix(opencode): read pay-as-you-go usage instead of failing with HTTP 500#2504epoch-chrono wants to merge 5 commits into
Conversation
opencode.ai retired the payload the OpenCode provider parses. Workspaces that bill per request have `subscription: null`, so the subscription server function answers with an empty payload on GET and HTTP 500 on the POST retry, which surfaces as "OpenCode API error: HTTP 500: HTTPError" and leaves the provider without any usage to show. `rollingUsage.usagePercent` no longer appears in the billing data at all. I fall back to the customer/billing server function, the same one the OpenCode Go provider already reads the Zen balance from, and derive usage from the fields opencode.ai serves today: monthly spend against the configured monthly limit, plus the remaining prepaid balance. Workspaces that still carry a subscription keep the existing path untouched, and the fallback only runs for subscription-shaped failures so credential errors still surface as such. I also treat a server function that resolves to null as an explicit null payload, so the POST retry that answers HTTP 500 is never sent. Refs steipete#706, steipete#273
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d56479cf1a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| let cost = ProviderCostSnapshot( | ||
| used: usage.monthlyUsageUSD, | ||
| limit: usage.monthlyLimitUSD ?? 0, |
There was a problem hiding this comment.
Render no-limit OpenCode pay-as-you-go spend
When the billing payload has monthlyLimit: null, this converts it to limit: 0 while primary is also nil, and the existing menu model drops OpenCode provider-cost sections with nonpositive limits (MenuCardView+Costs.swift:485). In that no-limit pay-as-you-go case the fetch now succeeds but the card still has no metric or cost section, so the parsed monthly spend/balance is effectively hidden; please add an OpenCode no-limit rendering path or otherwise keep the spend displayable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch — fixed in 5dd30e7.
You are right that the no-limit case fell through to guard cost.limit > 0 and left the card with nothing on it, even though the spend and balance had been parsed. I added an OpenCode branch for limit <= 0 in MenuCardView+Costs.swift, following the shape OpenAI and ClawRouter already use for limitless spend: monthly spend as the spend line, remaining prepaid balance underneath, no percentage. Workspaces that do have a limit keep the existing percentage rendering.
I kept limit: 0 as the signal rather than making the cost snapshot optional, since that is the convention those providers already rely on, and documented it where the snapshot is built. New OpenCodeMenuCardCostTests covers all three cases through Model.make: with a limit, without a limit, and without a limit or balance.
A pay-as-you-go workspace with no monthly limit produced an empty card: the fetch succeeded, but the snapshot has no primary window (no limit means no percentage) and its provider cost carries limit 0, which the shared cost section drops. The monthly spend and the prepaid balance were parsed and then never shown. I add an OpenCode branch for that case, matching the one OpenAI and ClawRouter already use for limitless spend: monthly spend as the spend line, remaining balance underneath. Workspaces that do have a limit keep the existing percentage rendering.
|
Codex review: needs changes before merge. Reviewed August 7, 2026, 1:08 PM ET / 17:08 UTC. ClawSweeper reviewWhat this changesThis PR adds a billing-data fallback for OpenCode pay-as-you-go workspaces and displays monthly spend, an optional limit, and balance instead of a subscription-endpoint HTTP 500. Merge readinessKeep open: the OpenCode fallback remains needed and has credible live proof, but the current head is dirty against main and retains a provider-specific menu-card branch that conflicts with main’s descriptor-selected presentation architecture. Priority: P2 Review scores
Verification
How this fits togetherCodexBar’s OpenCode provider converts browser-authenticated workspace responses into a usage snapshot for the menu-bar card. The subscription lookup is the normal input; this PR introduces billing data as the fallback input when pay-as-you-go accounts have no subscription usage. flowchart LR
A[OpenCode session cookie] --> B[Workspace lookup]
B --> C[Subscription usage lookup]
C --> D{Subscription data available?}
D -->|Yes| E[Usage snapshot]
D -->|No| F[Billing usage fallback]
F --> E
E --> G[Menu-bar cost card]
Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Rebase the fallback onto current main, select the no-limit OpenCode presentation through the provider descriptor, and retain focused parsing, fetcher, and menu-model coverage for limited and no-limit accounts. Do we have a high-confidence way to reproduce the issue? Yes—the PR supplies a redacted live before/after fetch trace, and current main still follows the subscription-only path shown in source. This review did not execute a live account request. Is this the best way to solve the issue? No—the billing fallback is narrow, but its menu-card presentation must move to main’s descriptor-selected cost-style boundary rather than retain provider-specific routing in the generic renderer. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 22b24b885693. LabelsLabel justifications:
EvidenceAcceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (14 earlier review cycles; latest 8 shown)
|
|
Fair ask — the PR body claimed a live verification without showing anything inspectable. I have now added the artifacts under "Evidence from a real pay-as-you-go workspace", captured today against a live account with IDs redacted. It covers three things rather than just the after state:
On the fixed-point concern specifically: the evidence includes the raw billing fields, so the arithmetic is checkable rather than asserted. Two limits on the proof, stated plainly: it exercises |
|
On the remaining schema/unit risk, it may help to know how this degrades in practice, since the failure modes are not symmetric. If a field is renamed or dropped, nothing is displayed wrong. If the unit convention changes, the realistic direction under-reports rather than over-reports. Moving Worth being explicit about the limits: the fixture tests pin today's shape, so they will keep passing if opencode.ai changes the payload upstream — the graceful nil path above is the actual protection, not the tests. And the fallback only runs after the subscription path has already failed, so workspaces that still return a subscription never reach this code. If you would rather have a defensive sanity check on the parsed magnitude before merge, I am happy to add one. |
Merge current main, preserve subscription-account classification on fallback, and add regression coverage.
|
Completed the maintainer follow-up in
The previous CI failures were stale-base issues: lint reported an 851-line Proof on the pushed tree:
No merge performed. |
Fixes the
OpenCode API error: HTTP 500: HTTPErrorreported in #706 and #273. I traced it to a schema change on opencode.ai rather than a transient server fault, and it turns out to be fixable client-side — the usage data is still served, under different field names.What breaks today
OpenCodeUsageFetcherresolves the workspace, then asks the subscription server function (7abeebee…) for usage. For a workspace that bills per request, that object no longer exists. I replayed the exact three calls the provider makes, with the same server IDs, headers and cookie:Auth is fine (no 401/403) and the workspace lookup works — the failure is isolated to the subscription function. Two details matter:
subscription: null, and the subscription function has nothing to return.rollingUsage/usagePercentno longer appear anywhere in the billing data (0 occurrences). The current fields aremonthlyUsage,monthlyLimitandbalance. So both theparseSubscriptionregex and the subscription call target a shape opencode.ai has replaced.There is also a smaller issue: a server function that resolves to null answers with
…["server-fn:<uuid>"]=[],null), whichisExplicitNullPayloaddoes not recognize. That is why the POST retry is sent at all, and it is the request that returns HTTP 500.What this changes
When the subscription lookup fails in a subscription-shaped way, I fall back to the customer/billing server function (
c83b78a6…) — the same oneOpenCodeGoUsageFetcheralready reads the Zen balance from — and derive usage from what opencode.ai serves today:usedPercent=monthlyUsage/monthlyLimit, rendered as the primary windowmonthlyUsage,monthlyLimitand the remaining prepaidbalance, rendered as provider costDeliberately conservative:
apiError/parseFailed. Credential and network failures propagate as before, and an expired session detected during the fallback still surfaces asinvalidCredentials.HTTP 500is no longer what the user sees.resetsAtstaysnilrather than guessing one.On the unit scale:
balanceandmonthlyUsagearrive as integers scaled by 1e8, whilemonthlyLimit/reloadAmount/reloadTriggerare whole USD. I did not pick that divisor myself —OpenCodeGoZenBalanceParser.billingScalealready uses it for the balance this app renders today, and my live values are consistent with it (spend matches the cycle, and the balance sits above the configured auto-reload trigger, which had not fired). It is isolated in a single named constant.Parsing is tolerant of both shapes: the billing function replies with SolidStart's
$R[...]JavaScript payload, so I try JSON first and fall back to a field scan that requirescustomerIDbefore trusting any number.Tests
OpenCodeZenBillingParserTestsplus a redacted billing fixture (Tests/CodexBarTests/Fixtures/Providers/OpenCode/billing-pay-as-you-go.txt):$R[...]payload, JSON payload, missing limit, legacy workspace that still has a subscription, and payloads that must be rejected.OpenCodeUsageFetcherErrorTests: pay-as-you-go workspace now yields a snapshot instead of an error, the POST that returns 500 is no longer sent, a POST failure still recovers through billing, and a signed-out billing response maps toinvalidCredentials. The existing null-payload test now asserts the graceful error after the billing attempt.OpenCodeMenuCardCostTestsfor the menu card itself: a workspace with a limit renders the percentage, and one without a limit still renders spend and balance instead of an empty card.toUsageSnapshotcases inOpenCodeUsageParserTestsfor the monthly window, the cost snapshot, the no-limit case, and clamping above 100%.Evidence from a real pay-as-you-go workspace
Captured today against a live opencode.ai account. Workspace/customer IDs are redacted; the session cookie was read from a file and never printed, and the account values below are my own.
Before —
main(this PR reverted), same account, same cookie. A small executable linkingCodexBarCorefrommainand callingOpenCodeUsageFetcher.fetchUsagedirectly:The three requests the provider makes, replayed at the HTTP level with the same server IDs and headers:
Step 2 is the payload
isExplicitNullPayloaddid not recognize, which is why step 3 is sent at all. Step 3 is the request that produces the reported error.What the billing data actually contains for that same workspace, from the customer/billing server function this PR falls back to:
This is also the concrete check on the fixed-point semantics:
monthlyUsage1556267684 / 1e8 = $15.56 against amonthlyLimitof 20 already in whole USD, andbalance1326177004 / 1e8 = $13.26, which is above thereloadTriggerof 5 — consistent with auto-reload not having fired.rollingUsage/usagePercentappear zero times anywhere in this payload.After — this branch, same account, same cookie, through the full fetch path (workspace lookup, subscription attempt, billing fallback, snapshot conversion):
So where the provider previously surfaced
HTTP 500: HTTPError, it now reports the monthly spend, the limit and the remaining prepaid balance, and the values line up with the raw payload above.One thing this proof does not cover: I could not run
make testor launch the packaged app, since this machine has Command Line Tools but no full Xcode (the app target needs the#Previewmacro plugin, and SwiftLint needssourcekitd). The run above linksCodexBarCorefrom this branch directly, so it exercises the fetch and snapshot path but not the SwiftUI rendering; the menu-card branch is covered byOpenCodeMenuCardCostTestsinstead. The CI run on this PR is still awaiting maintainer approval, so the suite has not executed yet.Refs #706, #273