From ac81235609097daccd185eccb57e12346c337bc3 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:21:30 -0300 Subject: [PATCH] fix(dashboard): surface Claude extraUsage credits in quota card (#6806) (#6896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enterprise-tier Claude accounts (default_raven_enterprise) don't get five_hour/seven_day utilization windows from Anthropic's OAuth usage endpoint — only an extra_usage credit-billing block. parseClaude() only read data.quotas, so quotas stayed {} and the dashboard showed "No quota data" even when extraUsage showed the account 100% exhausted. parseClaude() now folds an enabled extraUsage block into a credits-style quota row (mirroring parseCodex's bankedResetCredits pattern), both when quotas is empty and when it's already populated. --- .../fixes/6806-6806-claude-quota-data.md | 1 + .../components/ProviderLimits/quotaParsing.ts | 29 +++++- .../quota-parsing-claude-extra-usage.test.ts | 98 +++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 changelog.d/fixes/6806-6806-claude-quota-data.md create mode 100644 tests/unit/quota-parsing-claude-extra-usage.test.ts diff --git a/changelog.d/fixes/6806-6806-claude-quota-data.md b/changelog.d/fixes/6806-6806-claude-quota-data.md new file mode 100644 index 0000000000..bbc9f76803 --- /dev/null +++ b/changelog.d/fixes/6806-6806-claude-quota-data.md @@ -0,0 +1 @@ +- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806) diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts index d1d29eeb1a..0530404c05 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts @@ -158,12 +158,39 @@ function parseCodex(data: any) { return quotas; } +function buildClaudeExtraUsageQuota(extraUsage: any) { + const monthlyLimit = Number(extraUsage?.monthly_limit ?? 0); + const usedCredits = Number(extraUsage?.used_credits ?? 0); + const utilization = Number(extraUsage?.utilization ?? 0); + const remainingPercentage = Number.isFinite(utilization) + ? Math.max(0, 100 - utilization) + : undefined; + const remaining = Number.isFinite(monthlyLimit) ? Math.max(0, monthlyLimit - usedCredits) : 0; + + return buildCreditsQuota("extra_usage", remaining, remainingPercentage ?? 100, { + used: Number.isFinite(usedCredits) ? usedCredits : 0, + total: Number.isFinite(monthlyLimit) ? monthlyLimit : 0, + currency: extraUsage?.currency, + }); +} + +// #6806: some Claude plans (e.g. "default_raven_enterprise") return no +// five_hour/seven_day utilization windows at all — only a credit-billing +// extraUsage block — so quotas can be {} while extraUsage still holds real, +// actionable usage data. Fold it in instead of falling back to "No quota data". function parseClaude(data: any) { if (data?.message) return [{ name: "error", used: 0, total: 0, resetAt: null, message: data.message }]; - return quotaEntries(data).map(([name, quota]) => + + const quotas = quotaEntries(data).map(([name, quota]) => normalizeQuotaEntry(name, quota, { isPercentageOnly: true }) ); + + if (data?.extraUsage?.is_enabled) { + quotas.push(buildClaudeExtraUsageQuota(data.extraUsage)); + } + + return quotas; } function parseDeepseekQuota(quotaKey: string, quota: any) { diff --git a/tests/unit/quota-parsing-claude-extra-usage.test.ts b/tests/unit/quota-parsing-claude-extra-usage.test.ts new file mode 100644 index 0000000000..0eba6a6110 --- /dev/null +++ b/tests/unit/quota-parsing-claude-extra-usage.test.ts @@ -0,0 +1,98 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx"; + +interface QuotaRow { + name: string; + isCredits?: boolean; + remainingPercentage?: number; +} + +// Reproduces issue #6806: Claude Code "raven_enterprise" plan usage response has an +// empty `quotas: {}` (no five_hour/seven_day utilization fields returned by Anthropic +// for this plan) but a fully populated, 100%-exhausted `extraUsage` block. The UI must +// not fall back to "No quota data" when extraUsage has real, usable data. +test("#6806: parseQuotaData surfaces Claude extraUsage credits when quotas object is empty", () => { + const data = { + plan: "default_raven_enterprise", + quotas: {}, + extraUsage: { + is_enabled: true, + monthly_limit: 120000, + used_credits: 120015, + utilization: 100, + currency: "USD", + decimal_places: 2, + disabled_reason: null, + daily: null, + weekly: null, + }, + bootstrap: { + account_uuid: "redacted", + account_email: "redacted", + organization_uuid: "redacted", + organization_name: "redacted", + organization_type: "claude_enterprise", + organization_rate_limit_tier: "default_raven_enterprise", + }, + }; + + const parsed = parseQuotaData("claude", data); + + assert.ok( + parsed.length > 0, + `expected at least one quota row derived from extraUsage, got empty array: ${JSON.stringify(parsed)}` + ); + + const creditRow = parsed.find((row: QuotaRow) => row.isCredits); + assert.ok(creditRow, "expected a credits-style quota row derived from extraUsage"); + assert.equal(creditRow.remainingPercentage, 0, "utilization 100% means 0% remaining"); +}); + +test("#6806: parseQuotaData still surfaces extraUsage credits when quotas is also populated", () => { + const data = { + plan: "pro", + quotas: { + "session (5h)": { used: 10, total: 100, remainingPercentage: 90, resetAt: null }, + }, + extraUsage: { + is_enabled: true, + monthly_limit: 5000, + used_credits: 1000, + utilization: 20, + currency: "USD", + decimal_places: 2, + disabled_reason: null, + daily: null, + weekly: null, + }, + }; + + const parsed = parseQuotaData("claude", data); + + assert.equal(parsed.length, 2, `expected session quota + credits row, got: ${JSON.stringify(parsed)}`); + const creditRow = parsed.find((row: QuotaRow) => row.isCredits); + assert.ok(creditRow, "expected a credits-style quota row derived from extraUsage"); + assert.equal(creditRow.remainingPercentage, 80, "utilization 20% means 80% remaining"); +}); + +test("#6806: parseQuotaData does not add a credits row when extraUsage is disabled", () => { + const data = { + plan: "pro", + quotas: { + "session (5h)": { used: 10, total: 100, remainingPercentage: 90, resetAt: null }, + }, + extraUsage: { + is_enabled: false, + monthly_limit: 5000, + used_credits: 0, + utilization: 0, + }, + }; + + const parsed = parseQuotaData("claude", data); + + assert.equal(parsed.length, 1, `expected only the session quota, got: ${JSON.stringify(parsed)}`); + assert.equal(parsed.some((row: QuotaRow) => row.isCredits), false); +});