fix(cliproxy): harden Claude OAuth quota window parsing

This commit is contained in:
Tam Nhu Tran
2026-04-14 23:09:18 -04:00
parent 9e880825e7
commit 050b41bb55
3 changed files with 32 additions and 22 deletions
+16 -11
View File
@@ -74,7 +74,14 @@ function getClaudeWindowLabel(rateLimitType: string): string {
case 'overage': case 'overage':
return 'Extra usage'; return 'Extra usage';
default: default:
return rateLimitType || 'Unknown limit'; if (!rateLimitType) return 'Unknown limit';
return rateLimitType
.split(/[_-]+/g)
.filter((part) => part.length > 0)
.map((part) =>
/^\d+$/.test(part) ? part : `${part.charAt(0).toUpperCase()}${part.slice(1)}`
)
.join(' ');
} }
} }
@@ -188,15 +195,13 @@ function normalizeRestriction(
* Supports both policy-limits `restrictions` payloads and OAuth usage payloads * Supports both policy-limits `restrictions` payloads and OAuth usage payloads
* keyed by window name (`five_hour`, `seven_day`, `seven_day_sonnet`, ...). * keyed by window name (`five_hour`, `seven_day`, `seven_day_sonnet`, ...).
*/ */
const CLAUDE_USAGE_WINDOW_KEYS = new Set([ function isClaudeOAuthUsageWindowCandidate(key: string, raw: Record<string, unknown>): boolean {
'five_hour', if (key === 'extra_usage') return false;
'seven_day', if (asNumber(raw['utilization']) === null) return false;
'seven_day_opus',
'seven_day_sonnet', const resetAt = raw['resetsAt'] ?? raw['resets_at'] ?? raw['resetAt'] ?? raw['reset_at'] ?? null;
'seven_day_oauth_apps', return resetAt !== null && resetAt !== undefined;
'seven_day_cowork', }
'iguana_necktie',
]);
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] { export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
const rawRestrictions = payload['restrictions']; const rawRestrictions = payload['restrictions'];
@@ -218,9 +223,9 @@ export function buildClaudeQuotaWindows(payload: Record<string, unknown>): Claud
} }
} else if (toObject(payload)) { } else if (toObject(payload)) {
for (const [key, value] of Object.entries(payload)) { for (const [key, value] of Object.entries(payload)) {
if (!CLAUDE_USAGE_WINDOW_KEYS.has(key)) continue;
const raw = toObject(value); const raw = toObject(value);
if (!raw) continue; if (!raw) continue;
if (!isClaudeOAuthUsageWindowCandidate(key, raw)) continue;
const window = normalizeRestriction(raw, key); const window = normalizeRestriction(raw, key);
if (window) windows.push(window); if (window) windows.push(window);
} }
@@ -253,7 +253,7 @@ describe('Quota Exhaustion Handlers', () => {
expect(result.reason).toContain('no alternatives'); expect(result.reason).toContain('no alternatives');
}); });
it('should switch Claude accounts when fallback quota is unavailable but auth is valid', async () => { it('should switch Claude accounts when fallback quota endpoint returns 404', async () => {
writeRegistry({ writeRegistry({
claude: { claude: {
default: 'exhausted@example.com', default: 'exhausted@example.com',
@@ -294,16 +294,7 @@ describe('Quota Exhaustion Handlers', () => {
global.fetch = mock((_url: string, options?: RequestInit) => { global.fetch = mock((_url: string, options?: RequestInit) => {
const authHeader = new Headers(options?.headers).get('Authorization') ?? ''; const authHeader = new Headers(options?.headers).get('Authorization') ?? '';
if (authHeader === 'Bearer fallback-token') { if (authHeader === 'Bearer fallback-token') {
return Promise.resolve( return Promise.resolve(new Response('', { status: 404 }));
new Response(
JSON.stringify({
error: {
message: 'OAuth authentication is currently not supported.',
},
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
);
} }
return Promise.resolve(new Response('', { status: 500 })); return Promise.resolve(new Response('', { status: 500 }));
@@ -182,6 +182,20 @@ describe('Claude Quota Fetcher', () => {
expect(windows[1].rateLimitType).toBe('seven_day_sonnet'); expect(windows[1].rateLimitType).toBe('seven_day_sonnet');
expect(windows[1].remainingPercent).toBe(91); expect(windows[1].remainingPercent).toBe(91);
}); });
it('parses future OAuth usage windows without hardcoded keys', () => {
const windows = buildClaudeQuotaWindows({
seven_day_haiku: {
utilization: 16,
resets_at: '2026-03-06T10:00:00Z',
},
});
expect(windows).toHaveLength(1);
expect(windows[0].rateLimitType).toBe('seven_day_haiku');
expect(windows[0].label).toBe('Seven Day Haiku');
expect(windows[0].remainingPercent).toBe(84);
});
}); });
describe('buildClaudeCoreUsageSummary', () => { describe('buildClaudeCoreUsageSummary', () => {