Skip to content

Stop reporting credit balance as money spent - #208

Merged
tsouth89 merged 2 commits into
mainfrom
fix/credit-balance-reported-as-spend
Aug 5, 2026
Merged

Stop reporting credit balance as money spent#208
tsouth89 merged 2 commits into
mainfrom
fix/credit-balance-reported-as-spend

Conversation

@tsouth89

@tsouth89 tsouth89 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Two core providers put a remaining credit balance into CostSnapshot.used, which core/usage_snapshot.rs documents as "Amount used in the current period". The number is inverted: it falls as you spend, and reads $0.00 at exactly the moment the account is exhausted.

Codex

codex/api.rs contains both the right answer and the wrong one:

// SpendControlLimitSnapshot::to_cost_snapshot — correct
.unwrap_or_else(|| (limit - balance).max(0.0))

// extract_credits — what actually shipped
Some(CostSnapshot::new(balance, "USD", "Credits"))

The live path is build_result_from_json, and it never consulted individual_limit at all, so the raw balance is what users saw. (build_result, which does check the limit, is only reachable from tests.)

Now: spend is derived from the spend-control limit when one is reported, and not claimed at all when it isn't — a balance on its own genuinely cannot tell you what was spent. The balance is still surfaced, as its own info-only "Credits" line, so nothing is lost.

OpenCode Go

Already displayed the Zen balance honestly as an info-only line, then reported the same number as cost two lines later. The parser matches currentBalance / zenBalance / balanceUsd — all meaning remaining. The bogus cost snapshot is removed; the honest line stays.

Verification

805 rust (up from 801) / 474 tauri, clippy clean. Three new Codex tests cover: balance-without-limit claims no spend but still shows the balance; balance-with-limit derives $40 of $50; unlimited credits report neither.

Provenance

Found by an audit of the core providers only (Claude, Codex, Cursor, Copilot, Grok, Antigravity, OpenCode Go). Same defect class as the Cursor on-demand work in 1.5.24. The audit turned up five further findings, which I'm filing as issues rather than folding in here.

Summary by CodeRabbit

  • New Features
    • Usage information now clearly separates remaining credit balances from money spent.
    • Spending details are shown when applicable limits allow cost tracking.
  • Bug Fixes
    • Unlimited-credit accounts no longer display misleading cost information.
    • OpenCodeGo credit balances are now presented as informational remaining-credit windows rather than reported as spending.
    • Improved credit and usage reporting across supported providers.

Codex and OpenCode Go both put a remaining credit balance into
`CostSnapshot.used`, which is documented as "amount used in the current
period". The number was inverted: it fell as the user spent and read
$0.00 at exactly the moment the account was exhausted.

Codex proves it knew better. `SpendControlLimitSnapshot::to_cost_snapshot`
derives `used = (limit - balance).max(0.0)`, but the live JSON path never
consulted the spend-control limit at all, so the raw balance is what
shipped. Spend is now derived from the limit when one is reported, and
claimed not at all when it is not: a balance alone cannot tell you what
was spent. The balance is still shown, as its own "Credits" line.

OpenCode Go already displayed the Zen balance honestly as an info-only
line, then additionally reported the same number as cost. The bogus cost
snapshot is gone; the honest line stays.

Found by an audit of the core providers, the same defect class as the
Cursor on-demand work in 1.5.24.
Copilot AI lite review requested due to automatic review settings August 5, 2026 07:58
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ceiling 6146490 Commit Preview URL

Branch Preview URL
Aug 05 2026, 08:08 AM

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dbab75d-eba9-40e4-b67e-838587a9331d

📥 Commits

Reviewing files that changed from the base of the PR and between c7c36e3 and 6146490.

📒 Files selected for processing (1)
  • rust/src/providers/opencodego/mod.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/providers/opencodego/mod.rs

📝 Walkthrough

Walkthrough

Codex and OpenCodeGo now separate remaining credit balances from monetary spend. Codex derives spend only from spend-control limits. OpenCodeGo reports Zen balance as informational usage. Tests cover balance extraction and credit-limit scenarios.

Changes

Provider usage accounting

Layer / File(s) Summary
Codex credit accounting
rust/src/providers/codex/api.rs
Codex adds a separate credit-balance window. Spend requires a valid spend-control limit. Tests cover balance-only, limited, and unlimited credits.
OpenCodeGo balance reporting
rust/src/providers/opencodego/mod.rs
OpenCodeGo centralizes page parsing and reports Zen remaining balance without converting it into spent cost. A regression test validates the behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: separating credit balances from reported money spent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/credit-balance-reported-as-spend

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes incorrect cost reporting in two providers where a remaining credit balance was being surfaced as money spent (CostSnapshot.used), causing spend to decrease as the user spent and to show $0 at exhaustion. It updates the Codex and OpenCode Go providers to (1) only derive spend when a spend-control limit is available and (2) display remaining balance as an info-only line instead of cost.

Changes:

  • OpenCode Go: stop emitting a CostSnapshot from Zen balance; keep balance as an info-only rate window and add a regression test around parsing.
  • Codex: add an explicit “Credits … left” info-only line and only report cost when a spend-control limit is present (deriving used = limit - balance).
  • Add Codex tests covering: balance-without-limit (no spend claimed), balance-with-limit (spend derived), and unlimited credits (neither spend nor balance shown).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
