mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-11 02:21:58 +00:00
fix(cliproxy): harden Claude OAuth quota window parsing
This commit is contained in:
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user