Skip to content

Commit 7e2d356

Browse files
committed
fix: use OAuth usage endpoint for Claude Code provider limits
The Limits page showed "error" 0% for Claude Code (OAuth) providers because getClaudeUsage() called /v1/settings which requires API key with org admin access — unavailable to consumer OAuth tokens. Now uses https://api.anthropic.com/api/oauth/usage with the anthropic-beta: oauth-2025-04-20 header, which returns five_hour and seven_day utilization data for OAuth accounts. Falls back to legacy /v1/settings endpoint for API key users.
1 parent e5142c6 commit 7e2d356

File tree

1 file changed

+75
-5
lines changed

1 file changed

+75
-5
lines changed

open-sse/services/usage.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const CODEX_CONFIG = {
3131

3232
// Claude API config
3333
const CLAUDE_CONFIG = {
34+
oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage",
3435
usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage",
3536
settingsUrl: "https://api.anthropic.com/v1/settings",
3637
};
@@ -427,8 +428,79 @@ async function getAntigravitySubscriptionInfo(accessToken) {
427428
*/
428429
async function getClaudeUsage(accessToken) {
429430
try {
430-
// Try to get organization/account settings first
431-
const settingsResponse = await fetch("https://api.anthropic.com/v1/settings", {
431+
// Primary: Try OAuth usage endpoint (works with Claude Code consumer OAuth tokens)
432+
// Requires anthropic-beta: oauth-2025-04-20 header
433+
const oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, {
434+
method: "GET",
435+
headers: {
436+
Authorization: `Bearer ${accessToken}`,
437+
"Content-Type": "application/json",
438+
"anthropic-beta": "oauth-2025-04-20",
439+
"anthropic-version": "2023-06-01",
440+
},
441+
});
442+
443+
if (oauthResponse.ok) {
444+
const data = await oauthResponse.json();
445+
const quotas: Record<string, any> = {};
446+
447+
// Parse five_hour window (session limit)
448+
if (data.five_hour !== undefined) {
449+
quotas["session (5h)"] = {
450+
used: data.five_hour.utilization ?? 0,
451+
total: 100,
452+
resetAt: data.five_hour.resets_at || null,
453+
remainingPercentage: 100 - (data.five_hour.utilization ?? 0),
454+
unlimited: false,
455+
};
456+
}
457+
458+
// Parse seven_day window (weekly limit)
459+
if (data.seven_day !== undefined) {
460+
quotas["weekly (7d)"] = {
461+
used: data.seven_day.utilization ?? 0,
462+
total: 100,
463+
resetAt: data.seven_day.resets_at || null,
464+
remainingPercentage: 100 - (data.seven_day.utilization ?? 0),
465+
unlimited: false,
466+
};
467+
}
468+
469+
// Parse model-specific weekly windows (e.g., seven_day_sonnet, seven_day_opus)
470+
for (const [key, value] of Object.entries(data)) {
471+
if (key.startsWith("seven_day_") && key !== "seven_day" && value && typeof value === "object") {
472+
const modelName = key.replace("seven_day_", "");
473+
quotas[`weekly ${modelName} (7d)`] = {
474+
used: (value as any).utilization ?? 0,
475+
total: 100,
476+
resetAt: (value as any).resets_at || null,
477+
remainingPercentage: 100 - ((value as any).utilization ?? 0),
478+
unlimited: false,
479+
};
480+
}
481+
}
482+
483+
return {
484+
plan: "Claude Code",
485+
quotas,
486+
extraUsage: data.extra_usage || null,
487+
};
488+
}
489+
490+
// Fallback: Try legacy settings/org endpoint (for API key users with org admin access)
491+
return await getClaudeUsageLegacy(accessToken);
492+
} catch (error) {
493+
return { message: `Claude connected. Unable to fetch usage: ${(error as any).message}` };
494+
}
495+
}
496+
497+
/**
498+
* Legacy Claude usage fetcher for API key / org admin users.
499+
* Uses /v1/settings + /v1/organizations/{org_id}/usage endpoints.
500+
*/
501+
async function getClaudeUsageLegacy(accessToken) {
502+
try {
503+
const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, {
432504
method: "GET",
433505
headers: {
434506
Authorization: `Bearer ${accessToken}`,
@@ -440,7 +512,6 @@ async function getClaudeUsage(accessToken) {
440512
if (settingsResponse.ok) {
441513
const settings = await settingsResponse.json();
442514

443-
// Try usage endpoint if we have org info
444515
if (settings.organization_id) {
445516
const usageResponse = await fetch(
446517
`https://api.anthropic.com/v1/organizations/${settings.organization_id}/usage`,
@@ -471,10 +542,9 @@ async function getClaudeUsage(accessToken) {
471542
};
472543
}
473544

474-
// If settings API fails, OAuth token may not have required scope
475545
return { message: "Claude connected. Usage API requires admin permissions." };
476546
} catch (error) {
477-
return { message: `Claude connected. Unable to fetch usage: ${error.message}` };
547+
return { message: `Claude connected. Unable to fetch usage: ${(error as any).message}` };
478548
}
479549
}
480550

0 commit comments

Comments
 (0)