From 89eaad0391f2d8fb5aec7f57075817eab6e6422e Mon Sep 17 00:00:00 2001 From: Fabian Gubler Date: Sun, 1 Nov 2026 14:27:10 +0100 Subject: [PATCH] fix(shared): Claude falls back to another model when only one model's weekly limit is spent A Claude model-scoped weekly window such as seven_day_fable now only marks the model it names as exhausted. New threads defaulting to Fable fall back to Opus on the same Claude instance, and an Opus default no longer falls back when only Fable's limit is spent. Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/usageLimits.test.ts | 22 ++++++++++++++++++++++ packages/shared/src/usageLimits.ts | 7 +++++++ 2 files changed, 29 insertions(+) diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index 23447ae7930b0..41cd5f097f117 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -132,6 +132,9 @@ describe("new thread model fallback", () => { ["antigravity", ["gemini-weekly"], "antigravity-default", true], ["opencode", ["go_weekly"], "opencode/big-pickle", false], ["opencode", ["go_weekly"], "opencode-go/big-pickle", true], + ["claudeAgent", ["seven_day_fable"], "claude-fable-5-1", true], + ["claudeAgent", ["seven_day_fable"], "claude-opus-5-5", false], + ["claudeAgent", ["seven_day"], "claude-opus-5-5", true], ] as const)("matches %s pools %s to %s", (driver, ids, model, usesFallback) => { const defaultSelection = { ...selection, model }; expect( @@ -151,6 +154,25 @@ describe("new thread model fallback", () => { }).selection, ).toEqual(usesFallback ? fallback : defaultSelection); }); + it("falls back to another model on the same Claude instance when only its model limit is spent", () => { + const claude = provider({ + instanceId: fallback.instanceId, + driver: ProviderDriverKind.make("claudeAgent"), + usageLimits: { + checkedAt: limited.checkedAt, + windows: [{ ...window, id: "seven_day_fable", usedPercent: 100 }], + }, + }); + const opus = { ...fallback, model: "claude-opus-5-5" }; + expect( + resolveNewThreadModelSelection({ + ...defaults, + defaultSelection: fallback, + fallbackSelection: opus, + providers: [claude], + }).selection, + ).toEqual(opus); + }); }); describe("pace", () => { diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index bf7dfea4e04bf..0b6bd8fae89fd 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -56,6 +56,13 @@ export function exhaustedUsageWindow( return exhausted.find( (window) => !window.id.startsWith("go_") || model.startsWith("opencode-go/"), ); + case "claudeAgent": { + const slug = model.toLowerCase().replace(/[^a-z0-9]+/g, "_"); + return exhausted.find((window) => { + const scope = window.id.match(/^seven_day_(.+)$/)?.[1]; + return scope === undefined || slug.includes(scope); + }); + } default: return exhausted[0]; }