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

+103
View File
@@ -986,3 +986,106 @@ describe('today_cost: duplicate-email accounts get null (finding #11)', () => {
expect(body[0].today_cost).toBeCloseTo(3.75);
});
});
// ============================================================================
// Native subscription rows side-load into /summary
// ============================================================================
describe('/summary native subscription rows', () => {
function nativeRow(provider: 'claude-code' | 'codex'): BarSummaryRow {
return {
account_id: provider,
provider,
displayName: provider === 'codex' ? 'Codex' : 'Claude Code',
tier: provider === 'codex' ? 'pro' : 'max',
paused: false,
quota_percentage: 42,
quotaStatus: 'ok',
next_reset: '2026-06-09T20:00:00.000Z',
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'ok',
cached: false,
fetchedAt: '2026-06-09T14:00:00.000Z',
needsReauth: false,
};
}
async function buildRouter(getNativeAccountRows: () => Promise<BarSummaryRow[]>) {
const { createBarRouter, resetForceFreshDebounce: resetDebounce } = await import(
'../../../src/web-server/routes/bar-routes'
);
const app = express();
app.use(express.json());
const cliproxyAccount = makeAccountInfo({ id: 'pool@example.com', provider: 'agy' });
const router = createBarRouter({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAllAccountsSummary: () => ({ agy: [cliproxyAccount] }) as any,
getCachedQuota: () => null,
setCachedQuota: () => {},
invalidateQuotaCache: () => {},
fetchAccountQuota: async () => makeQuotaResult(),
getTodayCostByAccount: () => ({}),
loadCliproxyDetails: async () => [],
loadDailyUsage: async () => [],
loadHourlyUsage: async () => [],
runHealthChecks: async () => makeHealthReport(),
getNativeAccountRows,
});
app.use('/api/bar', router);
const srv = await new Promise<Server>((resolve, reject) => {
const instance = app.listen(0, '127.0.0.1');
instance.once('error', reject);
instance.once('listening', () => resolve(instance));
});
const addr = srv.address();
if (!addr || typeof addr === 'string') throw new Error('No server address');
resetDebounce();
return { srv, url: `http://127.0.0.1:${(addr as { port: number }).port}` };
}
it('appends native rows after the CLIProxy rows', async () => {
const { srv, url } = await buildRouter(async () => [
nativeRow('claude-code'),
nativeRow('codex'),
]);
const { body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(body.length).toBe(3);
expect(body[0].provider).toBe('agy'); // CLIProxy row first
expect(body[1].provider).toBe('claude-code');
expect(body[2].provider).toBe('codex');
});
it('degrades to CLIProxy-only rows when the native fetch never resolves (bounded side-load)', async () => {
const { srv, url } = await buildRouter(() => new Promise<BarSummaryRow[]>(() => {}));
const start = Date.now();
const { status, body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
const elapsed = Date.now() - start;
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(status).toBe(200);
expect(body.length).toBe(1);
expect(body[0].provider).toBe('agy');
// Must not block longer than the native side-load budget (+ small slack).
expect(elapsed).toBeLessThan(2_500);
});
it('returns CLIProxy-only rows (never 500) when the native fetch rejects', async () => {
const { srv, url } = await buildRouter(async () => {
throw new Error('native blew up');
});
const { status, body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(status).toBe(200);
expect(body.length).toBe(1);
expect(body[0].provider).toBe('agy');
});
});
@@ -0,0 +1,126 @@
/**
* Tests for the native Claude Code credential reader.
*
* All fs / Keychain access is injected, so these tests never touch the real
* filesystem or pop a macOS Keychain prompt.
*/
import { describe, expect, it } from 'bun:test';
import {
readClaudeCredentials,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
type ClaudeNativeCredentials,
} from '../../../src/web-server/usage/claude-native-credentials';
function makeCreds(overrides: Record<string, unknown> = {}): ClaudeNativeCredentials {
return {
claudeAiOauth: {
accessToken: 'tok-abc',
subscriptionType: 'max',
...overrides,
},
};
}
describe('readClaudeCredentials', () => {
it('parses the on-disk credentials file when present (file-first, no Keychain)', () => {
let keychainCalled = false;
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => true,
readFileSyncImpl: () => JSON.stringify(makeCreds()),
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds?.claudeAiOauth?.accessToken).toBe('tok-abc');
// File present means the Keychain must NOT be consulted (avoids prompt).
expect(keychainCalled).toBe(false);
});
it('falls back to the macOS Keychain when the file is absent', () => {
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('should not read file');
},
execSyncImpl: () => JSON.stringify(makeCreds({ subscriptionType: 'pro' })),
});
expect(creds?.claudeAiOauth?.subscriptionType).toBe('pro');
});
it('returns null when both file and Keychain are absent', () => {
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
throw new Error('no keychain entry');
},
});
expect(creds).toBeNull();
});
it('does not consult the Keychain on non-darwin platforms', () => {
let keychainCalled = false;
const creds = readClaudeCredentials({
platform: 'linux',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds).toBeNull();
expect(keychainCalled).toBe(false);
});
});
describe('hasSupportedSubscription', () => {
it.each(['', 'free', 'none'])('returns false for unsupported subscriptionType %p', (sub) => {
expect(hasSupportedSubscription(makeCreds({ subscriptionType: sub }))).toBe(false);
});
it.each(['max', 'pro', 'team', 'enterprise'])(
'returns true for supported subscriptionType %p',
(sub) => {
expect(hasSupportedSubscription(makeCreds({ subscriptionType: sub }))).toBe(true);
}
);
it('returns true via rateLimitTier regex when subscriptionType is empty', () => {
const creds = makeCreds({ subscriptionType: '', rateLimitTier: 'claude_max_20x' });
expect(hasSupportedSubscription(creds)).toBe(true);
});
it('returns false for null credentials', () => {
expect(hasSupportedSubscription(null)).toBe(false);
});
});
describe('token + tier extraction', () => {
it('getAccessToken returns the token or null', () => {
expect(getAccessToken(makeCreds())).toBe('tok-abc');
expect(getAccessToken(makeCreds({ accessToken: '' }))).toBeNull();
expect(getAccessToken(null)).toBeNull();
});
it('getSubscriptionTier returns the tier or null', () => {
expect(getSubscriptionTier(makeCreds({ subscriptionType: 'max' }))).toBe('max');
expect(getSubscriptionTier(makeCreds({ subscriptionType: '' }))).toBeNull();
expect(getSubscriptionTier(null)).toBeNull();
});
});
@@ -0,0 +1,100 @@
/**
* Tests for the Codex local quota collector (zero network).
*
* Uses a temp fixture rollout-*.jsonl read via the real Bun.spawn(['tail', ...])
* default impl (macOS-safe) plus injected fs seams for the directory walk.
*/
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { getCodexLocalQuota } from '../../../src/web-server/usage/codex-local-quota-collector';
let tmpDir: string;
let codexHome: string;
let sessionsDir: string;
function tokenCountLine(rateLimits: unknown): string {
return JSON.stringify({
timestamp: '2026-06-09T14:36:48.896Z',
type: 'event_msg',
payload: { type: 'token_count', info: {}, rate_limits: rateLimits },
});
}
function writeRollout(name: string, lines: string[]): string {
const day = path.join(sessionsDir, '2026', '06', '09');
fs.mkdirSync(day, { recursive: true });
const file = path.join(day, name);
fs.writeFileSync(file, lines.join('\n') + '\n');
return file;
}
beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-local-quota-'));
codexHome = path.join(tmpDir, '.codex');
sessionsDir = path.join(codexHome, 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
});
afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('getCodexLocalQuota', () => {
it('parses the last non-null rate_limits into a normalized quota', async () => {
writeRollout('rollout-2026-06-09T10-00-00-aaaa.jsonl', [
tokenCountLine(null),
tokenCountLine({
primary: { used_percent: 0.0, window_minutes: 300, resets_at: 1781033803 },
secondary: { used_percent: 48.0, window_minutes: 10080, resets_at: 1781192122 },
plan_type: 'pro',
}),
]);
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: codexHome }, now: Date.now() });
expect(quota).not.toBeNull();
// min(100-0, 100-48) = 52
expect(quota?.quotaPercentage).toBe(52);
expect(quota?.tier).toBe('pro');
// soonest reset = min(1781033803, 1781192122) -> primary
expect(quota?.nextReset).toBe(new Date(1781033803 * 1000).toISOString());
expect(quota?.stale).toBe(false);
});
it('returns null when every rate_limits is null (exec-mode session)', async () => {
// A newer session (later filename) with only null rate_limits wins the sort.
writeRollout('rollout-2026-06-09T11-00-00-bbbb.jsonl', [
tokenCountLine(null),
tokenCountLine(null),
]);
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: codexHome }, now: Date.now() });
expect(quota).toBeNull();
});
it('flags stale when the source file mtime is older than 5 minutes', async () => {
const file = writeRollout('rollout-2026-06-09T12-00-00-cccc.jsonl', [
tokenCountLine({
primary: { used_percent: 10, window_minutes: 300, resets_at: 1781033803 },
secondary: { used_percent: 5, window_minutes: 10080, resets_at: 1781192122 },
plan_type: 'plus',
}),
]);
const mtime = fs.statSync(file).mtimeMs;
// now = mtime + 6 minutes -> stale
const quota = await getCodexLocalQuota({
env: { CODEX_HOME: codexHome },
now: mtime + 6 * 60 * 1000,
});
expect(quota?.stale).toBe(true);
expect(quota?.tier).toBe('plus');
});
it('returns null when there are no rollout files at all', async () => {
const emptyHome = path.join(tmpDir, '.codex-empty');
fs.mkdirSync(path.join(emptyHome, 'sessions'), { recursive: true });
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: emptyHome }, now: Date.now() });
expect(quota).toBeNull();
});
});
@@ -0,0 +1,328 @@
/**
* Tests for the native subscription quota collector.
*
* The Anthropic fetch is ALWAYS mocked — these tests NEVER hit the live usage
* endpoint. A controllable clock drives TTL / backoff / breaker assertions.
*/
import { beforeEach, describe, expect, it } from 'bun:test';
import {
getNativeAccountRows,
resetNativeQuotaState,
type NativeQuotaDeps,
} from '../../../src/web-server/usage/native-quota-collector';
import type { ClaudeQuotaResult } from '../../../src/cliproxy/quota/quota-types';
import type { ClaudeNativeCredentials } from '../../../src/web-server/usage/claude-native-credentials';
// A jump comfortably past any single-call backoff cooldown (<= 60s), used so a
// breaker-test fetch is not blocked by the prior 429's per-call cooldown.
const MAX_COOLDOWN_JUMP = 61_000;
function maxCreds(): ClaudeNativeCredentials {
return { claudeAiOauth: { accessToken: 'native-tok', subscriptionType: 'max' } };
}
function successQuota(): ClaudeQuotaResult {
return {
success: true,
windows: [],
coreUsage: {
fiveHour: {
rateLimitType: 'five_hour',
label: 'Session limit',
remainingPercent: 42,
resetAt: '2026-06-09T20:00:00.000Z',
status: 'allowed',
},
weekly: {
rateLimitType: 'seven_day',
label: 'Weekly limit',
remainingPercent: 70,
resetAt: '2026-06-15T00:00:00.000Z',
status: 'allowed',
},
},
lastUpdated: Date.now(),
accountId: 'claude-code',
};
}
function reauthQuota(): ClaudeQuotaResult {
return {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId: 'claude-code',
needsReauth: true,
error: 'Authentication required',
};
}
function rateLimitedQuota(retryAfter?: string): ClaudeQuotaResult {
return {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId: 'claude-code',
httpStatus: 429,
retryable: true,
...(retryAfter ? { errorDetail: `retry-after:${retryAfter}` } : {}),
error: 'rate limited',
};
}
/** Build a deps object with a controllable clock + counted fetch. */
function makeDeps(
fetchImpl: (token: string) => Promise<ClaudeQuotaResult>,
clock: { now: number },
credsImpl: () => ClaudeNativeCredentials | null = maxCreds
): NativeQuotaDeps & { fetchCount: () => number } {
let count = 0;
return {
readCredentials: credsImpl,
fetchClaudeQuota: async (token: string) => {
count += 1;
return fetchImpl(token);
},
getCodexQuota: async () => null,
now: () => clock.now,
sleep: async () => {},
fetchCount: () => count,
};
}
beforeEach(() => {
resetNativeQuotaState();
});
describe('Claude native row mapping', () => {
it('maps a successful fetch into a claude-code ok row with min remaining', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row).toBeDefined();
expect(row?.quotaStatus).toBe('ok');
// min(42, 70)
expect(row?.quota_percentage).toBe(42);
expect(row?.next_reset).toBe('2026-06-09T20:00:00.000Z');
expect(row?.tier).toBe('max');
expect(row?.displayName).toBe('Claude Code');
expect(row?.account_id).toBe('claude-code');
expect(row?.needsReauth).toBe(false);
});
it('emits a reauth error row on 401/needsReauth', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => reauthQuota(), clock);
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row?.quotaStatus).toBe('error');
expect(row?.health).toBe('error');
expect(row?.needsReauth).toBe(true);
});
it('omits the claude row when there is no token', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(
async () => successQuota(),
clock,
() => null
);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
});
it('omits the claude row for an unsupported (free) subscription without spending a call', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(
async () => successQuota(),
clock,
() => ({
claudeAiOauth: { accessToken: 'x', subscriptionType: 'free' },
})
);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
expect(deps.fetchCount()).toBe(0);
});
});
describe('cache / TTL', () => {
it('serves cache within TTL and does NOT re-fetch', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
// Advance < 10 min: still cached.
clock.now += 5 * 60 * 1000;
const rows = await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
expect(rows.find((r) => r.provider === 'claude-code')?.cached).toBe(true);
});
it('re-fetches after the TTL expires', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
await getNativeAccountRows(deps);
clock.now += 11 * 60 * 1000; // past 10-min TTL
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(2);
});
});
describe('in-flight coalescing', () => {
it('shares ONE fetch across concurrent callers past TTL', async () => {
const clock = { now: 1_000_000 };
let resolveFetch: (q: ClaudeQuotaResult) => void = () => {};
const gate = new Promise<ClaudeQuotaResult>((resolve) => {
resolveFetch = resolve;
});
const deps = makeDeps(async () => gate, clock);
const p1 = getNativeAccountRows(deps);
const p2 = getNativeAccountRows(deps);
resolveFetch(successQuota());
await Promise.all([p1, p2]);
expect(deps.fetchCount()).toBe(1);
});
});
describe('Retry-After + backoff + circuit breaker', () => {
it('honors Retry-After: no fetch until the cooldown elapses', async () => {
const t0 = 1_000_000;
const clock = { now: t0 };
const deps = makeDeps(async () => rateLimitedQuota('30'), clock);
await getNativeAccountRows(deps); // fetch #1 -> 429, cooldown = t0 + 30s
expect(deps.fetchCount()).toBe(1);
// Within the 30s Retry-After cooldown -> zero network even though there is
// no cached row yet.
clock.now = t0 + 10_000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
// Past the 30s cooldown -> a fetch is allowed again.
clock.now = t0 + 31_000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(2);
});
it('trips the breaker after 3 consecutive 429s, then a success closes it', async () => {
const clock = { now: 1_000_000 };
let mode: 'fail' | 'ok' = 'fail';
const deps = makeDeps(
async () => (mode === 'fail' ? rateLimitedQuota() : successQuota()),
clock
);
// Three 429s; each separated past the per-call cooldown so they actually fetch.
for (let i = 0; i < 3; i++) {
await getNativeAccountRows(deps);
// jump past TTL and any backoff cooldown
clock.now += 11 * 60 * 1000 + MAX_COOLDOWN_JUMP;
}
expect(deps.fetchCount()).toBe(3);
// Breaker is open now -> zero network even past TTL.
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(3);
// Advance past the 15-min breaker cooldown; allow a success which closes it.
clock.now += 16 * 60 * 1000;
mode = 'ok';
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(4);
// After success, breaker closed: another fetch past TTL proceeds.
clock.now += 11 * 60 * 1000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(5);
});
});
describe('stale-on-fail', () => {
it('returns the last good row when a subsequent fetch rejects', async () => {
const clock = { now: 1_000_000 };
let mode: 'ok' | 'throw' = 'ok';
const deps = makeDeps(async () => {
if (mode === 'throw') throw new Error('network down');
return successQuota();
}, clock);
await getNativeAccountRows(deps); // good row cached
mode = 'throw';
clock.now += 11 * 60 * 1000; // force re-fetch
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row).toBeDefined();
expect(row?.quotaStatus).toBe('ok');
expect(row?.cached).toBe(true);
});
it('omits the row when the first-ever fetch fails (no prior cache)', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => {
throw new Error('network down');
}, clock);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
});
});
describe('Codex path', () => {
it('maps a local Codex quota into a codex ok row', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null, // no claude row
getCodexQuota: async () => ({
quotaPercentage: 52,
nextReset: '2026-06-09T19:00:00.000Z',
tier: 'pro',
stale: false,
}),
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'codex');
expect(row?.quotaStatus).toBe('ok');
expect(row?.quota_percentage).toBe(52);
expect(row?.tier).toBe('pro');
expect(row?.health).toBe('ok');
});
it('flags health warning when the Codex source is stale', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null,
getCodexQuota: async () => ({
quotaPercentage: 10,
nextReset: null,
tier: null,
stale: true,
}),
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'codex')?.health).toBe('warning');
});
it('omits the codex row when there is no rate_limits (exec-mode)', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null,
getCodexQuota: async () => null,
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'codex')).toBeUndefined();
});
});