fix(opencode): read pay-as-you-go usage instead of failing with HTTP 500 - #2504
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 maintainer review before merge. Reviewed August 13, 2026, 8:48 PM ET / August 14, 2026, 00:48 UTC. ClawSweeper reviewWhat this changesThe PR falls back from unavailable OpenCode subscription usage to pay-as-you-go billing data and renders monthly spend, an optional limit, and prepaid balance. Merge readinessKeep this PR open: it remains the active implementation candidate for the linked OpenCode provider failure, with strong contributor and owner-supplied proof but an external billing-schema compatibility decision still requiring maintainer review. Priority: P2 Review scores
Verification
How this fits togetherCodexBar’s OpenCode provider uses a session cookie to fetch provider usage, converts the response into a common usage snapshot, and displays that snapshot in the menu-bar card. This change adds a billing-data fallback when the subscription endpoint cannot represent a pay-as-you-go workspace. flowchart LR
A[Session cookie] --> B[OpenCode provider]
B --> C[Subscription endpoint]
C --> D{Usage available?}
D -->|Yes| E[Usage snapshot]
D -->|No| F[Billing endpoint]
F --> E
E --> G[Menu-bar usage card]
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Land the guarded billing fallback only after maintainer acceptance of the undocumented upstream billing contract, retaining the existing subscription path and graceful failure when required billing fields drift. Do we have a high-confidence way to reproduce the issue? Yes. The PR supplies a redacted live current-main trace using the same account and cookie, followed by an after-fix fetch-path result showing monthly spend, limit, and balance. Is this the best way to solve the issue? Yes, based on the supplied diff and proof: the fallback is restricted to subscription-shaped failures, preserves credential and normal subscription behavior, and degrades to an error rather than trusting missing billing fields. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against ec438d04c4c5. LabelsLabel justifications:
EvidenceWhat 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 (29 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. |
The only conflict was the OpenCode no-limit cost rule: main moved menu-card cost presentation behind the provider descriptor, so the shared renderer now switches on ProviderCostMenuCardStyle instead of provider identity, while this branch still carried an OpenCode-specific helper in the renderer. I resolved it by taking main's generic renderer and moving the rule to the seam main established: - Add a `payAsYouGoSpend` style. The existing styles do not cover this case: `payAsYouGoBalance` shows only a balance and `apiSpend` shows only spend, while a pay-as-you-go workspace with no configured limit needs monthly spend with the remaining prepaid balance next to it, and no percentage. - Render that style generically, alongside the other balance styles. - Select it from OpenCodeProviderDescriptor when the cost snapshot carries no limit, the same shape OpenAIAPIProviderDescriptor uses for `apiSpend`. Workspaces that do report a monthly limit keep the generic budget rendering.
|
Both author-side items are done in Thanks for picking up the follow-up in The merge had a single conflict, and it was exactly the descriptor boundary:
Workspaces that do report a monthly limit keep the generic budget rendering, so that path is unchanged.
Same caveat as before on my side: |
Fixes steipete#2697 Co-authored-by: Vitor Jr. <epoch-chrono@users.noreply.github.com>
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
Fixes #2697