Skip to content

systems oauth usage tracker

Nik edited this page May 30, 2026 · 1 revision

OAuth usage tracker

Active contributors: Nik, Ran

Purpose

The OAuth usage tracker fetches quota windows for connected Claude and Codex OAuth accounts and publishes them to the "OAuth Quota Usage" section of the settings window. It talks to the provider usage endpoints directly over HTTPS using the stored OAuth tokens, so it has no dependency on the codex CLI or any external binary. The Claude path additionally refreshes expired access tokens in place before reading usage.

It is implemented entirely in src/Sources/OAuthUsageTracker.swift.

Directory layout

src/Sources/
  OAuthUsageTracker.swift   # OAuthUsageTracker + OAuthUsageWindow + OAuthAccountUsage
  SettingsView.swift        # owns the refresh button and renders the windows
  AuthStatus.swift          # AuthManager / AuthAccount consumed by refresh(...)

Credential files live under ~/.cli-proxy-api/ (one JSON file per account). The Claude refresh path writes updated tokens back into those same files.

Key abstractions

Type Role
OAuthUsageWindow One quota window (title, usedPercent, resetText, resetDate). Exposes remainingPercent = max(0, 100 - usedPercent). Identifiable + Equatable.
OAuthAccountUsage Per-account result: id, provider (ServiceType), email, isLoading, windows, error, updatedAt.
OAuthUsageTracker @MainActor ObservableObject. Publishes accounts and isRefreshing; owns the in-flight refreshTask.
OAuthUsageParsing Private constants: 15 s request timeout and the allowed character set for form-URL-encoding the refresh token.

How it works

refresh(codexAccounts:claudeAccounts:) is the single entry point. It cancels any prior refreshTask, filters each provider's accounts down to enabled and non-expired (!isDisabled && !isExpired), and short-circuits to an empty list when nothing qualifies. It then publishes loading placeholders, flips isRefreshing to true, and launches a withTaskGroup that fetches every account in parallel. Results are collected, sorted by provider then email (case-insensitive), and published back on the main actor unless the task was cancelled.

sequenceDiagram
    participant SV as SettingsView
    participant T as OAuthUsageTracker
    participant G as TaskGroup
    participant CX as chatgpt.com (Codex)
    participant CL as api.anthropic.com (Claude)
    participant F as ~/.cli-proxy-api files

    SV->>T: refresh(codexAccounts, claudeAccounts)
    T->>T: cancel prior refreshTask
    T->>T: filter enabled + non-expired
    T->>SV: publish loading placeholders, isRefreshing = true
    T->>G: withTaskGroup
    par per Codex account
        G->>CX: GET /backend-api/wham/usage (Bearer + ChatGPT-Account-Id)
        CX-->>G: rate_limit JSON
    and per Claude account
        G->>F: read token, check expiry
        opt expired
            G->>CL: POST platform.claude.com /v1/oauth/token (refresh)
            CL-->>G: new access/refresh
            G->>F: write tokens atomically
        end
        G->>CL: GET /api/oauth/usage (anthropic-beta oauth-2025-04-20)
        CL-->>G: bucket JSON (401/403 -> retry after refresh)
    end
    G-->>T: [OAuthAccountUsage]
    T->>T: sort by provider, then email
    T->>SV: publish accounts, isRefreshing = false
Loading

Codex fetch

fetchCodexUsage reads the access_token from the account file and issues GET https://chatgpt.com/backend-api/wham/usage with an Authorization: Bearer header, Accept: application/json, a codex-cli user agent, and the ChatGPT-Account-Id header when account_id is present. parseCodexWindows reads rate_limit.primary_window as the "5-hour" window and rate_limit.secondary_window as the "Weekly" window. If the rate_limit shape is missing it falls back to parseGenericWindows, which flattens nested dictionaries and heuristically labels windows ("5-hour", "Weekly", "Full", "Session") from key/value text and percent/reset fields.

Claude fetch

fetchClaudeUsage reads the token and checks the expired timestamp. If expired, it calls refreshClaudeTokens, which POSTs to https://platform.claude.com/v1/oauth/token with client_id, grant_type=refresh_token, and the form-URL-encoded refresh token. On success it writes the new access_token, refresh_token, recomputed expired (now + expires_in), and last_refresh back into the credential file atomically (.write(to:options:[.atomic])). It then calls executeClaudeUsageRequest, which issues GET https://api.anthropic.com/api/oauth/usage with anthropic-beta: oauth-2025-04-20. A 401/403 response triggers retryClaudeUsageAfterRefresh, which forces a token refresh and retries once. parseClaudeWindows reads named buckets: five_hour ("5-hour"), seven_day ("Weekly"), seven_day_opus, seven_day_sonnet, seven_day_oauth_apps, seven_day_cowork, and seven_day_omelette, reading each bucket's utilization and resets_at.

Parsing helpers

  • numberValue coerces Double, Int, NSNumber, or numeric String (stripping %) to Double.
  • Percent normalization treats any value <= 1 as a fraction and multiplies by 100.
  • resetText(for:) renders both a relative ("in 3 hours") and absolute (medium date + short time) form.
  • parseISO8601Date accepts timestamps with or without fractional seconds.
  • remainingPercent on each window is 100 - usedPercent, clamped at zero, and is what the settings UI displays as "% left".

Integration points

  • SettingsView owns the refresh button (refreshOAuthUsage) and renders results through oauthUsageDashboard / oauthUsageAccountRow / usageWindowRow. It also refreshes automatically on appear and whenever the enabled Codex/Claude account signature changes.
  • AuthManager (src/Sources/AuthStatus.swift) supplies the AuthAccount arrays passed into refresh(...); only enabled, non-expired accounts are queried.
  • Credential files: the Claude refresh path writes refreshed tokens back to the per-account JSON under ~/.cli-proxy-api/, so a usage refresh can quietly extend a session's access token.

Entry points for modification

  • Add or relabel Claude buckets: edit the knownBuckets list in parseClaudeWindows.
  • Change Codex window mapping: edit parseCodexWindows / codexWindow, or the heuristics in parseGenericWindows / windowTitle / windowRank.
  • Adjust request timeout or token encoding: edit OAuthUsageParsing.
  • Change refresh filtering or sort order: edit refresh(codexAccounts:claudeAccounts:).
  • Change token-refresh behavior (endpoint, client id, written fields): edit refreshClaudeTokens.

Key source files

File Role
src/Sources/OAuthUsageTracker.swift Tracker, structs, fetch/parse helpers, Claude token refresh.
src/Sources/SettingsView.swift Refresh button, dashboard rendering, auto-refresh triggers.
src/Sources/AuthStatus.swift AuthManager / AuthAccount source for account lists.

Related pages

Clone this wiki locally