fix(cliproxy): handle Claude OAuth quota 401

This commit is contained in:
Brian Le
2026-03-03 21:56:18 -05:00
parent 58731eb9ea
commit 7e8f9e82c1
2 changed files with 89 additions and 8 deletions
+54 -8
View File
@@ -21,6 +21,7 @@ export const CLAUDE_POLICY_LIMITS_URL = 'https://api.anthropic.com/api/claude_co
const CLAUDE_QUOTA_TIMEOUT_MS = 10000;
const CLAUDE_QUOTA_MAX_ATTEMPTS = 2;
const CLAUDE_USER_AGENT = 'ccs-cli/claude-quota';
const CLAUDE_OAUTH_UNSUPPORTED_MESSAGE = 'oauth authentication is currently not supported';
interface ClaudeAuthData {
accessToken: string;
@@ -65,6 +66,37 @@ function isAuthExpired(expiry: string | null): boolean {
return expiry ? isTokenExpired(expiry) : false;
}
function extractErrorMessage(payload: unknown): string | null {
const root = toObject(payload);
if (!root) return null;
const direct = asString(root['message']);
if (direct) return direct;
const nested = toObject(root['error']);
if (!nested) return null;
return asString(nested['message']);
}
async function readResponseErrorMessage(response: Response): Promise<string | null> {
try {
const body = await response.text();
if (!body || body.trim().length === 0) return null;
try {
const parsed = JSON.parse(body) as unknown;
const extracted = extractErrorMessage(parsed);
if (extracted) return extracted;
} catch {
// fall through to plain-text fallback
}
return body.trim();
} catch {
return null;
}
}
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
try {
const raw = await fsp.readFile(filePath, 'utf-8');
@@ -161,6 +193,16 @@ function buildEmptyResult(
};
}
function buildPolicyUnavailableResult(accountId: string): ClaudeQuotaResult {
return {
success: true,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId,
};
}
/**
* Fetch quota for a single Claude account.
*/
@@ -204,18 +246,22 @@ export async function fetchClaudeQuota(
}
if (response.status === 401) {
const errorMessage = await readResponseErrorMessage(response);
if (errorMessage && errorMessage.toLowerCase().includes(CLAUDE_OAUTH_UNSUPPORTED_MESSAGE)) {
if (verbose) {
console.error(
'[i] Claude policy limits endpoint does not support OAuth tokens; treating quota as unavailable'
);
}
return buildPolicyUnavailableResult(accountId);
}
return buildEmptyResult('Authentication required for policy limits', accountId, true);
}
if (response.status === 404) {
// Some accounts may not expose policy limits; treat as empty but successful.
return {
success: true,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId,
};
// Some accounts may not expose policy limits; treat as unavailable but successful.
return buildPolicyUnavailableResult(accountId);
}
if (response.status === 403) {
@@ -360,6 +360,41 @@ describe('Claude Quota Fetcher', () => {
expect(result.error).toContain('Authentication');
});
it('treats OAuth-unsupported 401 as policy-limits unavailable', async () => {
createClaudeAccount(
'claude-oauth-unsupported@example.com',
{
access_token: 'oauth-token',
expired: '2099-01-01T00:00:00.000Z',
type: 'claude',
},
'claude'
);
global.fetch = mock(() =>
Promise.resolve(
new Response(
JSON.stringify({
type: 'error',
error: {
type: 'authentication_error',
message: 'OAuth authentication is currently not supported.',
},
}),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
)
) as typeof fetch;
const result = await fetchClaudeQuota('claude-oauth-unsupported@example.com');
expect(result.success).toBe(true);
expect(result.needsReauth).toBeUndefined();
expect(result.windows).toHaveLength(0);
expect(result.coreUsage?.fiveHour).toBeNull();
expect(result.coreUsage?.weekly).toBeNull();
});
it('fails fast when auth file has no token', async () => {
createClaudeAccount('claude-missing@example.com', {
access_token: ' ',