feat(bar): native Claude Code + Codex subscription quota (safe, server-side)

Surface the logged-in Claude Code and Codex subscription quota as first-class
summary rows so the existing gauge and alert engine render them with no new UI.

Claude Code: read the native token (~/.claude/.credentials.json, macOS Keychain
fallback) and reuse the existing Anthropic usage fetch+normalize via a new
token-fed entry point. The /oauth/usage endpoint is hostile to polling, so the
fetch is server-side only behind a 10-minute on-demand cache, in-flight
coalescing, Retry-After + exponential backoff with jitter, a 3-strike circuit
breaker with a 15-minute cooldown, and serve-stale-on-failure; logged-out or
unsupported subscriptions never spend a token call. Codex: zero-network read of
the latest rate_limits from local session rollouts, omitted when absent.
Native rows side-load bounded so a slow or failed fetch degrades to CLIProxy
rows, never an error.
This commit is contained in:
Tam Nhu Tran committed 2026-06-09 13:37:38 -04:00
1 parent 0fed2982f5
commit dbeb0c543a
9 files changed
+1481 -21

No files matched your search

+64 -20
View File
@@ -218,25 +218,19 @@ function buildEmptyResult(
}
/**
* Fetch quota for a single Claude account.
* Run the Anthropic OAuth usage fetch loop for a known-good access token.
*
* This is the single Anthropic-call surface shared by both the CLIProxy-managed
* path (fetchClaudeQuota) and the native-login path (fetchClaudeQuotaWithToken).
* It owns the 401/403/404/429/5xx branch logic, the bounded retry loop, and the
* window normalization so neither caller re-implements the hostile-endpoint
* handling. The callers differ only in WHERE the token comes from.
*/
export async function fetchClaudeQuota(
async function runClaudeUsageFetch(
accessToken: string,
accountId: string,
verbose = false
verbose: boolean
): Promise<ClaudeQuotaResult> {
const authData = await readClaudeAuthData(accountId);
if (!authData) {
return buildEmptyResult('Auth file not found for Claude account', accountId);
}
if (authData.isExpired) {
return buildEmptyResult(
'Token expired - re-authenticate with ccs cliproxy auth claude',
accountId,
true
);
}
let lastError = 'Unknown error';
for (let attempt = 1; attempt <= CLAUDE_QUOTA_MAX_ATTEMPTS; attempt++) {
@@ -248,7 +242,7 @@ export async function fetchClaudeQuota(
method: 'GET',
signal: controller.signal,
headers: {
Authorization: `Bearer ${authData.accessToken}`,
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
@@ -283,6 +277,9 @@ export async function fetchClaudeQuota(
lastError =
(await readResponseErrorMessage(response)) ||
`Claude OAuth usage API error: ${response.status}`;
// Surface the upstream status + Retry-After so an outer caller (the
// native collector's circuit-breaker) can honor backoff guidance.
const retryAfter = response.headers.get('retry-after');
if (
attempt < CLAUDE_QUOTA_MAX_ATTEMPTS &&
(response.status === 429 || response.status >= 500)
@@ -291,7 +288,12 @@ export async function fetchClaudeQuota(
continue;
}
clearTimeout(timeoutId);
return buildEmptyResult(lastError, accountId);
return {
...buildEmptyResult(lastError, accountId),
httpStatus: response.status,
retryable: response.status === 429 || response.status >= 500,
...(retryAfter ? { errorDetail: `retry-after:${retryAfter}` } : {}),
};
}
let payload: unknown;
@@ -338,12 +340,54 @@ export async function fetchClaudeQuota(
if (attempt >= CLAUDE_QUOTA_MAX_ATTEMPTS) {
clearTimeout(timeoutId);
return buildEmptyResult(lastError, accountId);
return { ...buildEmptyResult(lastError, accountId), retryable: true };
}
}
}
return buildEmptyResult(lastError, accountId);
return { ...buildEmptyResult(lastError, accountId), retryable: true };
}
/**
* Fetch quota using a directly-supplied native OAuth access token.
*
* Reuses the exact Anthropic call + normalization as fetchClaudeQuota; the only
* difference is the token source (the logged-in Claude Code credential rather
* than a CLIProxy-managed auth file). Lives in this file so it can share the
* file-private beta header, timeout, attempt count, and branch logic.
*/
export async function fetchClaudeQuotaWithToken(
accessToken: string,
accountId = 'claude-code',
verbose = false
): Promise<ClaudeQuotaResult> {
if (!accessToken || accessToken.trim().length === 0) {
return buildEmptyResult('Missing native Claude access token', accountId, true);
}
return runClaudeUsageFetch(accessToken.trim(), accountId, verbose);
}
/**
* Fetch quota for a single Claude account.
*/
export async function fetchClaudeQuota(
accountId: string,
verbose = false
): Promise<ClaudeQuotaResult> {
const authData = await readClaudeAuthData(accountId);
if (!authData) {
return buildEmptyResult('Auth file not found for Claude account', accountId);
}
if (authData.isExpired) {
return buildEmptyResult(
'Token expired - re-authenticate with ccs cliproxy auth claude',
accountId,
true
);
}
return runClaudeUsageFetch(authData.accessToken, accountId, verbose);
}
/**
+24 -1
View File
@@ -104,6 +104,13 @@ export interface BarRouterDeps {
* audit shells out via a synchronous execSync that must never run here.
*/
runHealthChecks?: () => Promise<HealthReport>;
/**
* Native subscription quota rows (Claude Code + Codex). Defaults to an empty
* async so older tests that build deps without it keep passing. The native
* collector owns its own long-TTL cache + safety controls, so this is cheap
* to call per request.
*/
getNativeAccountRows?: () => Promise<BarSummaryRow[]>;
}
// ============================================================================
@@ -122,6 +129,13 @@ const PER_ACCOUNT_TIMEOUT_MS = 5_000;
/** Bound for the cost side-load so a slow snapshot read can't dominate the response. */
const SIDELOAD_TIMEOUT_MS = 1_500;
/**
* Bound for the native-subscription side-load. A slow or failed native fetch
* resolves to [] so the response paints CLIProxy rows only — never errors, never
* blocks. Native rows have their own 10-min cache, so the common path is instant.
*/
const NATIVE_SIDELOAD_TIMEOUT_MS = 1_500;
/** Timestamp of the last successful force-fresh pull (epoch ms, 0 = never) */
let lastForceFreshAt = 0;
@@ -475,7 +489,14 @@ export function createBarRouter(deps: BarRouterDeps): Router {
});
const rows = await Promise.race([gather, deadline]);
res.json(rows);
// Native subscription rows (Claude Code + Codex) are side-loaded AFTER the
// CLIProxy rows resolve, bounded so a slow/failed native fetch degrades to
// [] rather than blocking or erroring the response.
const getNative = deps.getNativeAccountRows ?? (async () => [] as BarSummaryRow[]);
const nativeRows = (await withTimeout(getNative(), NATIVE_SIDELOAD_TIMEOUT_MS)) ?? [];
res.json([...rows, ...nativeRows]);
} catch (err) {
console.error('[bar-routes] /summary error:', (err as Error).message);
res.status(500).json({ error: 'Internal server error' });
@@ -523,6 +544,7 @@ import { fetchAccountQuota } from '../../cliproxy/quota/quota-fetcher';
import { getTodayCostByAccount } from '../usage/data-aggregator';
import { loadCliproxySnapshotDetails } from '../usage/cliproxy-snapshot-reader';
import { getCachedDailyData, getCachedHourlyData } from '../usage/aggregator';
import { getNativeAccountRows } from '../usage/native-quota-collector';
/** Production bar router — wired to real dependencies */
const barRouter: Router = createBarRouter({
@@ -535,6 +557,7 @@ const barRouter: Router = createBarRouter({
loadCliproxyDetails: loadCliproxySnapshotDetails,
loadDailyUsage: () => getCachedDailyData(),
loadHourlyUsage: () => getCachedHourlyData(),
getNativeAccountRows: () => getNativeAccountRows(),
});
export default barRouter;
@@ -0,0 +1,139 @@
/**
* Native Claude Code credential reader.
*
* Reads the LOGGED-IN Claude Code OAuth token so the bar can show the user's
* own subscription quota (Max/Pro/Team) without going through CLIProxy-managed
* auth files.
*
* File-first, Keychain-fallback: we read ~/.claude/.credentials.json directly
* because hitting the macOS Keychain pops a permission dialog. The Keychain
* fallback is load-bearing on machines where Claude Code stores the token there
* instead of on disk, so it must be kept even though the file path is preferred.
*
* Only the user's own token is read here; the single read-only Anthropic usage
* endpoint is the only thing that ever sees it (see native-quota-collector).
*/
import { existsSync, readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import * as os from 'node:os';
import * as path from 'node:path';
/** Shape of the relevant slice of ~/.claude/.credentials.json */
export interface ClaudeNativeCredentials {
claudeAiOauth?: {
accessToken?: string;
subscriptionType?: string;
rateLimitTier?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
/** Injectable seams so unit tests never touch the real fs/Keychain. */
export interface CredentialReaderDeps {
platform?: NodeJS.Platform;
homedir?: string;
existsSyncImpl?: (p: string) => boolean;
readFileSyncImpl?: (p: string) => string;
execSyncImpl?: (cmd: string, opts: Record<string, unknown>) => string | Buffer;
}
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
const KEYCHAIN_TIMEOUT_MS = 5000;
/** Subscription types that mean "no real subscription" -> skip the fetch. */
const UNSUPPORTED_SUBSCRIPTION_TYPES = new Set(['', 'free', 'none']);
/** rateLimitTier values that imply an entitled subscription. */
const SUPPORTED_RATE_LIMIT_TIER = /claude|max|pro|team|enterprise/;
function parseCredentials(raw: string): ClaudeNativeCredentials | null {
try {
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as ClaudeNativeCredentials;
}
} catch {
// fall through
}
return null;
}
/**
* Read the native Claude Code credentials.
*
* Order: the on-disk credentials file first (no prompt), then the macOS
* Keychain as a fallback. Returns null when neither source yields a parseable
* object.
*/
export function readClaudeCredentials(
deps: CredentialReaderDeps = {}
): ClaudeNativeCredentials | null {
const platform = deps.platform ?? os.platform();
const homedir = deps.homedir ?? os.homedir();
const existsImpl = deps.existsSyncImpl ?? existsSync;
const readImpl = deps.readFileSyncImpl ?? ((p: string) => readFileSync(p, 'utf8'));
const execImpl = deps.execSyncImpl ?? execSync;
const credentialsPath = path.join(homedir, '.claude', '.credentials.json');
if (existsImpl(credentialsPath)) {
try {
const parsed = parseCredentials(readImpl(credentialsPath));
if (parsed) return parsed;
} catch {
// fall through to Keychain
}
}
if (platform === 'darwin') {
try {
const out = execImpl(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -w`, {
timeout: KEYCHAIN_TIMEOUT_MS,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
const raw = (typeof out === 'string' ? out : out.toString('utf8')).trim();
if (raw) {
const parsed = parseCredentials(raw);
if (parsed) return parsed;
}
} catch {
// no Keychain entry / access denied -> null
}
}
return null;
}
/** Pull the OAuth access token, or null when absent. */
export function getAccessToken(creds: ClaudeNativeCredentials | null): string | null {
const token = creds?.claudeAiOauth?.accessToken;
return typeof token === 'string' && token.trim().length > 0 ? token : null;
}
/** Pull the subscription tier (e.g. "max" / "pro"), or null. */
export function getSubscriptionTier(creds: ClaudeNativeCredentials | null): string | null {
const tier = creds?.claudeAiOauth?.subscriptionType;
return typeof tier === 'string' && tier.trim().length > 0 ? tier.trim() : null;
}
/**
* True when the credentials describe a real, entitled subscription.
*
* Gating on this BEFORE fetching means a free/logged-out user never spends a
* token call against the hostile usage endpoint and never gets a phantom row.
*/
export function hasSupportedSubscription(creds: ClaudeNativeCredentials | null): boolean {
const subscriptionType = String(creds?.claudeAiOauth?.subscriptionType ?? '')
.trim()
.toLowerCase();
if (subscriptionType && !UNSUPPORTED_SUBSCRIPTION_TYPES.has(subscriptionType)) {
return true;
}
const rateLimitTier = String(creds?.claudeAiOauth?.rateLimitTier ?? '')
.trim()
.toLowerCase();
return SUPPORTED_RATE_LIMIT_TIER.test(rateLimitTier);
}
@@ -0,0 +1,221 @@
/**
* Codex local quota collector (zero network).
*
* Codex writes a `rate_limits` object into its rollout session logs
* (~/.codex/sessions/<y>/<m>/<d>/rollout-*.jsonl). We read the most recent
* session's last non-null rate_limits to surface the user's Codex subscription
* quota WITHOUT any network call — pure local file read.
*
* Exec-mode sessions often never emit rate_limits (the field stays null); in
* that case we return null so the bar simply omits the Codex row rather than
* inventing a fake one.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { resolveCodexConfigPaths } from '../services/codex-dashboard-service';
/** Normalized Codex quota snapshot from a local session log. */
export interface CodexLocalQuota {
/** Remaining percentage (0-100): min across primary/secondary windows. */
quotaPercentage: number;
/** ISO timestamp of the soonest window reset, null if unknown. */
nextReset: string | null;
/** plan_type from the session (e.g. "pro"/"plus"), null if absent. */
tier: string | null;
/** True when the source file is older than the freshness window. */
stale: boolean;
}
/** Injectable seams for deterministic tests (no real fs / no tail subprocess). */
export interface CodexLocalQuotaDeps {
env?: NodeJS.ProcessEnv;
homeDir?: string;
existsSyncImpl?: (p: string) => boolean;
readdirImpl?: (dir: string) => fs.Dirent[];
statMtimeMsImpl?: (p: string) => number;
/** Returns the last N lines of a file (default: Bun tail). */
tailLinesImpl?: (file: string, lines: number) => Promise<string[]>;
now?: number;
}
/** Lines to scan from the tail; rate_limits sits near the end of a session. */
const TAIL_LINES = 200;
/** A source older than this is reported stale (but still emitted). */
const STALE_AFTER_MS = 5 * 60 * 1000;
interface CodexRateWindow {
usedPercent: number;
resetsAtSeconds: number | null;
}
interface CodexRateLimits {
primary: CodexRateWindow | null;
secondary: CodexRateWindow | null;
planType: string | null;
}
function asObject(value: unknown): Record<string, unknown> | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function asFiniteNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function parseWindow(value: unknown): CodexRateWindow | null {
const obj = asObject(value);
if (!obj) return null;
const usedPercent = asFiniteNumber(obj['used_percent']);
if (usedPercent === null) return null;
return {
usedPercent,
resetsAtSeconds: asFiniteNumber(obj['resets_at']),
};
}
/**
* Extract a non-null rate_limits object from a parsed JSONL line.
* rate_limits lives under `payload` in the token_count event.
*/
function extractRateLimits(line: string): CodexRateLimits | null {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
return null;
}
const root = asObject(parsed);
if (!root) return null;
const payload = asObject(root['payload']) ?? root;
const rateLimits = asObject(payload['rate_limits']);
if (!rateLimits) return null;
const primary = parseWindow(rateLimits['primary']);
const secondary = parseWindow(rateLimits['secondary']);
// A rate_limits object with neither window carries no usable signal.
if (!primary && !secondary) return null;
const planType =
typeof rateLimits['plan_type'] === 'string' && rateLimits['plan_type'].trim().length > 0
? (rateLimits['plan_type'] as string).trim()
: null;
return { primary, secondary, planType };
}
/** Recursive rollout-*.jsonl walker, lexicographically sorted (ISO ts in name). */
function collectRolloutFiles(
dir: string,
existsImpl: (p: string) => boolean,
readdirImpl: (d: string) => fs.Dirent[]
): string[] {
if (!existsImpl(dir)) return [];
const files: string[] = [];
for (const entry of readdirImpl(dir)) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectRolloutFiles(entryPath, existsImpl, readdirImpl));
continue;
}
if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
files.push(entryPath);
}
}
return files.sort();
}
/**
* Default tail using Bun.spawn (macOS-safe: no tac, no GNU timeout).
* Reads the last `lines` lines so we never load a huge session into memory.
*/
async function defaultTailLines(file: string, lines: number): Promise<string[]> {
const proc = Bun.spawn(['tail', `-${lines}`, file], {
stdout: 'pipe',
stderr: 'ignore',
});
const text = await new Response(proc.stdout).text();
await proc.exited;
return text.split('\n').filter((l) => l.trim().length > 0);
}
function computeQuotaPercentage(rate: CodexRateLimits): number {
const remaining: number[] = [];
if (rate.primary) remaining.push(100 - rate.primary.usedPercent);
if (rate.secondary) remaining.push(100 - rate.secondary.usedPercent);
// Clamp into [0,100] to guard against odd upstream values.
return Math.max(0, Math.min(100, Math.min(...remaining)));
}
function computeNextReset(rate: CodexRateLimits): string | null {
const resets: number[] = [];
if (rate.primary?.resetsAtSeconds !== null && rate.primary?.resetsAtSeconds !== undefined) {
resets.push(rate.primary.resetsAtSeconds);
}
if (rate.secondary?.resetsAtSeconds !== null && rate.secondary?.resetsAtSeconds !== undefined) {
resets.push(rate.secondary.resetsAtSeconds);
}
if (resets.length === 0) return null;
return new Date(Math.min(...resets) * 1000).toISOString();
}
/**
* Read the latest Codex session's most recent rate_limits and normalize it.
* Returns null when no session carries a usable rate_limits object.
*/
export async function getCodexLocalQuota(
deps: CodexLocalQuotaDeps = {}
): Promise<CodexLocalQuota | null> {
const existsImpl = deps.existsSyncImpl ?? fs.existsSync;
const readdirImpl =
deps.readdirImpl ?? ((dir: string) => fs.readdirSync(dir, { withFileTypes: true }));
const statMtimeMsImpl = deps.statMtimeMsImpl ?? ((p: string) => fs.statSync(p).mtimeMs);
const tailLinesImpl = deps.tailLinesImpl ?? defaultTailLines;
const now = deps.now ?? Date.now();
const { baseDir } = resolveCodexConfigPaths({ env: deps.env, homeDir: deps.homeDir });
const sessionsDir = path.join(baseDir, 'sessions');
const rolloutFiles = collectRolloutFiles(sessionsDir, existsImpl, readdirImpl);
if (rolloutFiles.length === 0) return null;
// Filename carries an ISO timestamp, so the lexicographically-last file is
// the most recent session.
const latest = rolloutFiles[rolloutFiles.length - 1];
let lines: string[];
try {
lines = await tailLinesImpl(latest, TAIL_LINES);
} catch {
return null;
}
// Iterate backwards for the most recent non-null rate_limits.
let rate: CodexRateLimits | null = null;
for (let i = lines.length - 1; i >= 0; i--) {
const found = extractRateLimits(lines[i]);
if (found) {
rate = found;
break;
}
}
if (!rate) return null;
let stale = false;
try {
stale = now - statMtimeMsImpl(latest) > STALE_AFTER_MS;
} catch {
// Unknown mtime -> treat as fresh; the data itself is still valid.
stale = false;
}
return {
quotaPercentage: computeQuotaPercentage(rate),
nextReset: computeNextReset(rate),
tier: rate.planType,
stale,
};
}
@@ -0,0 +1,376 @@
/**
* Native subscription quota collector — the ONLY server-side fetch surface for
* the user's own Claude Code + Codex subscription quota.
*
* The macOS bar reads localhost /api/bar/summary and NEVER calls Anthropic. All
* Anthropic traffic originates here, under strict safety controls, because the
* OAuth usage endpoint is undocumented and hostile to polling (persistent 429s,
* no Retry-After, first-party-only policy). The controls below exist to protect
* the user's account:
*
* - long TTL (10 min) on-demand cache, never a tight timer loop
* - in-flight coalescing so concurrent /summary calls share one fetch
* - Retry-After honored; exponential backoff + jitter on 429/5xx
* - circuit breaker stops calling after repeated 429s for a cooldown
* - serve-stale-on-failure; only omit a row when there is genuinely no data
*
* Codex is a pure local file read (no network), so it skips the network guards.
*/
import {
readClaudeCredentials,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
type ClaudeNativeCredentials,
} from './claude-native-credentials';
import { fetchClaudeQuotaWithToken } from '../../cliproxy/quota/quota-fetcher-claude';
import { getCodexLocalQuota, type CodexLocalQuota } from './codex-local-quota-collector';
import type { ClaudeQuotaResult } from '../../cliproxy/quota/quota-types';
import type { BarSummaryRow } from '../routes/bar-routes';
// ============================================================================
// Safety constants (concrete, named, module-level)
// ============================================================================
/** On-demand cache TTL. Floor is 5 min; we use 10 min because the bar polls
* /summary far more often than a hook fires. */
const NATIVE_QUOTA_TTL_MS = 600_000; // 10 minutes
/** Exponential backoff base; delay = min(base * 2^n, MAX) + jitter. */
const RETRY_BASE_MS = 1_000;
/** Ceiling for any single backoff / Retry-After cooldown derived from one call. */
const MAX_BACKOFF_MS = 60_000; // 1 minute
/** Jitter added to backoff to avoid synchronized retries. */
const JITTER_MAX_MS = 500;
/** Consecutive 429s that trip the breaker open. */
const CB_TRIP_THRESHOLD = 3;
/** How long the breaker stays open (zero network) once tripped. */
const CB_COOLDOWN_MS = 900_000; // 15 minutes
const CLAUDE_PROVIDER = 'claude-code';
const CODEX_PROVIDER = 'codex';
// ============================================================================
// Injectable dependencies (tests inject mocks; never live Anthropic in CI)
// ============================================================================
export interface NativeQuotaDeps {
/** Read the native Claude Code credentials. */
readCredentials?: () => ClaudeNativeCredentials | null;
/** Fetch Claude quota with a directly-supplied native token. */
fetchClaudeQuota?: (accessToken: string, accountId?: string) => Promise<ClaudeQuotaResult>;
/** Read Codex quota from local session logs (zero network). */
getCodexQuota?: () => Promise<CodexLocalQuota | null>;
/** Clock seam for deterministic backoff/TTL/breaker tests. */
now?: () => number;
/** Sleep seam (no real delay in tests). */
sleep?: (ms: number) => Promise<void>;
}
// ============================================================================
// Per-provider mutable state (module-scoped; reset() for tests)
// ============================================================================
interface ProviderState {
/** Last successfully-built row, kept for stale-on-fail and TTL serving. */
cachedRow: BarSummaryRow | null;
/** Epoch ms when cachedRow was produced. */
cachedAt: number;
/** Shared in-flight promise; concurrent callers await this, not a new fetch. */
pending: Promise<BarSummaryRow | null> | null;
/** Consecutive 429 count toward the breaker threshold. */
consecutive429: number;
/** Epoch ms until which the breaker is open (no network). */
breakerOpenUntil: number;
/** Epoch ms until which a Retry-After / backoff cooldown holds. */
cooldownUntil: number;
/** Attempt counter feeding exponential backoff. */
backoffAttempt: number;
}
function freshProviderState(): ProviderState {
return {
cachedRow: null,
cachedAt: 0,
pending: null,
consecutive429: 0,
breakerOpenUntil: 0,
cooldownUntil: 0,
backoffAttempt: 0,
};
}
const claudeState = freshProviderState();
/** Reset all module state. Tests call this to avoid cross-test pollution. */
export function resetNativeQuotaState(): void {
Object.assign(claudeState, freshProviderState());
}
// ============================================================================
// Helpers
// ============================================================================
function defaultSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Parse Retry-After (seconds or HTTP-date) into ms, capped at MAX_BACKOFF_MS. */
function parseRetryAfterMs(detail: string | undefined, now: number): number | null {
if (!detail) return null;
const match = /retry-after:(.+)$/.exec(detail);
if (!match) return null;
const raw = match[1].trim();
const asSeconds = Number(raw);
if (Number.isFinite(asSeconds)) {
return Math.min(Math.max(0, asSeconds) * 1000, MAX_BACKOFF_MS);
}
const asDate = Date.parse(raw);
if (Number.isFinite(asDate)) {
return Math.min(Math.max(0, asDate - now), MAX_BACKOFF_MS);
}
return null;
}
function computeBackoffMs(attempt: number): number {
const exp = Math.min(RETRY_BASE_MS * 2 ** attempt, MAX_BACKOFF_MS);
const jitter = Math.floor(Math.random() * JITTER_MAX_MS);
return exp + jitter;
}
/**
* Best-guess remaining percentage across the 5h + weekly core windows, mirroring
* the CLIProxy quota-manager derivation: min of remaining across non-overage
* windows, falling back to all windows if core summary is absent.
*/
function deriveClaudeQuotaPercentage(quota: ClaudeQuotaResult): number | null {
const coreWindows = [quota.coreUsage?.fiveHour, quota.coreUsage?.weekly].filter(
(w): w is NonNullable<typeof w> => !!w
);
if (coreWindows.length > 0) {
return Math.min(...coreWindows.map((w) => w.remainingPercent));
}
const usageWindows = quota.windows.filter((w) => w.rateLimitType !== 'overage');
if (usageWindows.length > 0) {
return Math.min(...usageWindows.map((w) => w.remainingPercent));
}
return null;
}
/** Soonest non-null reset ISO across the two core windows. */
function deriveClaudeNextReset(quota: ClaudeQuotaResult): string | null {
const resets = [quota.coreUsage?.fiveHour?.resetAt, quota.coreUsage?.weekly?.resetAt]
.filter((r): r is string => typeof r === 'string')
.map((r) => ({ iso: r, ms: new Date(r).getTime() }))
.filter((r) => Number.isFinite(r.ms))
.sort((a, b) => a.ms - b.ms);
return resets.length > 0 ? resets[0].iso : null;
}
function buildClaudeRow(quota: ClaudeQuotaResult, tier: string | null, now: number): BarSummaryRow {
return {
account_id: CLAUDE_PROVIDER,
provider: CLAUDE_PROVIDER,
displayName: 'Claude Code',
tier,
paused: false,
quota_percentage: deriveClaudeQuotaPercentage(quota),
quotaStatus: 'ok',
next_reset: deriveClaudeNextReset(quota),
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'ok',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: false,
};
}
function buildCodexRow(quota: CodexLocalQuota, now: number): BarSummaryRow {
return {
account_id: CODEX_PROVIDER,
provider: CODEX_PROVIDER,
displayName: 'Codex',
tier: quota.tier,
paused: false,
quota_percentage: quota.quotaPercentage,
quotaStatus: 'ok',
next_reset: quota.nextReset,
is_default: false,
last_activity_at: null,
today_cost: null,
// Codex is a local read; a stale source still reflects real usage so we keep
// quotaStatus 'ok' but flag health 'warning' to hint freshness.
health: quota.stale ? 'warning' : 'ok',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: false,
};
}
/** Return the cached row marked cached=true (used for TTL + stale serving). */
function serveCached(state: ProviderState): BarSummaryRow | null {
if (!state.cachedRow) return null;
return { ...state.cachedRow, cached: true };
}
// ============================================================================
// Claude path with full safety controls
// ============================================================================
async function collectClaudeRow(deps: NativeQuotaDeps): Promise<BarSummaryRow | null> {
const now = (deps.now ?? Date.now)();
const state = claudeState;
// Serve from cache while within TTL — on-demand only, NO network.
if (state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
return serveCached(state);
}
// Breaker open or cooldown active -> zero network, serve stale (may be null).
if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
return serveCached(state);
}
// Coalesce: concurrent callers past TTL share one in-flight fetch.
if (state.pending) {
return state.pending;
}
const readCredentials = deps.readCredentials ?? readClaudeCredentials;
const fetchQuota = deps.fetchClaudeQuota ?? fetchClaudeQuotaWithToken;
const sleep = deps.sleep ?? defaultSleep;
state.pending = (async (): Promise<BarSummaryRow | null> => {
try {
const creds = readCredentials();
// No token / unsupported subscription -> never spend a call, omit the row.
if (!creds || !hasSupportedSubscription(creds)) {
return serveCached(state);
}
const token = getAccessToken(creds);
if (!token) {
return serveCached(state);
}
const tier = getSubscriptionTier(creds);
const quota = await fetchQuota(token, CLAUDE_PROVIDER);
if (quota.success) {
// Success closes the breaker and clears backoff.
state.consecutive429 = 0;
state.breakerOpenUntil = 0;
state.cooldownUntil = 0;
state.backoffAttempt = 0;
const row = buildClaudeRow(quota, tier, now);
state.cachedRow = row;
state.cachedAt = now;
return { ...row, cached: false };
}
// 401 -> token expired. Emit a reauth row so the bar can prompt; this is
// a real, actionable state distinct from a transient failure.
if (quota.needsReauth) {
const row: BarSummaryRow = {
account_id: CLAUDE_PROVIDER,
provider: CLAUDE_PROVIDER,
displayName: 'Claude Code',
tier,
paused: false,
quota_percentage: null,
quotaStatus: 'error',
next_reset: null,
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'error',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: true,
};
// Do not cache the reauth row as a good value; it should re-evaluate
// once the user re-auths. But return it now.
return row;
}
// 429 / 5xx / transient. Apply backoff + breaker, then serve stale.
const is429 = quota.httpStatus === 429;
if (is429) {
state.consecutive429 += 1;
if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
state.breakerOpenUntil = now + CB_COOLDOWN_MS;
}
const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
// We do NOT sleep-then-retry inside the request path (that would burn
// the request budget). The cooldown gates the NEXT call instead.
void sleep; // retained as an injectable seam for future inline retry
} else if (quota.retryable) {
const backoff = computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
}
// Serve last good row on failure; omit if we never succeeded.
return serveCached(state);
} catch {
// Network/parse rejection -> treat as transient, serve stale.
const backoff = computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
return serveCached(state);
} finally {
state.pending = null;
}
})();
return state.pending;
}
// ============================================================================
// Codex path (local read, no network guards needed)
// ============================================================================
async function collectCodexRow(deps: NativeQuotaDeps): Promise<BarSummaryRow | null> {
const now = (deps.now ?? Date.now)();
const getCodex = deps.getCodexQuota ?? getCodexLocalQuota;
try {
const quota = await getCodex();
if (!quota) return null; // exec-mode / no rate_limits -> omit the row
return buildCodexRow(quota, now);
} catch {
return null;
}
}
// ============================================================================
// Public entry point
// ============================================================================
/**
* Build the native subscription rows (Claude Code + Codex) for /summary.
*
* Each path is independently try/caught so one failing source never blocks the
* other or the response. Returns only rows that represent real data.
*/
export async function getNativeAccountRows(deps: NativeQuotaDeps = {}): Promise<BarSummaryRow[]> {
const [claude, codex] = await Promise.all([
collectClaudeRow(deps).catch(() => null),
collectCodexRow(deps).catch(() => null),
]);
const rows: BarSummaryRow[] = [];
if (claude) rows.push(claude);
if (codex) rows.push(codex);
return rows;
}