Skip to content

Share one usage builder between Claude's OAuth and web paths - #209

Merged
tsouth89 merged 2 commits into
mainfrom
sou-546-share-claude-usage-builder
Aug 5, 2026
Merged

Share one usage builder between Claude's OAuth and web paths#209
tsouth89 merged 2 commits into
mainfrom
sou-546-share-claude-usage-builder

Conversation

@tsouth89

@tsouth89 tsouth89 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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.toml807 passed (805 baseline, +2 new)
  • cargo test --manifest-path apps/desktop-tauri/src-tauri/Cargo.toml474 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

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.

Claude's two sources each dropped what the other rendered. They were
separate response types with separate snapshot builders, so every window
Anthropic added had to be wired twice and never was.

OAuth parsed a design window and never rendered it, parsed extra usage
and threw the cost away. Web only ever read `seven_day_opus` for the
model-specific slot, so an account with no Opus pool showed nothing where
Sonnet belonged.

Both now deserialize into `ClaudeUsageResponse` and build through
`build_snapshot`, which maps every window once. The map-based
deserializer that web needed for Anthropic's overlapping alias keys now
covers OAuth too.

Two behavior changes fall out of the unification:

- OAuth's `seven_day_omelette` was aliased onto routines and rendered as
  "Daily Routines". It maps to the promotional window now, so it reads
  "Weekly promo" as it already did on web.
- Web falls back to the usage payload's embedded extra usage when the
  dedicated overage endpoint fails, rather than reporting no cost.

807 rust / 474 tauri, clippy and fmt clean on both manifests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 5, 2026 11:47
@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 261538c Commit Preview URL

Branch Preview URL
Aug 05 2026, 02:00 PM

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5bda4205-e6d4-4248-85ee-42bcad7d9f4d

📥 Commits

Reviewing files that changed from the base of the PR and between e046fe8 and 261538c.

📒 Files selected for processing (1)
  • rust/src/providers/claude/web_api.rs
 ______________________________________________________
< Plot twist: Your tests were the real bugs all along. >
 ------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

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: 5bda4205-e6d4-4248-85ee-42bcad7d9f4d

📥 Commits

Reviewing files that changed from the base of the PR and between e046fe8 and 261538c.

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

📝 Walkthrough

Walkthrough

Claude usage parsing and snapshot construction now use shared types across OAuth and web providers. The shared module handles payload variants, usage-window normalization, scoped limits, and extra-usage cost conversion.

Changes

Claude usage normalization

Layer / File(s) Summary
Shared usage contracts and normalization
rust/src/providers/claude/mod.rs, rust/src/providers/claude/usage_api.rs
Adds shared Claude usage types, flexible payload deserialization, utilization normalization, window mapping, scoped limits, extra-usage conversion, and tests.
OAuth and web provider integration
rust/src/providers/claude/oauth/mod.rs, rust/src/providers/claude/web_api.rs
Replaces local response models with shared types. Both providers use shared snapshot construction and normalized extra-usage cost handling. Tests use the shared models.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • tsouth89/ceiling#103: Both changes modify OAuth usage snapshot construction in rust/src/providers/claude/oauth/mod.rs.

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeUsageFetcher
  participant ClaudeUsageResponse
  participant SnapshotBuilder
  ClaudeUsageFetcher->>ClaudeUsageResponse: deserialize OAuth or web payload
  ClaudeUsageResponse->>SnapshotBuilder: build_snapshot()
  SnapshotBuilder-->>ClaudeUsageFetcher: normalized usage snapshot
Loading
🚥 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: sharing one usage builder across Claude OAuth and web paths.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 sou-546-share-claude-usage-builder

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
rust/src/providers/claude/usage_api.rs (2)

290-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test that pins Opus precedence over Sonnet.

build_snapshot selects seven_day_opus first and falls back to seven_day_sonnet. web_shape_keeps_sonnet_as_the_model_specific_window supplies only Sonnet, so a reversed or_else order would still pass. Add a case that supplies both windows.

As per coding guidelines: "Add or extend focused Rust tests near the changed module; use deterministic samples or fixtures for parser and fetcher changes where practical."

