From e214fb1c30ede189a363afea2cd577a265491752 Mon Sep 17 00:00:00 2001 From: decolua Date: Sat, 5 Sep 2026 22:05:18 +0700 Subject: [PATCH] feat(usage): add Claude Fable quota tracker support - Recognize Fable weekly windows and normalize to weekly fable (7d) - Fall back to 100% available weekly Fable window when Anthropic payload omits it - Forward remaining percentages and enforce canonical Claude quota order in Quota Tracker Co-Authored-By: Claude Code --- open-sse/services/usage/claude.js | 24 +++++++++++++++++-- .../usage/components/ProviderLimits/utils.js | 14 +++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/open-sse/services/usage/claude.js b/open-sse/services/usage/claude.js index ce3e01a0..d45731ab 100644 --- a/open-sse/services/usage/claude.js +++ b/open-sse/services/usage/claude.js @@ -102,14 +102,34 @@ async function fetchClaudeUsageRaw(accessToken, proxyOptions = null) { quotas["weekly (7d)"] = createQuotaObject(data.seven_day); } - // Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus) + // Parse model-specific weekly windows (e.g. seven_day_sonnet, seven_day_opus, seven_day_fable) + const MODEL_DISPLAY_NAMES = { + fable_5_1: "fable", + fable_5: "fable", + }; + for (const [key, value] of Object.entries(data)) { if (key.startsWith("seven_day_") && key !== "seven_day" && hasUtilization(value)) { - const modelName = key.replace("seven_day_", ""); + const rawName = key.replace("seven_day_", ""); + const modelName = MODEL_DISPLAY_NAMES[rawName] || rawName; quotas[`weekly ${modelName} (7d)`] = createQuotaObject(value); + } else if ((key === "fable" || key === "fable_5" || key === "fable_5_1") && hasUtilization(value)) { + quotas["weekly fable (7d)"] = createQuotaObject(value); } } + // Fallback: surface Fable quota row if weekly window exists but Fable was not returned yet + if (!quotas["weekly fable (7d)"] && hasUtilization(data.seven_day)) { + quotas["weekly fable (7d)"] = { + used: 0, + total: 100, + remaining: 100, + remainingPercentage: 100, + resetAt: parseResetTime(data.seven_day.resets_at), + unlimited: false, + }; + } + return { plan: "Claude Code", extraUsage: data.extra_usage ?? null, diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js index 96a77678..86fd30d8 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js @@ -510,6 +510,8 @@ export function parseQuotaData(provider, data) { name, used: quota.used || 0, total: quota.total || 0, + remaining: quota.remaining !== undefined ? quota.remaining : Math.max(0, (quota.total || 100) - (quota.used || 0)), + remainingPercentage: quota.remainingPercentage !== undefined ? quota.remainingPercentage : calculatePercentage(quota.used, quota.total), resetAt: quota.resetAt || null, }); }); @@ -664,6 +666,18 @@ export function parseQuotaData(provider, data) { return []; } + if (provider?.toLowerCase() === "claude") { + const CLAUDE_QUOTA_ORDER = { + "session (5h)": 0, + "weekly (7d)": 1, + "weekly fable (7d)": 2, + "weekly opus (7d)": 3, + "weekly sonnet (7d)": 4, + }; + normalizedQuotas.sort((a, b) => (CLAUDE_QUOTA_ORDER[a.name] ?? 99) - (CLAUDE_QUOTA_ORDER[b.name] ?? 99)); + return normalizedQuotas; + } + // Sort quotas according to PROVIDER_MODELS order const modelOrder = getModelsByProviderId(provider); if (modelOrder.length > 0) {