/** * usage/claude.ts — Claude (Anthropic OAuth + legacy org) usage fetcher + plan-label helper. * * Extracted from services/usage.ts (god-file decomposition): the Claude family — the API * config, the plan-label picker, the OAuth usage fetcher (getClaudeUsage) with its legacy * settings/org fallback (getClaudeUsageLegacy). Depends only on the sibling scalar/quota * leaves + Claude identity/cooldown helpers + safePercentage — no host coupling — so it * lives as a co-located provider leaf. usage.ts imports getClaudeUsage (dispatcher) + * getClaudePlanLabel (__testing). Behavior-preserving move. */ import { safePercentage } from "@/shared/utils/formatting"; import { CLAUDE_CODE_VERSION, fetchClaudeBootstrap } from "../../executors/claudeIdentity.ts"; import { isClaudeOauthUsageCoolingDown, markClaudeOauthUsage429 } from "../claudeUsageCooldown.ts"; import { toRecord } from "./scalars.ts"; import { type UsageQuota, parseResetTime } from "./quota.ts"; type JsonRecord = Record; // 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", apiVersion: "2023-06-01", }; export function getClaudePlanLabel(...candidates: Array): string | null { for (const candidate of candidates) { if (typeof candidate !== "string") continue; const trimmed = candidate.trim(); if ( !trimmed || trimmed.toLowerCase() === "claude code" || trimmed.toLowerCase() === "unknown" ) { continue; } return trimmed; } return null; } /** * Claude Usage - Try to fetch from Anthropic API */ export async function getClaudeUsage(accessToken?: string) { if (!accessToken) { return { message: "Claude connected. Access token not available.", bootstrap: null }; } // Refresh bootstrap in parallel; best-effort, failure non-fatal. const bootstrapPromise = fetchClaudeBootstrap(accessToken).catch(() => null); // Skip OAuth usage call while this token is cooling down from a recent 429 // (chat with the same token still works — only the quota endpoint is throttled). if (isClaudeOauthUsageCoolingDown(accessToken)) { const legacy = await getClaudeUsageLegacy(accessToken); return { ...legacy, bootstrap: await bootstrapPromise }; } try { // Real CLI uses axios here, not Stainless — UA is `claude-code/` // (not `claude-cli/...`) and the shape is simpler than /v1/messages. const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 10_000); let oauthResponse; try { oauthResponse = await fetch(CLAUDE_CONFIG.oauthUsageUrl, { method: "GET", headers: { Accept: "application/json, text/plain, */*", "Accept-Encoding": "gzip, compress, deflate, br", Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", "User-Agent": `claude-code/${CLAUDE_CODE_VERSION}`, "anthropic-beta": "oauth-2025-04-20", }, signal: ctrl.signal, }); } finally { clearTimeout(timer); } if (oauthResponse.ok) { const data = toRecord(await oauthResponse.json()); const quotas: Record = {}; // utilization = percentage USED (e.g., 90 means 90% used, 10% remaining) // Confirmed via user report #299: Claude.ai shows 87% used = OmniRoute must show 13% remaining. const hasUtilization = (window: JsonRecord) => window && typeof window === "object" && safePercentage(window.utilization) !== undefined; const createQuotaObject = (window: JsonRecord) => { const used = safePercentage(window.utilization) as number; // utilization = % used const remaining = Math.max(0, 100 - used); return { used, total: 100, remaining, resetAt: parseResetTime(window.resets_at), remainingPercentage: remaining, unlimited: false, }; }; const fiveHour = toRecord(data.five_hour); if (hasUtilization(fiveHour)) { quotas["session (5h)"] = createQuotaObject(fiveHour); } const sevenDay = toRecord(data.seven_day); if (hasUtilization(sevenDay)) { quotas["weekly (7d)"] = createQuotaObject(sevenDay); } // Map Anthropic's internal codenames (e.g., omelette → Designer) for display. const MODEL_DISPLAY_NAMES: Record = { omelette: "designer", }; for (const [key, value] of Object.entries(data)) { const valueRecord = toRecord(value); if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(valueRecord)) { const codename = key.replace("seven_day_", ""); const modelName = MODEL_DISPLAY_NAMES[codename] || codename; quotas[`weekly ${modelName} (7d)`] = createQuotaObject(valueRecord); } } const bootstrap = await bootstrapPromise; const plan = getClaudePlanLabel( typeof data.tier === "string" ? data.tier : null, typeof data.plan === "string" ? data.plan : null, typeof data.subscription_type === "string" ? data.subscription_type : null, bootstrap?.organization_rate_limit_tier ) ?? undefined; return { ...(plan ? { plan } : {}), quotas, extraUsage: data.extra_usage ?? null, bootstrap, }; } // Cool down OAuth usage polling after a 429 (quota endpoint only — chat is unaffected). if (oauthResponse.status === 429) { markClaudeOauthUsage429(accessToken); } // Fallback: OAuth endpoint returned non-OK, try legacy settings/org endpoint console.warn( `[Claude Usage] OAuth endpoint returned ${oauthResponse.status}, falling back to legacy` ); const legacy = await getClaudeUsageLegacy(accessToken); return { ...legacy, bootstrap: await bootstrapPromise }; } catch (error) { return { message: `Claude connected. Unable to fetch usage: ${(error as Error).message}`, bootstrap: await bootstrapPromise, }; } } /** * Legacy Claude usage fetcher for API key / org admin users. * Uses /v1/settings + /v1/organizations/{org_id}/usage endpoints. */ async function getClaudeUsageLegacy(accessToken?: string) { try { const settingsResponse = await fetch(CLAUDE_CONFIG.settingsUrl, { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, "anthropic-version": CLAUDE_CONFIG.apiVersion, }, }); if (settingsResponse.ok) { const settings = toRecord(await settingsResponse.json()); const organizationId = typeof settings.organization_id === "string" ? settings.organization_id : ""; if (organizationId) { const usageResponse = await fetch( CLAUDE_CONFIG.usageUrl.replace("{org_id}", organizationId), { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, "anthropic-version": CLAUDE_CONFIG.apiVersion, }, } ); if (usageResponse.ok) { const usage = await usageResponse.json(); return { plan: settings.plan || "Unknown", organization: settings.organization_name, quotas: usage, }; } } return { plan: settings.plan || "Unknown", organization: settings.organization_name, message: "Claude connected. Usage details require admin access.", }; } return { message: "Claude connected. Usage API requires admin permissions." }; } catch (error) { return { message: `Claude connected. Unable to fetch usage: ${(error as Error).message}` }; } }