Temporary research file. Delete after implementation.
- Codex Usage API
- Current System Architecture
- Anthropic-Hardcoded Locations
- Provider-Agnostic Parts (No Changes Needed)
- Implementation Plan
Sources: OpenAI Codex source code (github.com/openai/codex, Rust codebase), CodexBar macOS app (github.com/steipete/CodexBar), Context7 Codex developer docs.
GET https://chatgpt.com/backend-api/wham/usage
Fallback (when base URL doesn't contain /backend-api):
GET {base_url}/api/codex/usage
Required Headers:
Authorization: Bearer <access_token>
ChatGPT-Account-Id: <account_id>
Content-Type: application/json
Accept: application/jsonaccess_token— The OAuth access token fromauth.openai.com(same token ourcodex-oauth.tsalready obtains)account_id— Account UUID from OAuth token data. Stored in~/.codex/auth.jsonundertokens.account_id. Optional per CodexBar ("when available") but may be required.
From codex-rs/codex-backend-openapi-models/src/models/rate_limit_status_payload.rs:
{
"plan_type": "plus",
"rate_limit": {
"allowed": true,
"limit_reached": false,
"primary_window": {
"used_percent": 96,
"limit_window_seconds": 18000,
"reset_after_seconds": 673,
"reset_at": 1730947200
},
"secondary_window": {
"used_percent": 70,
"limit_window_seconds": 604800,
"reset_after_seconds": 43200,
"reset_at": 1730980800
}
},
"credits": {
"has_credits": false,
"unlimited": true,
"balance": null
},
"additional_rate_limits": [
{
"limit_name": "codex_other",
"metered_feature": "codex_other",
"rate_limit": {
"allowed": true,
"limit_reached": false,
"primary_window": {
"used_percent": 70,
"limit_window_seconds": 3600,
"reset_after_seconds": 1800,
"reset_at": 1730947200
}
}
}
]
}primary_window= 5h session (18000s). Maps to oursessionPercent.secondary_window= Weekly (604800s = 7d). Maps to ourweeklyPercent.reset_at= Unix timestamp (seconds). Convert to ms for oursessionResetTimestamp/weeklyResetTimestamp.plan_typevalues:guest,free,go,plus,pro,free_workspace,team,business,education,quorum,k12,enterprise,edu
Rate limit data is also returned in HTTP response headers on every /v1/responses call:
x-codex-primary-used-percent → float (e.g., "25.0")
x-codex-primary-window-minutes → integer (e.g., "300" for 5h)
x-codex-primary-reset-at → unix timestamp seconds
x-codex-secondary-used-percent → float (weekly)
x-codex-secondary-window-minutes → integer
x-codex-secondary-reset-at → unix timestamp seconds
x-codex-credits-has-credits → "true" or "false"
x-codex-credits-unlimited → "true" or "false"
x-codex-credits-balance → decimal string e.g. "9.99"
SSE event type codex.rate_limits also carries this data inline in streaming responses.
Our codex-oauth.ts already uses the correct flow:
- Client ID:
app_EMoamEEZ73f0CkXaXp7hrann(same as Codex CLI) - Auth endpoint:
https://auth.openai.com/oauth/authorize - Token endpoint:
https://auth.openai.com/oauth/token - Scopes:
openid profile email offline_access - Refresh:
POST https://auth.openai.com/oauth/tokenwithgrant_type=refresh_token
Missing: account_id for the ChatGPT-Account-Id header. Options:
- Decode from the JWT access token
- Read from
~/.codex/auth.json(tokens.account_id) - Extract during OAuth token exchange (may be in response)
- Try without it first (optional per CodexBar docs)
The app has TWO account management systems that don't fully integrate:
System A: Legacy Claude Profile Manager (Main Process)
claude-profile-manager.ts— Manages OAuth profiles, rate limits, usage, auto-swapclaude-profiles.json— Stores profiles withactiveProfileId,accountPriorityOrderusage-monitor.ts— Polls Anthropic's/api/oauth/usageendpoint every 30stoken-refresh.ts— Refreshes tokens viaconsole.anthropic.com/v1/oauth/tokenrate-limit-detector.ts— Detects rate limits, triggers auto-swapprofile-scorer.ts— Scores profiles by availability for auto-swap- 100% Anthropic-specific. Only knows about Anthropic OAuth tokens, Anthropic endpoints, Anthropic keychain format.
System B: Multi-Provider Accounts (Renderer + Settings)
ProviderAccount[]insettings-store.ts— All connected accounts (any provider)globalPriorityOrder: string[]in AppSettings — Manual priority queueuseActiveProvider()hook — First account in priority order = active- Provider-agnostic. Works for all 10 providers. But has NO usage monitoring, NO auto-swap.
The gap: System A handles usage monitoring + auto-swap but only for Anthropic. System B handles multi-provider accounts but has no usage awareness.
UsageMonitor.start() → 30s interval
↓
checkUsageAndSwap()
├─ determineActiveProfile() ← Hardcoded: defaults to anthropic baseUrl
├─ getCredential() ← Hardcoded: reads from Anthropic keychain
│ └─ ensureValidToken(configDir) ← Hardcoded: refreshes via Anthropic endpoint
├─ fetchUsageViaAPI() ← Hardcoded: only allows anthropic/zai/zhipu domains
│ ├─ getUsageEndpoint(provider) ← Only 3 providers configured
│ ├─ Add anthropic-specific headers ← if (provider === 'anthropic') add beta headers
│ └─ Parse response ← Provider-specific normalization
├─ emit('usage-updated') → IPC 'claude:usageUpdated' → renderer
├─ emit('all-profiles-usage-updated') → IPC 'claude:allProfilesUsageUpdated' → renderer
└─ checkThresholdsExceeded()
└─ performProactiveSwap() ← Only swaps Anthropic profiles
Manual swap (UI):
User clicks account in UsageIndicator popover
→ handleSwapAccount(accountId)
→ setQueueOrder([accountId, ...rest]) ← Reorders globalPriorityOrder
→ requestUsageUpdate() ← Refreshes usage display
Automatic swap (rate limit hit):
SDK operation fails with 429
→ detectRateLimit(output) ← Pattern: "Limit reached · resets..."
→ recordRateLimitEvent(profileId)
→ getBestAvailableProfileEnv()
→ profileManager.setActiveProfile() ← Only updates claude-profiles.json
→ usageMonitor.getAllProfilesUsage() ← Refreshes UI
← Returns new profile env vars
Problem: Auto-swap updates claude-profiles.json but NOT globalPriorityOrder. The renderer's priority queue may be out of sync.
| Component | What it shows | Provider-specific? |
|---|---|---|
AuthStatusIndicator |
Provider badge (OpenAI/Anthropic) + auth type label | Codex = green "Codex", Anthropic = orange "OAuth" |
UsageIndicator |
Usage bars OR "Subscription" OR "Unlimited" | Anthropic OAuth = bars, Codex OAuth = "Subscription", API = "Unlimited" |
ProviderAccountCard |
Account card in settings with usage bars | Shows usage bars only when account.usage populated (Anthropic only) |
ProviderAccountsList |
All accounts grouped by provider | Generic, but re-auth routes differ per provider |
AddAccountDialog |
OAuth flow + account creation | Different flows: Codex → codexAuthLogin(), Anthropic → claudeAuthLoginSubprocess() |
ProviderSection |
Provider group with "Add" buttons | Button label: "Add Codex Subscription" vs "Add OAuth" |
Types use "Claude" prefix but are structurally generic:
ClaudeUsageSnapshot → { sessionPercent, weeklyPercent, resetTimestamps, profileId, ... }
ClaudeUsageData → { sessionUsagePercent, weeklyUsagePercent }
ClaudeRateLimitEvent → { type, hitAt, resetAt }
ProfileUsageSummary → { sessionPercent, weeklyPercent, availabilityScore, ... }
AllProfilesUsage → { activeProfile, allProfiles[], fetchedAt }These types work perfectly for Codex data — same session/weekly model. No structural changes needed, just need to populate them.
| File | Line(s) | What's hardcoded | What to do |
|---|---|---|---|
usage-monitor.ts:45-49 |
ALLOWED_USAGE_API_DOMAINS |
Only api.anthropic.com, api.z.ai, open.bigmodel.cn |
Add chatgpt.com |
usage-monitor.ts:60-73 |
PROVIDER_USAGE_ENDPOINTS |
Only anthropic/zai/zhipu paths | Add { provider: 'openai', usagePath: '/wham/usage' } |
usage-monitor.ts:662,1069,1346,1359 |
baseUrl: 'https://api.anthropic.com' |
Hardcoded fallback for all OAuth profiles | Detect provider from account, use chatgpt.com/backend-api for Codex |
usage-monitor.ts:1424 |
if (provider === 'anthropic') adds beta headers |
Anthropic-specific anthropic-beta header |
Add else if (provider === 'openai') to add ChatGPT-Account-Id header |
token-refresh.ts:31 |
ANTHROPIC_TOKEN_ENDPOINT = 'https://console.anthropic.com/v1/oauth/token' |
Only Anthropic refresh endpoint | Route to auth.openai.com/oauth/token for Codex |
token-refresh.ts:37 |
CLAUDE_CODE_CLIENT_ID = '9d1c250a-...' |
Only Anthropic client ID | Use app_EMoamEEZ73f0CkXaXp7hrann for Codex |
UsageIndicator.tsx:118 |
provider === 'anthropic' && authType === 'oauth' |
Only Anthropic gets usage bars | Add || provider === 'openai' |
| File | Line(s) | What's hardcoded | What to do |
|---|---|---|---|
usage-monitor.ts:1040-1072 |
determineActiveProfile() |
Returns baseUrl: 'https://api.anthropic.com' for all OAuth |
Detect provider, return chatgpt.com/backend-api for Codex |
credential-utils.ts |
Keychain service names | "Claude Code-credentials" |
Codex tokens stored differently (file-based, not keychain) |
usage-monitor.ts:1513 |
if (provider === 'zai' || provider === 'zhipu') |
Provider-specific response unwrapping | Add Codex response parsing (different JSON structure) |
rate-limit-detector.ts:14 |
RATE_LIMIT_PATTERN |
Claude-specific: "Limit reached · resets..." |
Add Codex-specific patterns |
| IPC channel names | 'claude:usageUpdated', 'claude:allProfilesUsageUpdated' |
"claude" prefix | Cosmetic — rename to 'usage:updated' etc. (optional, low priority) |
| Item | What | Why low priority |
|---|---|---|
| Type naming | ClaudeUsageSnapshot → UsageSnapshot |
Structural refactor, types work as-is for Codex |
| IPC method names | requestUsageUpdate returns ClaudeUsageSnapshot |
Works fine, just naming |
claudeProfileId on ProviderAccount |
Only used for Anthropic OAuth | Codex doesn't need it |
These components already work for any provider and need NO changes:
| Component/Module | Why it's already generic |
|---|---|
profile-scorer.ts |
Scores by billingModel, usage thresholds, rate limit events — no provider checks |
rate-limit-manager.ts |
Stores/checks rate limit events — pure data, no provider logic |
operation-registry.ts |
Tracks running operations — no provider awareness |
ProviderAccount type |
Has provider field, billingModel, usage — works for any provider |
globalPriorityOrder |
Array of account IDs — provider-agnostic ordering |
useActiveProvider() hook |
Returns first account in priority order — generic |
ProviderAccountCard |
Shows usage bars when account.usage is populated — will work for Codex once data flows |
AddAccountDialog |
Already has separate Codex OAuth flow |
AuthStatusIndicator |
Already shows Codex-specific green badge |
| All i18n keys | Codex-specific labels already exist |
Create apps/desktop/src/main/claude-profile/codex-usage-fetcher.ts:
// Responsibilities:
// 1. Read Codex OAuth token (from our codex-auth.json)
// 2. Read account_id (from ~/.codex/auth.json or JWT decode)
// 3. Call GET https://chatgpt.com/backend-api/wham/usage
// 4. Parse response into ClaudeUsageSnapshot format
// 5. Handle 401 → refresh token via codex-oauth.ts
// 6. Handle 403 → mark as needsReauthenticationKey function:
async function fetchCodexUsage(accessToken: string, accountId?: string): Promise<ClaudeUsageSnapshot>Modify usage-monitor.ts:
- Add
chatgpt.comtoALLOWED_USAGE_API_DOMAINS - Add Codex to
PROVIDER_USAGE_ENDPOINTS - Update
determineActiveProfile()to detect Codex accounts fromglobalPriorityOrder - Update
getCredential()to read Codex OAuth token (fromcodex-auth.json) - Update
fetchUsageViaAPI()to handle Codex response format - Add Codex-specific headers (
ChatGPT-Account-Id) - Add Codex response parsing (different JSON structure than Anthropic)
Modify token-refresh.ts or create parallel Codex path:
- When refreshing a Codex token, use
auth.openai.com/oauth/tokenwith Codex client ID - When refreshing an Anthropic token, use
console.anthropic.com/v1/oauth/tokenwith Claude client ID - Provider detection: check the account's
providerfield, or detect from token prefix
UsageIndicator.tsx:118— Add|| provider === 'openai'tohasUsageMonitoring- That's it — the rest of the UI already handles usage bars, reset times, multi-profile display generically
- Add Codex-specific rate limit patterns to
rate-limit-detector.ts - Codex returns
"codexErrorInfo": "UsageLimitExceeded"on limit hit - Auto-swap logic in
profile-scorer.tsalready works — it just needs usage data populated
| Aspect | Anthropic (Claude Code) | OpenAI (Codex) |
|---|---|---|
| Usage endpoint | api.anthropic.com/api/oauth/usage |
chatgpt.com/backend-api/wham/usage |
| Auth header | Bearer <oauth_token> |
Bearer <access_token> + ChatGPT-Account-Id |
| Session window | ~5h | Configurable (limit_window_seconds) |
| Weekly window | 7 days | Configurable (limit_window_seconds) |
| Token source | Keychain (Claude Code-credentials) |
File (codex-auth.json) |
| Token refresh | console.anthropic.com/v1/oauth/token |
auth.openai.com/oauth/token |
| Client ID | 9d1c250a-e61b-44d9-88ed-5944d1962f5e |
app_EMoamEEZ73f0CkXaXp7hrann |
| Passive tracking | Not available | x-codex-* response headers |
| Rate limit error | "Limit reached · resets Dec 17..." |
"codexErrorInfo": "UsageLimitExceeded" |
| Profile isolation | ~/.claude-profiles/{name}/ dirs |
Single codex-auth.json file |
| Multi-account | Multiple config dirs in keychain | Single file (no multi-account yet) |
- Undocumented API —
chatgpt.com/backend-api/wham/usageis internal. The Codex CLI depends on it, so it's unlikely to break silently. - Account ID — May be required. Test without it first. If needed, decode from JWT or read
~/.codex/auth.json. - CORS — Not an issue (Electron main process = Node.js).
- Polling rate — Unknown if OpenAI rate-limits
wham/usage. Start conservatively (every 30-60s). - Multi-account Codex — Codex CLI doesn't support multiple accounts. We store one token file. If user has multiple Codex accounts, they'd need to re-auth each time (unlike Anthropic which supports multiple config dirs).