Skip to content

Commit 720c9c0

Browse files
committed
fix(agents-usage): parse Claude usage as direct percentages
- Treat all Claude `/api/oauth/usage` windows as 0-100 percentages - Update fixtures and add regression coverage for weekly parsing
1 parent 5927b61 commit 720c9c0

2 files changed

Lines changed: 32 additions & 13 deletions

File tree

packages/agents-usage/src/collectors/claude.test.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ import {
88
parseClaudeUsage,
99
} from "./claude";
1010

11+
// The Claude /api/oauth/usage endpoint reports `utilization` in percent (0-100)
12+
// for every window, not as a 0-1 fraction. These values mirror a real response.
1113
const FIXTURE = {
12-
five_hour: { utilization: 1, resets_at: "2026-05-29T12:00:00Z" },
13-
seven_day: { utilization: 0.22, resets_at: "2026-06-01T00:00:00Z" },
14-
seven_day_opus: { utilization: 0.1, resets_at: "2026-06-01T00:00:00Z" },
14+
five_hour: { utilization: 21, resets_at: "2026-05-29T12:00:00Z" },
15+
seven_day: { utilization: 1, resets_at: "2026-06-01T00:00:00Z" },
16+
seven_day_opus: { utilization: 10, resets_at: "2026-06-01T00:00:00Z" },
1517
extra_usage: {
1618
is_enabled: true,
1719
monthly_limit: 25000,
1820
used_credits: 7836,
19-
utilization: 0.31,
21+
utilization: 31,
2022
currency: "USD",
2123
},
2224
};
@@ -30,13 +32,13 @@ describe("formatClaudePlan", () => {
3032
});
3133

3234
describe("parseClaudeUsage", () => {
33-
it("maps windows and normalizes mixed Claude utilization units", () => {
35+
it("maps every Claude window as a direct percentage", () => {
3436
const snap = parseClaudeUsage(FIXTURE, FAKE_NOW_MS, { plan: "Claude Pro Subscription" });
3537
expect(snap.status).toBe("ok");
3638
const session = snap.windows.find((w) => w.id === "session-5h");
37-
expect(session?.usedPercent).toBe(1);
39+
expect(session?.usedPercent).toBe(21);
3840
expect(session?.resetsAt).toBe(Date.parse("2026-05-29T12:00:00Z"));
39-
expect(snap.windows.find((w) => w.id === "weekly")?.usedPercent).toBe(22);
41+
expect(snap.windows.find((w) => w.id === "weekly")?.usedPercent).toBe(1);
4042
expect(snap.windows.find((w) => w.id === "weekly-opus")?.usedPercent).toBe(10);
4143
// extra_usage is pay-as-you-go overage, surfaced as a dollar "extra-usage"
4244
// line — never as a "monthly" rate window.
@@ -60,6 +62,18 @@ describe("parseClaudeUsage", () => {
6062
const snap = parseClaudeUsage({ five_hour: { utilization: 1 } }, FAKE_NOW_MS);
6163
expect(snap.windows.find((w) => w.id === "session-5h")?.usedPercent).toBe(1);
6264
});
65+
66+
it("treats weekly utilization as a direct percentage, not a 0-1 fraction", () => {
67+
// Regression: the API reports weekly utilization in percent like every other
68+
// window. A value of 1 means 1% and must not be read as the fraction 1.0 →
69+
// 100% (which rendered a freshly-reset weekly window as a full red bar).
70+
const snap = parseClaudeUsage(
71+
{ seven_day: { utilization: 1 }, seven_day_sonnet: { utilization: 0.5 } },
72+
FAKE_NOW_MS,
73+
);
74+
expect(snap.windows.find((w) => w.id === "weekly")?.usedPercent).toBe(1);
75+
expect(snap.windows.find((w) => w.id === "weekly-sonnet")?.usedPercent).toBe(0.5);
76+
});
6377
});
6478

6579
describe("collectClaude", () => {

packages/agents-usage/src/collectors/claude.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { DEFAULT_CLIENT_VERSIONS } from "../clientVersions";
2-
import { normalizePercent, toEpochMs } from "../formatters";
2+
import { toEpochMs } from "../formatters";
33
import type { CollectOptions, HostPort } from "../host";
44
import type { UsageSnapshot, UsageWindow, UsageWindowId } from "../types";
55

@@ -52,9 +52,8 @@ function windowFrom(
5252
id: UsageWindowId,
5353
label: string,
5454
raw: ClaudeWindowRaw | undefined,
55-
normalize: (value: number | undefined) => number | undefined = normalizePercent,
5655
): UsageWindow | undefined {
57-
const usedPercent = normalize(raw?.utilization);
56+
const usedPercent = normalizeClaudePercent(raw?.utilization);
5857
if (usedPercent === undefined) return undefined;
5958
const resetsAt = toEpochMs(raw?.resets_at);
6059
return {
@@ -66,7 +65,13 @@ function windowFrom(
6665
};
6766
}
6867

69-
function normalizeClaudeSessionPercent(value: number | undefined): number | undefined {
68+
/**
69+
* The Claude `/api/oauth/usage` endpoint reports `utilization` already in
70+
* percent (0-100) for every window — session, weekly, and overage alike. Clamp
71+
* to 0-100 and round to one decimal; never rescale (a value of 1 means 1%, not
72+
* the fraction 1.0 → 100%).
73+
*/
74+
function normalizeClaudePercent(value: number | undefined): number | undefined {
7075
if (value === undefined || !Number.isFinite(value) || value < 0) return undefined;
7176
return Math.min(100, Math.max(0, Math.round(value * 10) / 10));
7277
}
@@ -80,7 +85,7 @@ export function parseClaudeUsage(
8085
const data = (body ?? {}) as ClaudeUsageResponse;
8186
const windows: UsageWindow[] = [];
8287
for (const w of [
83-
windowFrom("session-5h", "Session (5h)", data.five_hour, normalizeClaudeSessionPercent),
88+
windowFrom("session-5h", "Session (5h)", data.five_hour),
8489
windowFrom("weekly", "Weekly", data.seven_day),
8590
windowFrom("weekly-opus", "Weekly (Opus)", data.seven_day_opus),
8691
windowFrom("weekly-sonnet", "Weekly (Sonnet)", data.seven_day_sonnet),
@@ -95,7 +100,7 @@ export function parseClaudeUsage(
95100
const usedCents = data.extra_usage.used_credits;
96101
const limitCents = data.extra_usage.monthly_limit;
97102
const pct =
98-
normalizePercent(data.extra_usage.utilization) ??
103+
normalizeClaudePercent(data.extra_usage.utilization) ??
99104
(usedCents !== undefined && limitCents ? Math.min(100, (usedCents / limitCents) * 100) : 0);
100105
windows.push({
101106
id: "extra-usage",

0 commit comments

Comments
 (0)