diff --git a/apps/server/src/provider/claudeUsageLimits.test.ts b/apps/server/src/provider/claudeUsageLimits.test.ts index f20d8b0543636..b683c80f4f5e8 100644 --- a/apps/server/src/provider/claudeUsageLimits.test.ts +++ b/apps/server/src/provider/claudeUsageLimits.test.ts @@ -199,6 +199,22 @@ describe("claudeUsageResponseToLimits", () => { ).toEqual([monthlyWindow]); }); + it("flags a $0 cap instead of reporting a monthly window", () => { + const response = { + rate_limits_available: true, + rate_limits: { + five_hour: null, + seven_day: null, + extra_usage: { is_enabled: true, used_credits: 0, monthly_limit: 0, utilization: null }, + }, + }; + expect(claudeUsageResponseToLimits({ checkedAt, response }).limits).toEqual({ + checkedAt, + windows: [], + zeroSpendCap: true, + }); + }); + it("omits disabled budgets and refuses invalid money without an enabled fallback", () => { const rateLimits = enterpriseResponse.rate_limits; const response = { diff --git a/apps/server/src/provider/claudeUsageLimits.ts b/apps/server/src/provider/claudeUsageLimits.ts index d0acf43bb4968..8f81f2e7c7ef3 100644 --- a/apps/server/src/provider/claudeUsageLimits.ts +++ b/apps/server/src/provider/claudeUsageLimits.ts @@ -202,6 +202,7 @@ export function claudeUsageResponseToLimits(input: { overageIncluded ??= entry.display_name; } const spendPercent = readSpendPercent(response.rate_limits); + const extra = response.rate_limits.extra_usage; if (spendPercent !== undefined) { windows.push({ id: "monthly_spend", @@ -211,7 +212,10 @@ export function claudeUsageResponseToLimits(input: { }); } return { - limits: makeUsageLimits({ checkedAt, windows }), + limits: { + ...makeUsageLimits({ checkedAt, windows }), + ...(extra?.is_enabled && extra.monthly_limit === 0 ? { zeroSpendCap: true } : {}), + }, names: { overageIncluded }, }; } diff --git a/apps/web/src/components/usage/UsageLimitsPooled.tsx b/apps/web/src/components/usage/UsageLimitsPooled.tsx index d5f46398fa31c..8ec1c7a02afcd 100644 --- a/apps/web/src/components/usage/UsageLimitsPooled.tsx +++ b/apps/web/src/components/usage/UsageLimitsPooled.tsx @@ -523,6 +523,18 @@ function PoolBar({ index={position + 1} showAccountName={pool.columns.length > 1} /> + ) : pool.kind === "monthly" && member.account.limits.zeroSpendCap ? ( +
+ + $0 cap +
) : null, )} diff --git a/packages/contracts/src/providerUsageLimits.ts b/packages/contracts/src/providerUsageLimits.ts index 67acbe0d35b05..c3b871e996bf4 100644 --- a/packages/contracts/src/providerUsageLimits.ts +++ b/packages/contracts/src/providerUsageLimits.ts @@ -44,6 +44,8 @@ export type ServerProviderResetCredits = typeof ServerProviderResetCredits.Type; export const ServerProviderUsageLimits = Schema.Struct({ checkedAt: IsoDateTime, windows: ForwardCompatibleArray(ServerProviderUsageWindow), + /** A $0 monthly spend cap has no percentage, so it cannot be a window. */ + zeroSpendCap: Schema.optional(Schema.Boolean), credentialFingerprint: Schema.optional(TrimmedNonEmptyString), resetCredits: Schema.optional(ServerProviderResetCredits), externalUsage: Schema.optional( diff --git a/packages/provider-core/src/server/usageLimits.test.ts b/packages/provider-core/src/server/usageLimits.test.ts index 4b9c1b39c862b..6e2c1a18f0b62 100644 --- a/packages/provider-core/src/server/usageLimits.test.ts +++ b/packages/provider-core/src/server/usageLimits.test.ts @@ -66,10 +66,10 @@ describe("applyUsageLimitsUpdate", () => { ).toEqual({ checkedAt, windows: [weekly], unavailable: { reason: "probeFailed" } }); }); - it("preserves reset credits when a streamed window update changes usage", () => { + it("preserves reset credits and a $0 cap when a streamed window update changes usage", () => { const resetCredits = { availableCount: 2, nextExpiresAt: "2026-10-01T00:00:00.000Z" }; const next = applyUsageLimitsUpdate({ - previous: { ...published, resetCredits }, + previous: { ...published, resetCredits, zeroSpendCap: true }, checkedAt: "2026-09-03T12:00:05.000Z", update: { windows: [{ ...session, usedPercent: 55 }] }, }); @@ -78,6 +78,7 @@ describe("applyUsageLimitsUpdate", () => { checkedAt: "2026-09-03T12:00:05.000Z", windows: [{ ...session, usedPercent: 55 }, weekly], resetCredits, + zeroSpendCap: true, }); }); }); diff --git a/packages/provider-core/src/server/usageLimits.ts b/packages/provider-core/src/server/usageLimits.ts index 1e4bcd0090854..b8ff4edff10f7 100644 --- a/packages/provider-core/src/server/usageLimits.ts +++ b/packages/provider-core/src/server/usageLimits.ts @@ -84,6 +84,7 @@ export function applyUsageLimitsUpdate(input: { return { ...makeUsageLimits({ checkedAt: input.checkedAt, windows: merged.values() }), ...(previous?.resetCredits !== undefined ? { resetCredits: previous.resetCredits } : {}), + ...(previous?.zeroSpendCap ? { zeroSpendCap: true } : {}), ...(unavailable !== undefined ? { unavailable } : {}), }; }