diff --git a/packages/adapters/claude-local/src/server/quota.ts b/packages/adapters/claude-local/src/server/quota.ts index 859a59c2..eac10e44 100644 --- a/packages/adapters/claude-local/src/server/quota.ts +++ b/packages/adapters/claude-local/src/server/quota.ts @@ -187,13 +187,15 @@ function formatExtraUsageLabel(extraUsage: AnthropicExtraUsage): string | null { ) { return null; } - return `${formatCurrencyAmount(usedCredits, extraUsage.currency)} / ${formatCurrencyAmount(monthlyLimit, extraUsage.currency)}`; + // API returns values in cents — convert to dollars for display + return `${formatCurrencyAmount(usedCredits / 100, extraUsage.currency)} / ${formatCurrencyAmount(monthlyLimit / 100, extraUsage.currency)}`; } -/** Convert a 0-1 utilization fraction to a 0-100 integer percent. Returns null for null/undefined input. */ +/** Convert a utilization value to a 0-100 integer percent. Returns null for null/undefined input. + * Handles both 0-1 fractions (legacy) and 0-100 percentages (current API). */ export function toPercent(utilization: number | null | undefined): number | null { if (utilization == null) return null; - return Math.min(100, Math.round(utilization * 100)); + return Math.min(100, Math.round(utilization < 1 ? utilization * 100 : utilization)); } /** fetch with an abort-based timeout so a hanging provider api doesn't block the response indefinitely */ diff --git a/server/src/__tests__/quota-windows.test.ts b/server/src/__tests__/quota-windows.test.ts index c9d0fd92..e9eb26a9 100644 --- a/server/src/__tests__/quota-windows.test.ts +++ b/server/src/__tests__/quota-windows.test.ts @@ -39,24 +39,34 @@ describe("toPercent", () => { expect(toPercent(0)).toBe(0); }); - it("converts 0.5 to 50", () => { + it("treats values < 1 as fraction and multiplies by 100 (0.5 → 50%)", () => { expect(toPercent(0.5)).toBe(50); }); - it("converts 1.0 to 100", () => { - expect(toPercent(1.0)).toBe(100); + it("treats values >= 1 as already-percentage (34 → 34%)", () => { + expect(toPercent(34.0)).toBe(34); + expect(toPercent(91.0)).toBe(91); + }); + + it("treats value exactly 1.0 as 1% (not 100%) — the < 1 heuristic boundary", () => { + // 1.0 is NOT < 1, so it is treated as already-percentage → 1% + expect(toPercent(1.0)).toBe(1); }); it("clamps overshoot to 100", () => { - // floating-point utilization can slightly exceed 1.0 - expect(toPercent(1.001)).toBe(100); - expect(toPercent(1.01)).toBe(100); + expect(toPercent(105)).toBe(100); + expect(toPercent(101)).toBe(100); }); - it("rounds to nearest integer", () => { + it("rounds to nearest integer for fractions", () => { expect(toPercent(0.333)).toBe(33); expect(toPercent(0.666)).toBe(67); }); + + it("rounds to nearest integer for percentages", () => { + expect(toPercent(48.52)).toBe(49); + expect(toPercent(23.4)).toBe(23); + }); }); // --------------------------------------------------------------------------- @@ -516,37 +526,48 @@ describe("fetchClaudeQuota", () => { expect(windows).toEqual([]); }); - it("parses five_hour window", async () => { - mockFetch({ five_hour: { utilization: 0.4, resets_at: "2026-01-01T00:00:00Z" } }); + it("parses five_hour window with percentage-range utilization", async () => { + mockFetch({ five_hour: { utilization: 34.0, resets_at: "2026-01-01T00:00:00Z" } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "Current session", - usedPercent: 40, + usedPercent: 34, resetsAt: "2026-01-01T00:00:00Z", }); }); - it("parses seven_day window", async () => { - mockFetch({ seven_day: { utilization: 0.75, resets_at: null } }); + it("parses seven_day window with percentage-range utilization", async () => { + mockFetch({ seven_day: { utilization: 91.0, resets_at: null } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "Current week (all models)", - usedPercent: 75, + usedPercent: 91, resetsAt: null, }); }); + it("still handles legacy 0-1 fraction utilization", async () => { + mockFetch({ five_hour: { utilization: 0.4, resets_at: null } }); + const windows = await fetchClaudeQuota("token"); + expect(windows[0]).toMatchObject({ + label: "Current session", + usedPercent: 40, + }); + }); + it("parses seven_day_sonnet and seven_day_opus windows", async () => { mockFetch({ - seven_day_sonnet: { utilization: 0.2, resets_at: null }, - seven_day_opus: { utilization: 0.9, resets_at: null }, + seven_day_sonnet: { utilization: 23.0, resets_at: null }, + seven_day_opus: { utilization: 85.0, resets_at: null }, }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(2); expect(windows[0]!.label).toBe("Current week (Sonnet only)"); + expect(windows[0]!.usedPercent).toBe(23); expect(windows[1]!.label).toBe("Current week (Opus only)"); + expect(windows[1]!.usedPercent).toBe(85); }); it("sets usedPercent to null when utilization is absent", async () => { @@ -557,10 +578,10 @@ describe("fetchClaudeQuota", () => { it("includes all four windows when all are present", async () => { mockFetch({ - five_hour: { utilization: 0.1, resets_at: null }, - seven_day: { utilization: 0.2, resets_at: null }, - seven_day_sonnet: { utilization: 0.3, resets_at: null }, - seven_day_opus: { utilization: 0.4, resets_at: null }, + five_hour: { utilization: 10.0, resets_at: null }, + seven_day: { utilization: 20.0, resets_at: null }, + seven_day_sonnet: { utilization: 30.0, resets_at: null }, + seven_day_opus: { utilization: 40.0, resets_at: null }, }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(4); @@ -571,6 +592,7 @@ describe("fetchClaudeQuota", () => { "Current week (Sonnet only)", "Current week (Opus only)", ]); + expect(windows.map((w: QuotaWindow) => w.usedPercent)).toEqual([10, 20, 30, 40]); }); it("parses extra usage when the OAuth response includes it", async () => { @@ -591,6 +613,25 @@ describe("fetchClaudeQuota", () => { }, ]); }); + + it("formats extra usage credits from cents to dollars", async () => { + mockFetch({ + extra_usage: { + is_enabled: true, + monthly_limit: 14000, + used_credits: 6793, + utilization: 48.52, + }, + }); + const windows = await fetchClaudeQuota("token"); + expect(windows).toHaveLength(1); + expect(windows[0]).toMatchObject({ + label: "Extra usage", + usedPercent: 49, + valueLabel: "$67.93 / $140.00", + detail: "Monthly extra usage pool", + }); + }); }); // ---------------------------------------------------------------------------