💚 Proposed test
#[test]
fn opus_wins_over_sonnet_for_the_model_specific_window() {
    let response: ClaudeUsageResponse = serde_json::from_str(
        r#"{
            "five_hour": {"utilization": 12},
            "seven_day_opus": {"utilization": 67},
            "seven_day_sonnet": {"utilization": 45}
        }"#,
    )
    .expect("response parses");

    assert_eq!(
        snapshot(&response)
            .model_specific
            .expect("Opus model-specific window")
            .used_percent,
        67.0
    );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/providers/claude/usage_api.rs` around lines 290 - 309, Add a focused
test near web_shape_keeps_sonnet_as_the_model_specific_window that provides both
seven_day_opus and seven_day_sonnet, then assert
snapshot(&response).model_specific uses the Opus utilization value. Keep the
existing Sonnet-only coverage unchanged and use deterministic JSON input.

Source: Coding guidelines


168-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the window-length constants.

Some(10080) repeats five times and Some(300) once. Named constants make the five-hour and seven-day intent explicit and prevent a future edit from changing only one site.

♻️ Proposed refactor
 impl ClaudeUsageResponse {
+    const FIVE_HOUR_MINUTES: u32 = 5 * 60;
+    const SEVEN_DAY_MINUTES: u32 = 7 * 24 * 60;
+
     pub(super) fn utilization_scale(&self) -> UtilizationScale {

Then replace Some(300) with Some(Self::FIVE_HOUR_MINUTES) and each Some(10080) with Some(Self::SEVEN_DAY_MINUTES).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/src/providers/claude/usage_api.rs` around lines 168 - 207, Define named
window-length constants on the relevant implementation, such as
FIVE_HOUR_MINUTES and SEVEN_DAY_MINUTES, then replace every Some(300) and
Some(10080) passed to convert with the corresponding constant. Update all
occurrences consistently, including the model-specific and auxiliary window
conversions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rust/src/providers/claude/web_api.rs`:
- Around line 343-351: Remove the embedded-payload fallback from the cost
selection in the surrounding provider response flow: update the expression using
ClaudeExtraUsage::cost_snapshot so it only uses the successful dedicated
endpoint result. Preserve the existing extra_usage assignment and
endpoint-failure fallback implemented earlier, and continue applying with_cost
only when the selected endpoint snapshot provides a cost.

---

Nitpick comments:
In `@rust/src/providers/claude/usage_api.rs`:
- Around line 290-309: Add a focused test near
web_shape_keeps_sonnet_as_the_model_specific_window that provides both
seven_day_opus and seven_day_sonnet, then assert
snapshot(&response).model_specific uses the Opus utilization value. Keep the
existing Sonnet-only coverage unchanged and use deterministic JSON input.
- Around line 168-207: Define named window-length constants on the relevant
implementation, such as FIVE_HOUR_MINUTES and SEVEN_DAY_MINUTES, then replace
every Some(300) and Some(10080) passed to convert with the corresponding
constant. Update all occurrences consistently, including the model-specific and
auxiliary window conversions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 766e312c-0d93-4eb1-bcab-2890ee4ef21d

📥 Commits

Reviewing files that changed from the base of the PR and between 61e6303 and e046fe8.

📒 Files selected for processing (4)
  • rust/src/providers/claude/mod.rs
  • rust/src/providers/claude/oauth/mod.rs
  • rust/src/providers/claude/usage_api.rs
  • rust/src/providers/claude/web_api.rs

Comment thread rust/src/providers/claude/web_api.rs Outdated
The fetch site already falls back to the usage payload's embedded extra
usage when the dedicated overage endpoint fails, so a second fallback at
the cost site only fires when that endpoint *succeeded* and reported
`is_enabled: false`. That turned a deliberate "no overage" answer into a
cost read from the stale embedded copy.

Caught by CodeRabbit on #209.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@tsouth89

tsouth89 commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Good catch — and it was worse than redundant.

The endpoint-failure fallback already exists on main at the fetch site (web_api.rs:303-307), so the second or_else could only fire when the dedicated endpoint succeeded and reported is_enabled: false. It turned a deliberate "overage is off" answer into a cost read from the embedded copy — the same defect class #208 just fixed.

Removed in 261538c. My PR description above was also wrong to list "web gained a cost fallback" as a behavior change; the fallback was already there, and the net change to web's cost path is now nil.

807 rust / 474 tauri, clippy and fmt clean.

@tsouth89
tsouth89 merged commit 17af200 into main Aug 5, 2026
10 checks passed
@tsouth89
tsouth89 deleted the sou-546-share-claude-usage-builder branch August 5, 2026 14:07
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