diff --git a/apps/server/src/provider/ClaudeProvider.ts b/apps/server/src/provider/ClaudeProvider.ts index 97512d3d621f0..d4df602000144 100644 --- a/apps/server/src/provider/ClaudeProvider.ts +++ b/apps/server/src/provider/ClaudeProvider.ts @@ -244,7 +244,7 @@ export type ClaudeCapabilitiesProbe = { readonly apiProvider: string | undefined; readonly slashCommands: ReadonlyArray; readonly models?: ReadonlyArray; - /** Undefined means get_usage failed; absent windows on success mean no subscription windows, as with API keys. */ + /** Omitted when usage collection is skipped or fails; `rate_limits.error` also means failure. Absent windows on success mean no subscription windows. */ readonly usage?: Pick; readonly usageCheckedAt?: string; }; diff --git a/apps/server/src/provider/claudeUsageLimits.test.ts b/apps/server/src/provider/claudeUsageLimits.test.ts index f20d8b0543636..92ad088a17731 100644 --- a/apps/server/src/provider/claudeUsageLimits.test.ts +++ b/apps/server/src/provider/claudeUsageLimits.test.ts @@ -107,6 +107,25 @@ describe("claudeUsageResponseToLimits", () => { ]); }); + it("treats a refused usage read as a failed probe, not as zero windows", () => { + expect( + claudeUsageResponseToLimits({ + checkedAt, + response: { + rate_limits_available: true, + rate_limits: { + ...({ + error: { type: "rate_limit_error", message: "Rate limited. Please try again later." }, + } as object), + }, + }, + }), + ).toEqual({ + names: noNames, + limits: { checkedAt, windows: [], unavailable: { reason: "probeFailed" } }, + }); + }); + const enterpriseResponse = { rate_limits_available: true, rate_limits: { diff --git a/apps/server/src/provider/claudeUsageLimits.ts b/apps/server/src/provider/claudeUsageLimits.ts index d0acf43bb4968..6e029ec1c1254 100644 --- a/apps/server/src/provider/claudeUsageLimits.ts +++ b/apps/server/src/provider/claudeUsageLimits.ts @@ -177,7 +177,8 @@ export function claudeUsageResponseToLimits(input: { readonly checkedAt: string; }): { readonly limits: ServerProviderUsageLimits; readonly names: ClaudeScopedLimitNames } { const { response, checkedAt } = input; - if (!response.rate_limits_available || !response.rate_limits) { + // The SDK does not type it, but a refused usage read (e.g. HTTP 429) arrives as `rate_limits: { error }`. + if (!response.rate_limits_available || !response.rate_limits || "error" in response.rate_limits) { return { limits: makeUnavailableUsageLimits({ checkedAt,