From b5a3a3d019faa605e04dc5f20bb948b23293910a Mon Sep 17 00:00:00 2001 From: DavyMassoneto Date: Wed, 4 Mar 2026 17:57:09 -0300 Subject: [PATCH 1/5] fix: use OAuth usage endpoint for Claude Code provider limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- open-sse/services/usage.ts | 80 +++++++++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 18c2df6762..b974ce0058 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -31,6 +31,7 @@ const CODEX_CONFIG = { // Claude API config const CLAUDE_CONFIG = { + oauthUsageUrl: "https://api.anthropic.com/api/oauth/usage", usageUrl: "https://api.anthropic.com/v1/organizations/{org_id}/usage", settingsUrl: "https://api.anthropic.com/v1/settings", }; @@ -427,8 +428,79 @@ async function getAntigravitySubscriptionInfo(accessToken) { */ async function getClaudeUsage(accessToken) { try { - // Try to get organization/account settings first - const settingsResponse = await fetch("https://api.anthropic.com/v1/settings", { + // Primary: Try OAuth usage endpoint (works with Claude Code consumer OAuth tokens) + // Requires anthropic-beta: oauth-2025-04-20 header + const oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "anthropic-beta": "oauth-2025-04-20", + "anthropic-version": "2023-06-01", + }, + }); + + if (oauthResponse.ok) { + const data = await oauthResponse.json(); + const quotas: Record = {}; + + // Parse five_hour window (session limit) + if (data.five_hour !== undefined) { + quotas["session (5h)"] = { + used: data.five_hour.utilization ?? 0, + total: 100, + resetAt: data.five_hour.resets_at || null, + remainingPercentage: 100 - (data.five_hour.utilization ?? 0), + unlimited: false, + }; + } + + // Parse seven_day window (weekly limit) + if (data.seven_day !== undefined) { + quotas["weekly (7d)"] = { + used: data.seven_day.utilization ?? 0, + total: 100, + resetAt: data.seven_day.resets_at || null, + remainingPercentage: 100 - (data.seven_day.utilization ?? 0), + unlimited: false, + }; + } + + // Parse model-specific weekly windows (e.g., seven_day_sonnet, seven_day_opus) + for (const [key, value] of Object.entries(data)) { + if (key.startsWith("seven_day_") && key !== "seven_day" && value && typeof value === "object") { + const modelName = key.replace("seven_day_", ""); + quotas[`weekly ${modelName} (7d)`] = { + used: (value as any).utilization ?? 0, + total: 100, + resetAt: (value as any).resets_at || null, + remainingPercentage: 100 - ((value as any).utilization ?? 0), + unlimited: false, + }; + } + } + + return { + plan: "Claude Code", + quotas, + extraUsage: data.extra_usage || null, + }; + } + + // Fallback: Try legacy settings/org endpoint (for API key users with org admin access) + return await getClaudeUsageLegacy(accessToken); + } catch (error) { + return { message: `Claude connected. Unable to fetch usage: ${(error as any).message}` }; + } +} + +/** + * Legacy Claude usage fetcher for API key / org admin users. + * Uses /v1/settings + /v1/organizations/{org_id}/usage endpoints. + */ +async function getClaudeUsageLegacy(accessToken) { + try { + const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, @@ -440,7 +512,6 @@ async function getClaudeUsage(accessToken) { if (settingsResponse.ok) { const settings = await settingsResponse.json(); - // Try usage endpoint if we have org info if (settings.organization_id) { const usageResponse = await fetch( `https://api.anthropic.com/v1/organizations/${settings.organization_id}/usage`, @@ -471,10 +542,9 @@ async function getClaudeUsage(accessToken) { }; } - // If settings API fails, OAuth token may not have required scope return { message: "Claude connected. Usage API requires admin permissions." }; } catch (error) { - return { message: `Claude connected. Unable to fetch usage: ${error.message}` }; + return { message: `Claude connected. Unable to fetch usage: ${(error as any).message}` }; } } From 6ea8d094b24ec671c8f337c6287395ca82a06aa6 Mon Sep 17 00:00:00 2001 From: DavyMassoneto Date: Wed, 4 Mar 2026 18:19:17 -0300 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20invert=20utilization=20values=20?= =?UTF-8?q?=E2=80=94=20API=20returns=20remaining,=20not=20used?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OAuth usage endpoint returns utilization as percentage remaining, not percentage used. Claude.ai showing 10% used corresponded to utilization=90 from the API. --- open-sse/services/usage.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index b974ce0058..e824d4f6ef 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -445,36 +445,45 @@ async function getClaudeUsage(accessToken) { const quotas: Record = {}; // Parse five_hour window (session limit) + // utilization = percentage remaining, not used if (data.five_hour !== undefined) { + const remaining = data.five_hour.utilization ?? 0; quotas["session (5h)"] = { - used: data.five_hour.utilization ?? 0, + used: 100 - remaining, total: 100, resetAt: data.five_hour.resets_at || null, - remainingPercentage: 100 - (data.five_hour.utilization ?? 0), + remainingPercentage: remaining, unlimited: false, }; } // Parse seven_day window (weekly limit) if (data.seven_day !== undefined) { + const remaining = data.seven_day.utilization ?? 0; quotas["weekly (7d)"] = { - used: data.seven_day.utilization ?? 0, + used: 100 - remaining, total: 100, resetAt: data.seven_day.resets_at || null, - remainingPercentage: 100 - (data.seven_day.utilization ?? 0), + remainingPercentage: remaining, unlimited: false, }; } // Parse model-specific weekly windows (e.g., seven_day_sonnet, seven_day_opus) for (const [key, value] of Object.entries(data)) { - if (key.startsWith("seven_day_") && key !== "seven_day" && value && typeof value === "object") { + if ( + key.startsWith("seven_day_") && + key !== "seven_day" && + value && + typeof value === "object" + ) { const modelName = key.replace("seven_day_", ""); + const remaining = (value as any).utilization ?? 0; quotas[`weekly ${modelName} (7d)`] = { - used: (value as any).utilization ?? 0, + used: 100 - remaining, total: 100, resetAt: (value as any).resets_at || null, - remainingPercentage: 100 - ((value as any).utilization ?? 0), + remainingPercentage: remaining, unlimited: false, }; } From aba12ad5db7349ded46333fd78c5dd7cd52ff988 Mon Sep 17 00:00:00 2001 From: DavyMassoneto Date: Wed, 4 Mar 2026 18:29:05 -0300 Subject: [PATCH 3/5] chore: sync package-lock.json with package.json v1.8.1 --- package-lock.json | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7eb34fd874..fd5f92f00c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "1.8.0", + "version": "1.8.1", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -6180,7 +6180,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8268,17 +8267,6 @@ } } }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", - "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", From d19f336286613d7cedff7103f54e00bbf6af095c Mon Sep 17 00:00:00 2001 From: DavyMassoneto Date: Wed, 4 Mar 2026 19:27:40 -0300 Subject: [PATCH 4/5] refactor: address PR review feedback - Use parseResetTime() for resetAt fields (consistency with other fetchers) - Extract createQuotaObject() helper to reduce duplication - Remove unnecessary Content-Type header from GET requests - Use CLAUDE_CONFIG.usageUrl with template interpolation in legacy fallback --- open-sse/services/usage.ts | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index e824d4f6ef..b9166d062d 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -434,7 +434,6 @@ async function getClaudeUsage(accessToken) { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", "anthropic-beta": "oauth-2025-04-20", "anthropic-version": "2023-06-01", }, @@ -444,29 +443,24 @@ async function getClaudeUsage(accessToken) { const data = await oauthResponse.json(); const quotas: Record = {}; - // Parse five_hour window (session limit) // utilization = percentage remaining, not used - if (data.five_hour !== undefined) { - const remaining = data.five_hour.utilization ?? 0; - quotas["session (5h)"] = { + const createQuotaObject = (window: any) => { + const remaining = window.utilization ?? 0; + return { used: 100 - remaining, total: 100, - resetAt: data.five_hour.resets_at || null, + resetAt: parseResetTime(window.resets_at) || null, remainingPercentage: remaining, unlimited: false, }; + }; + + if (data.five_hour !== undefined) { + quotas["session (5h)"] = createQuotaObject(data.five_hour); } - // Parse seven_day window (weekly limit) if (data.seven_day !== undefined) { - const remaining = data.seven_day.utilization ?? 0; - quotas["weekly (7d)"] = { - used: 100 - remaining, - total: 100, - resetAt: data.seven_day.resets_at || null, - remainingPercentage: remaining, - unlimited: false, - }; + quotas["weekly (7d)"] = createQuotaObject(data.seven_day); } // Parse model-specific weekly windows (e.g., seven_day_sonnet, seven_day_opus) @@ -478,14 +472,7 @@ async function getClaudeUsage(accessToken) { typeof value === "object" ) { const modelName = key.replace("seven_day_", ""); - const remaining = (value as any).utilization ?? 0; - quotas[`weekly ${modelName} (7d)`] = { - used: 100 - remaining, - total: 100, - resetAt: (value as any).resets_at || null, - remainingPercentage: remaining, - unlimited: false, - }; + quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value); } } @@ -513,7 +500,6 @@ async function getClaudeUsageLegacy(accessToken) { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", "anthropic-version": "2023-06-01", }, }); @@ -523,12 +509,11 @@ async function getClaudeUsageLegacy(accessToken) { if (settings.organization_id) { const usageResponse = await fetch( - `https://api.anthropic.com/v1/organizations/${settings.organization_id}/usage`, + CLAUDE_CONFIG.usageUrl.replace("{org_id}", settings.organization_id), { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, - "Content-Type": "application/json", "anthropic-version": "2023-06-01", }, } From 2ec0cd13cd07eb7a66e324525e2c6f9243a7485a Mon Sep 17 00:00:00 2001 From: DavyMassoneto Date: Wed, 4 Mar 2026 22:17:00 -0300 Subject: [PATCH 5/5] fix(claude): correct utilization inversion and propagate remainingPercentage - Fix createQuotaObject: utilization is percentage USED, not remaining - Add optional chaining to window access in createQuotaObject - Use typeof object guards for five_hour/seven_day instead of !== undefined - Use nullish coalescing for extra_usage - Propagate remainingPercentage in parseQuotaData claude case --- open-sse/services/usage.ts | 16 +++++++++------- .../usage/components/ProviderLimits/utils.tsx | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index b9166d062d..5fa08b70b4 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -443,23 +443,25 @@ async function getClaudeUsage(accessToken) { const data = await oauthResponse.json(); const quotas: Record = {}; - // utilization = percentage remaining, not used + // utilization = percentage USED (e.g., 22 means 22% used, 78% remaining) const createQuotaObject = (window: any) => { - const remaining = window.utilization ?? 0; + const used = window?.utilization ?? 0; + const remaining = 100 - used; return { - used: 100 - remaining, + used, total: 100, - resetAt: parseResetTime(window.resets_at) || null, + remaining, + resetAt: parseResetTime(window?.resets_at), remainingPercentage: remaining, unlimited: false, }; }; - if (data.five_hour !== undefined) { + if (data.five_hour && typeof data.five_hour === "object") { quotas["session (5h)"] = createQuotaObject(data.five_hour); } - if (data.seven_day !== undefined) { + if (data.seven_day && typeof data.seven_day === "object") { quotas["weekly (7d)"] = createQuotaObject(data.seven_day); } @@ -479,7 +481,7 @@ async function getClaudeUsage(accessToken) { return { plan: "Claude Code", quotas, - extraUsage: data.extra_usage || null, + extraUsage: data.extra_usage ?? null, }; } diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx index 86e098832e..3212355141 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx @@ -159,6 +159,7 @@ export function parseQuotaData(provider, data) { used: quota.used || 0, total: quota.total || 0, resetAt: quota.resetAt || null, + remainingPercentage: quota.remainingPercentage, }); }); }