Skip to content

fix(cost): reconcile usage estimates and billing labels#520

Closed
willytop8 wants to merge 5 commits intosteipete:mainfrom
willytop8:upstream-cost-usage-reconciliation
Closed

fix(cost): reconcile usage estimates and billing labels#520
willytop8 wants to merge 5 commits intosteipete:mainfrom
willytop8:upstream-cost-usage-reconciliation

Conversation

@willytop8
Copy link

Summary

  • reconcile Claude OAuth and Claude web extra-usage values so obvious unit mismatches prefer the web billing value
  • update Codex and Claude local token-cost pricing/model normalization for current model names
  • relabel local token cost as an estimate and rename the latest daily aggregate from "Today" to "Latest day"
  • bump the local cost cache version so pricing updates invalidate stale cached totals

Testing

  • swift build
  • swift test --filter ClaudeOAuthTests
  • swift test --filter ClaudeUsageTests
  • swift test --filter OpenAIDashboardParserTests
  • swift test --filter CostUsageDecodingTests
  • swift test --filter CostUsageScannerTests
  • swift test --filter CLICostTests
  • swift test --filter CostUsagePricingTests
  • swift test --filter MenuCardModelTests

Notes

  • This PR intentionally excludes the local handoff doc commit and keeps scope to code and tests.
  • The local token-cost numbers remain estimates derived from local logs and published model pricing; they are intentionally separate from dashboard credits and Claude billing values.

willytop8 and others added 5 commits March 12, 2026 19:44
Transform CodexBar into a focused 5-provider app targeting Claude Code,
OpenAI Codex, Google Gemini, Google Antigravity, and Perplexity.

Provider strategy upgrades:
- Claude: OAuth-only via api.anthropic.com/api/oauth/usage (no CLI/web fallback)
- Codex: CLI RPC only (remove web dashboard scrape)
- Gemini: OAuth API + FSEvents watcher on ~/.gemini/ for instant updates
- Antigravity: local probe + FSEvents watcher on ~/.codeium/ + 3x retry backoff
- Perplexity: new provider — web API with session cookie stored in Keychain

Adaptive refresh scheduler:
- Active (>50% utilisation): 15s; moderate: 60s; idle: 600s
- 120s hysteresis before dropping out of active rate
- 429 rate-limit backoff: 300s cooldown, auto-resume

Staleness indicators in menu cards:
- ✅ green (<30s), ⚠️ yellow (30s–5m), ❌ red (>5m or error)
- Menu bar dot triggers when any provider is >5m stale
- isStale(provider:) now time-based, not error-only

Non-target providers: left intact, defaultEnabled: false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AdaptiveRefreshScheduler now tracks lastRefreshedAt per provider and
  exposes shouldRefresh()/recordRefresh() so each provider is gated on
  its own effective interval, not just the global timer tick.  Fast local
  providers (Codex 15 s) no longer drag remote API providers (Claude,
  Gemini, Perplexity) along at the same cadence.
- During a 429 backoff, effectiveInterval returns the remaining backoff
  time (not a fixed 600 s), so the provider is skipped entirely until the
  window expires.
- Rate-limit backoff (1800 s) and hard remote-API floor (300 s) are now
  persisted across relaunches via UserDefaults.
- ClaudeOAuthFetchStrategy gains isAlwaysAvailable flag; set to true in
  auto mode so the pipeline always runs fetch() and surfaces real errors
  instead of the opaque "no available fetch strategy" message.
- User-initiated refreshes pass forceAllProviders: true to bypass the
  per-provider gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /api/oauth/usage endpoint is a private beta endpoint built for
on-demand queries, not continuous polling. 5-minute polling (12×/hr)
was too aggressive. 15 minutes (4×/hr) keeps usage data reasonably
fresh while staying well within rate limits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raise Claude's minimum polling floor, preserve cached Claude data during 429 backoff, and keep forced refreshes from bypassing active rate-limit cooldowns.

Refresh the README fork description, remove internal fork-admin documents, sanitize hard-coded local paths and personal contact details, and delete local junk artifacts before pushing.
@willytop8
Copy link
Author

Closing this in favor of #522, which contains the same fix rebuilt cleanly on top of upstream main as a single commit.

@willytop8 willytop8 closed this Mar 13, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be163eb5ff

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

var request = URLRequest(
url: URL(string: "https://www.perplexity.ai/rest/user/settings")!,
timeoutInterval: context.webTimeout)
request.setValue(cookie, forHTTPHeaderField: "Cookie")

Choose a reason for hiding this comment

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

P1 Badge Prefix Perplexity cookie name before sending request

The settings UI asks users to paste the __Secure-next-auth.session-token value, but this request sends that raw value directly as the Cookie header. A valid cookie header must be name=value, so users who follow the UI instructions will send an invalid cookie and get unauthenticated responses from https://www.perplexity.ai/rest/user/settings, effectively breaking Perplexity usage fetching unless they manually add the cookie name themselves.

Useful? React with 👍 / 👎.

Comment on lines +417 to +420
let due = self.shouldRefreshProvider(
provider,
forceAllProviders: forceAllProviders,
now: now)

Choose a reason for hiding this comment

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

P1 Badge Update adaptive scheduler state for token-account refresh path

This new per-provider due gate depends on AdaptiveRefreshScheduler, but providers that go through refreshTokenAccounts (when showAllTokenAccountsInMenu is enabled with multiple accounts) return early in refreshProvider and never call recordRefresh/recordRateLimit. In that mode, the scheduler never advances for that provider, so shouldRefreshProvider keeps it due on every cycle and Claude multi-account setups can still be hammered despite the new long minimum interval and 429 backoff logic.

Useful? React with 👍 / 👎.

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.

1 participant