From f4bd03e67533273aa8d3cad568bda779d18bd97e Mon Sep 17 00:00:00 2001 From: reyanshdagoat9113 Date: Wed, 4 Nov 2026 22:26:17 +0530 Subject: [PATCH] fix(server): Claude limits update from rate_limit_event unifiedWindows Co-Authored-By: Claude Sonnet 5.5 --- .../src/provider/claudeUsageLimits.test.ts | 47 +++++++++++++++++++ apps/server/src/provider/claudeUsageLimits.ts | 23 +++++++++ 2 files changed, 70 insertions(+) diff --git a/apps/server/src/provider/claudeUsageLimits.test.ts b/apps/server/src/provider/claudeUsageLimits.test.ts index f20d8b0543636..d61d4c1a81f1e 100644 --- a/apps/server/src/provider/claudeUsageLimits.test.ts +++ b/apps/server/src/provider/claudeUsageLimits.test.ts @@ -249,6 +249,53 @@ describe("claudeRateLimitEventToUpdate", () => { }); }); + it("reads every window from unifiedWindows when the event carries no utilization", () => { + // Shape captured from Claude Code 2.1.318. + const info = { + status: "allowed", + rateLimitType: "five_hour", + resetsAt: 1_784_000_000, + unifiedWindows: { + five_hour: { utilization: 0.5, resetsAt: 1_784_000_000, status: "allowed" }, + seven_day: { utilization: 0.25, resetsAt: 1_784_100_000, status: "allowed" }, + seven_day_opus: { utilization: 0.9, resetsAt: 1_784_100_000 }, + }, + } as const; + expect(claudeRateLimitEventToUpdate(info, noNames)).toEqual({ + windows: [ + { + id: "five_hour", + kind: "session", + label: "Session", + usedPercent: 50, + windowDurationMins: 300, + resetsAt: "2026-07-14T03:33:20.000Z", + }, + { + id: "seven_day", + kind: "weekly", + label: "Weekly", + usedPercent: 25, + windowDurationMins: 10080, + resetsAt: "2026-07-15T07:20:00.000Z", + }, + ], + }); + }); + + it("ignores unifiedWindows that carry no utilization", () => { + expect( + claudeRateLimitEventToUpdate( + { + status: "allowed", + rateLimitType: "five_hour", + unifiedWindows: { five_hour: {} }, + } as never, + noNames, + ), + ).toBeUndefined(); + }); + it("lands the streamed overage-included bucket on the row the probe named", () => { const event = { status: "allowed", diff --git a/apps/server/src/provider/claudeUsageLimits.ts b/apps/server/src/provider/claudeUsageLimits.ts index d0acf43bb4968..6732c35236e8c 100644 --- a/apps/server/src/provider/claudeUsageLimits.ts +++ b/apps/server/src/provider/claudeUsageLimits.ts @@ -151,11 +151,34 @@ function makeWindow( }; } +interface UnifiedWindow { + readonly utilization?: number; + readonly resetsAt?: number; +} + +/** + * Newer CLIs put each window's 0-1 utilization under `unifiedWindows` and omit the + * event's own `utilization`. The pinned SDK typings do not declare it yet. + */ +function readUnifiedWindows(info: SDKRateLimitInfo): ServerProviderUsageWindow[] { + const unified = ( + info as { readonly unifiedWindows?: Readonly> } + ).unifiedWindows; + return Object.keys(WINDOWS).flatMap((id) => { + const window = unified?.[id]; + return typeof window?.utilization === "number" + ? [makeWindow(id, window.utilization * 100, isoFromEpochSeconds(window.resetsAt))] + : []; + }); +} + /** Stream utilization is 0-1. Drop unnamed overage buckets until probed to avoid unreconcilable rows. */ export function claudeRateLimitEventToUpdate( info: SDKRateLimitInfo, names: ClaudeScopedLimitNames, ): ProviderUsageLimitsUpdate | undefined { + const unifiedWindows = readUnifiedWindows(info); + if (unifiedWindows.length > 0) return { windows: unifiedWindows }; const type: string | undefined = info.rateLimitType; if (!type || typeof info.utilization !== "number") { return undefined;