mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 16:14:31 +00:00
fix(cliproxy): fetch Claude OAuth quota from usage endpoint
This commit is contained in:
1 parent
42d6427c60
commit
9e880825e7
3 files changed
+119
-110
No files matched your search
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Claude Quota Response Normalization Helpers
|
||||
*
|
||||
* Parses Anthropic policy limits payload into normalized windows and core usage summary.
|
||||
* Parses Anthropic policy-limits or OAuth-usage payloads into normalized windows and core usage summary.
|
||||
*/
|
||||
|
||||
import type { ClaudeCoreUsageSummary, ClaudeQuotaWindow } from './quota-types';
|
||||
@@ -185,8 +185,19 @@ function normalizeRestriction(
|
||||
|
||||
/**
|
||||
* Parse raw policy limits response into normalized windows.
|
||||
* Supports both array and object-map `restrictions` shapes.
|
||||
* Supports both policy-limits `restrictions` payloads and OAuth usage payloads
|
||||
* keyed by window name (`five_hour`, `seven_day`, `seven_day_sonnet`, ...).
|
||||
*/
|
||||
const CLAUDE_USAGE_WINDOW_KEYS = new Set([
|
||||
'five_hour',
|
||||
'seven_day',
|
||||
'seven_day_opus',
|
||||
'seven_day_sonnet',
|
||||
'seven_day_oauth_apps',
|
||||
'seven_day_cowork',
|
||||
'iguana_necktie',
|
||||
]);
|
||||
|
||||
export function buildClaudeQuotaWindows(payload: Record<string, unknown>): ClaudeQuotaWindow[] {
|
||||
const rawRestrictions = payload['restrictions'];
|
||||
const windows: ClaudeQuotaWindow[] = [];
|
||||
@@ -206,9 +217,19 @@ export function buildClaudeQuotaWindows(payload: Record<string, unknown>): Claud
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
} else if (toObject(payload)) {
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
if (!CLAUDE_USAGE_WINDOW_KEYS.has(key)) continue;
|
||||
const raw = toObject(value);
|
||||
if (!raw) continue;
|
||||
const window = normalizeRestriction(raw, key);
|
||||
if (window) windows.push(window);
|
||||
}
|
||||
|
||||
// Some responses may contain a single restriction object directly.
|
||||
const direct = normalizeRestriction(payload);
|
||||
if (direct) windows.push(direct);
|
||||
if (windows.length === 0) {
|
||||
const direct = normalizeRestriction(payload);
|
||||
if (direct) windows.push(direct);
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Quota Fetcher for Claude (Anthropic) Accounts
|
||||
*
|
||||
* Fetches policy limits from Claude API and normalizes 5h + weekly windows.
|
||||
* Fetches OAuth usage windows from Claude API and normalizes 5h + weekly windows.
|
||||
*/
|
||||
|
||||
import * as path from 'node:path';
|
||||
@@ -17,11 +17,10 @@ import {
|
||||
|
||||
export { buildClaudeQuotaWindows, buildClaudeCoreUsageSummary };
|
||||
|
||||
export const CLAUDE_POLICY_LIMITS_URL = 'https://api.anthropic.com/api/claude_code/policy_limits';
|
||||
export const CLAUDE_OAUTH_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
||||
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';
|
||||
const CLAUDE_OAUTH_BETA_HEADER = 'oauth-2025-04-20';
|
||||
|
||||
interface ClaudeAuthData {
|
||||
accessToken: string;
|
||||
@@ -193,16 +192,6 @@ 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.
|
||||
*/
|
||||
@@ -230,46 +219,43 @@ export async function fetchClaudeQuota(
|
||||
const timeoutId = setTimeout(() => controller.abort(), CLAUDE_QUOTA_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(CLAUDE_POLICY_LIMITS_URL, {
|
||||
const response = await fetch(CLAUDE_OAUTH_USAGE_URL, {
|
||||
method: 'GET',
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
Authorization: `Bearer ${authData.accessToken}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': CLAUDE_USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
if (verbose) {
|
||||
console.error(`[i] Claude policy limits status: ${response.status} (attempt ${attempt})`);
|
||||
console.error(`[i] Claude OAuth usage status: ${response.status} (attempt ${attempt})`);
|
||||
}
|
||||
|
||||
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);
|
||||
return buildEmptyResult(
|
||||
errorMessage || 'Authentication required for Claude OAuth usage',
|
||||
accountId,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
// Some accounts may not expose policy limits; treat as unavailable but successful.
|
||||
return buildPolicyUnavailableResult(accountId);
|
||||
return buildEmptyResult('Claude OAuth usage endpoint not found', accountId);
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
return buildEmptyResult('Not authorized for policy limits', accountId);
|
||||
return buildEmptyResult('Not authorized for Claude OAuth usage', accountId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
lastError = `Policy limits API error: ${response.status}`;
|
||||
lastError =
|
||||
(await readResponseErrorMessage(response)) ||
|
||||
`Claude OAuth usage API error: ${response.status}`;
|
||||
if (
|
||||
attempt < CLAUDE_QUOTA_MAX_ATTEMPTS &&
|
||||
(response.status === 429 || response.status >= 500)
|
||||
@@ -283,11 +269,11 @@ export async function fetchClaudeQuota(
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch {
|
||||
return buildEmptyResult('Invalid policy limits format', accountId);
|
||||
return buildEmptyResult('Invalid Claude OAuth usage format', accountId);
|
||||
}
|
||||
|
||||
if (!toObject(payload)) {
|
||||
return buildEmptyResult('Invalid policy limits format', accountId);
|
||||
return buildEmptyResult('Invalid Claude OAuth usage format', accountId);
|
||||
}
|
||||
|
||||
const windows = buildClaudeQuotaWindows(payload as Record<string, unknown>);
|
||||
@@ -304,7 +290,7 @@ export async function fetchClaudeQuota(
|
||||
clearTimeout(timeoutId);
|
||||
lastError =
|
||||
error instanceof Error && error.name === 'AbortError'
|
||||
? 'Policy limits request timeout'
|
||||
? 'Claude OAuth usage request timeout'
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: 'Unknown error';
|
||||
@@ -313,7 +299,7 @@ export async function fetchClaudeQuota(
|
||||
const errorDetails =
|
||||
error instanceof Error ? (error.stack ?? error.message) : JSON.stringify(error);
|
||||
console.error(
|
||||
`[!] Claude policy limits failed (attempt ${attempt}): ${lastError}${errorDetails ? `\n${errorDetails}` : ''}`
|
||||
`[!] Claude OAuth usage failed (attempt ${attempt}): ${lastError}${errorDetails ? `\n${errorDetails}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user