Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions apps/server/src/provider/Layers/claudeUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,76 @@ describe("claudeUsageResponseToLimits", () => {
});

describe("claudeRateLimitEventToUpdate", () => {
it("updates all windows carried by a stream-json rate-limit event", () => {
// Synthetic values using the Claude Code 2.1.281 stream-json serializer shape.
const event = {
type: "rate_limit_event",
rate_limit_info: {
status: "allowed" as const,
unifiedWindows: {
five_hour: { utilization: 0.25, resetsAt: 1_900_000_000 },
seven_day: { utilization: 0.5, resetsAt: 1_900_000_001 },
seven_day_overage_included: { utilization: 0.2, resetsAt: 1_900_000_002 },
},
},
uuid: "00000000-0000-4000-8000-000000000001",
session_id: "00000000-0000-4000-8000-000000000002",
};
const update = claudeRateLimitEventToUpdate(event.rate_limit_info, {
overageIncluded: "Example",
});
expect(
update?.windows.map(({ id, usedPercent, resetsAt }) => ({ id, usedPercent, resetsAt })),
).toEqual([
{ id: "five_hour", usedPercent: 25, resetsAt: "2030-03-17T17:46:40.000Z" },
{ id: "seven_day", usedPercent: 50, resetsAt: "2030-03-17T17:46:41.000Z" },
{ id: "seven_day_example", usedPercent: 20, resetsAt: "2030-03-17T17:46:42.000Z" },
]);
expect(
claudeRateLimitEventToUpdate(event.rate_limit_info, noNames)?.windows.map(({ id }) => id),
).toEqual(["five_hour", "seven_day"]);
});

it("lets the top-level update override the same unified window", () => {
const info = {
status: "allowed" as const,
rateLimitType: "five_hour" as const,
utilization: 0.75,
resetsAt: 1_900_000_010,
unifiedWindows: {
five_hour: { utilization: 0.25, resetsAt: 1_900_000_000 },
seven_day: { utilization: 0.5, resetsAt: 1_900_000_001 },
},
};
expect(
claudeRateLimitEventToUpdate(info, noNames)?.windows.map(({ id, usedPercent }) => [
id,
usedPercent,
]),
).toEqual([
["five_hour", 75],
["seven_day", 50],
]);
expect(claudeRateLimitEventToUpdate(info, noNames)?.windows[0]?.resetsAt).toBe(
"2030-03-17T17:46:50.000Z",
);
});

it("ignores malformed and unknown unified windows", () => {
const info = {
status: "allowed" as const,
unifiedWindows: {
five_hour: { utilization: NaN },
seven_day: { utilization: -1 },
unknown: { utilization: 1 },
constructor: { utilization: 1 },
toString: { utilization: 1 },
seven_day_overage_included: null,
},
};
expect(claudeRateLimitEventToUpdate(info, noNames)).toBeUndefined();
});

it("scales the 0–1 utilization and epoch-second reset onto the probe's window id", () => {
expect(
claudeRateLimitEventToUpdate(
Expand Down
36 changes: 24 additions & 12 deletions apps/server/src/provider/Layers/claudeUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* - `get_usage` (on demand, during the capabilities probe) reports every
* window at once as 0–100 percentages with ISO reset times.
* - `rate_limit_event` (streamed during a turn) names one window at a time
* - `rate_limit_event` (streamed during a turn) reports unified windows
* with a 0–1 utilization fraction and an epoch-seconds reset.
*
* @module provider/Layers/claudeUsageLimits
Expand Down Expand Up @@ -135,19 +135,31 @@ export function claudeRateLimitEventToUpdate(
info: SDKRateLimitInfo,
names: ClaudeScopedLimitNames,
): ProviderUsageLimitsUpdate | undefined {
const type: string | undefined = info.rateLimitType;
if (!type || typeof info.utilization !== "number") {
return undefined;
}
const usedPercent = info.utilization * 100;
const resetsAt = isoFromEpochSeconds(info.resetsAt);
if (type in WINDOWS) {
return { windows: [makeWindow(type, usedPercent, resetsAt)] };
const windows = new Map<string, ServerProviderUsageWindow>();
const add = (type: string, utilization: number, resetsAt: number | undefined) => {
if (!Number.isFinite(utilization) || utilization < 0) return;
const reset = isoFromEpochSeconds(resetsAt);
if (Object.hasOwn(WINDOWS, type)) {
windows.set(type, makeWindow(type, utilization * 100, reset));
} else if (type === OVERAGE_INCLUDED_EVENT_TYPE && names.overageIncluded) {
const window = scopedWindow(names.overageIncluded, utilization * 100, reset);
windows.set(window.id, window);
}
};
const unified = (info as { readonly unifiedWindows?: unknown }).unifiedWindows;
if (typeof unified === "object" && unified !== null && !Array.isArray(unified)) {
for (const [type, entry] of Object.entries(unified)) {
if (typeof entry !== "object" || entry === null) continue;
const { utilization, resetsAt } = entry as { utilization?: unknown; resetsAt?: unknown };
if (typeof utilization === "number") {
add(type, utilization, typeof resetsAt === "number" ? resetsAt : undefined);
}
}
}
if (type === OVERAGE_INCLUDED_EVENT_TYPE && names.overageIncluded) {
return { windows: [scopedWindow(names.overageIncluded, usedPercent, resetsAt)] };
if (info.rateLimitType && typeof info.utilization === "number") {
add(info.rateLimitType, info.utilization, info.resetsAt);
}
return undefined;
return windows.size === 0 ? undefined : { windows: [...windows.values()] };
}

/**
Expand Down
Loading