rust/src/providers/opencodego/mod.rs Removes reporting Zen balance as cost; keeps it as an info-only line and adds a small regression test.
rust/src/providers/codex/api.rs Splits “credit balance remaining” from “money spent”; derives spend only from spend-control limit and adds targeted tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rust/src/providers/opencodego/mod.rs
The test claimed to verify the Zen balance is not reported as spend, but
only asserted that parse_zen_balance returns a number, which the fix did
not touch.

Split the snapshot assembly out of the fetch as `result_from_page`, so
the behavior is testable without a network round trip, and assert what
the fix actually changed: no CostSnapshot, and the balance present as an
info-only line reading "$12.50" at 0%.

The fixture is shaped like the real page (unquoted JS keys for the
windows, balance as display text); a quoted-JSON fixture parses as
neither, which is what the first attempt got wrong.
@tsouth89

tsouth89 commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Good catch, fixed. The test name promised behavior verification and only asserted parse_zen_balance returns a number — which the fix didn't touch, so it would have passed against the buggy code.

Rather than rename it down, I split the snapshot assembly out of the fetch as result_from_page, so the behavior is testable without a network call. The test now asserts what actually changed: no CostSnapshot, and the balance present as an info-only line reading $12.50 at 0%.

Worth noting the fixture took two tries. A quoted-JSON page parses as neither — extract_window's regex needs \s*[:=] immediately after the key, which a closing quote breaks, and parse_zen_balance needs either valid JSON or a $-prefixed string. The fixture is now shaped like the real page: unquoted JS keys for the windows, balance as display text.

805 rust / 474 tauri, clippy clean.

@tsouth89
tsouth89 merged commit 61e6303 into main Aug 5, 2026
11 checks passed
@tsouth89
tsouth89 deleted the fix/credit-balance-reported-as-spend branch August 5, 2026 08:11
tsouth89 added a commit that referenced this pull request Aug 5, 2026
Closes SOU-546.

Claude's OAuth and web sources had drifted into two response types with
two snapshot builders. Every window Anthropic adds has to be wired
twice, and it never was — so each path dropped what the other rendered.
One root cause, three symptoms.

## What was broken

- **OAuth lost the Design window.** It was parsed (`sevenDayDesign`,
with `seven_day_oauth_apps` aliased onto it) and then simply never read
when the snapshot was built. Only routines made it into
`extra_rate_windows`.
- **OAuth lost extra-usage dollars.** The `ExtraUsage` struct existed,
deserialized correctly, and was discarded.
`ProviderFetchResult::new(usage, "oauth")` never got a cost snapshot, so
the monthly limit and spend that web showed were invisible on OAuth.
- **Web lost the Sonnet window.** Its model-specific slot read
`seven_day_opus` and nothing else, so an account with no Opus pool
rendered an empty slot where Sonnet belonged. OAuth already had the
Opus-then-Sonnet fallback.

## What changed

`rust/src/providers/claude/usage_api.rs` is new and owns the wire shape:
`ClaudeUsageResponse`, `ClaudeUsageWindow`, `ClaudeExtraUsage`, plus
`build_snapshot` and `extra_usage_cost`. Both fetchers deserialize into
it and build through it.

The map-based deserializer that web needed — Anthropic ships overlapping
keys like `seven_day_design` and `seven_day_omelette` in one payload,
which serde aliases reject as a duplicate field — now covers OAuth too,
with both casings accepted on every key.

`build_snapshot` takes the window converter as a closure, which
preserves the one difference that is real: OAuth's `to_rate_window`
returns `Option` and skips windows with no utilization; web's always
emits. Both keep the behavior they had.

## Behavior change beyond the three symptoms

**An OAuth window is renamed.** OAuth aliased `seven_day_omelette` onto
routines, so it rendered as "Daily Routines". It maps to the promotional
window now and reads **"Weekly promo"**, as it already did on web. The
web name is the correct one; this is a visible label change for anyone
whose payload carries that key.

Web's cost path is unchanged. An earlier revision of this PR added a
second extra-usage fallback at the cost site; CodeRabbit caught that the
fetch site already falls back to the embedded payload when the dedicated
overage endpoint fails, so the second one could only fire when that
endpoint *succeeded* and reported `is_enabled: false` — turning a
deliberate "overage is off" into a cost read from the embedded copy.
Removed in 261538c.

## Open item before release

`used_credits` is treated as money spent. Web has always done this and
it is unchanged here — but this PR propagates that assumption to OAuth
for the first time. Given #208 just fixed exactly this inversion in
Codex and OpenCode Go, and that audit turned up five further findings,
the number is worth confirming against a live OAuth account in the
running app. It is unreachable from tests and mocks.

## Verification

- `cargo test --manifest-path rust/Cargo.toml` — **807 passed** (805
baseline, +2 new)
- `cargo test --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml` —
**474 passed**
- `cargo clippy --all-targets -- -D warnings` — clean on both manifests
- `cargo fmt --check` — clean

Two new regression tests in `usage_api.rs` cover all three symptoms: an
OAuth-shaped payload keeps Design and converts extra-usage cents to
dollars with its limit; a web-shaped payload with no Opus pool puts
Sonnet in the model-specific slot. Existing test counts per file are
unchanged — nothing was deleted to get here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Improvements**
  * Improved Claude usage reporting across OAuth and web connections.
* Usage windows now display consistently for primary, secondary,
model-specific, and weekly limits.
* Added extra-usage details, including spending limits, credits used,
currency, and associated costs.
* Improved compatibility with Claude response formats using camelCase or
snake_case data.
* Null or unavailable usage fields are handled more reliably across
usage reports.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: tsouth89 <tsouth89@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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