Skip to content

Commit 4010689

Browse files
committed
fix(shared): only count the usage pools the default model draws from
1 parent 2b814d7 commit 4010689

3 files changed

Lines changed: 66 additions & 4 deletions

File tree

‎apps/server/src/textGeneration/TextGeneration.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const make = Effect.gen(function* () {
7979
const limited = exhaustedUsageWindow(
8080
snapshots.find((provider) => provider.instanceId === selection.instanceId),
8181
now,
82+
selection.model,
8283
);
8384
const call = (modelSelection: ModelSelection) =>
8485
resolveInstance(registry, operation, modelSelection.instanceId).pipe(

‎packages/shared/src/usageLimits.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,37 @@ describe("new thread model fallback", () => {
137137
).toEqual(selection);
138138
}
139139
});
140+
it("only counts the usage pools the default model draws from", () => {
141+
const pools = (driver: string, ids: readonly string[]) =>
142+
provider({
143+
driver: ProviderDriverKind.make(driver),
144+
usageLimits: {
145+
checkedAt: "2026-09-03T11:00:00.000Z",
146+
windows: ids.map((id) => ({ ...window, id, usedPercent: 100 })),
147+
},
148+
});
149+
const resolve = (limited: ServerProvider, model: string) =>
150+
resolveNewThreadModelSelection({
151+
defaultSelection: { instanceId: limited.instanceId, model },
152+
fallbackSelection: fallback,
153+
providers: [limited, available],
154+
now,
155+
}).selection?.model;
156+
expect(resolve(pools("cursor", ["autoPercentUsed"]), "claude-opus-5-5")).toBe(
157+
"claude-opus-5-5",
158+
);
159+
expect(resolve(pools("cursor", ["autoPercentUsed"]), "composer-2.5")).toBe("composer-2.5");
160+
expect(resolve(pools("cursor", ["autoPercentUsed", "apiPercentUsed"]), "composer-2.5")).toBe(
161+
fallback.model,
162+
);
163+
expect(resolve(pools("antigravity", ["gemini-weekly"]), "claude-opus-5-5")).toBe(
164+
"claude-opus-5-5",
165+
);
166+
expect(resolve(pools("antigravity", ["3p-weekly"]), "claude-opus-5-5")).toBe(fallback.model);
167+
expect(resolve(pools("opencode", ["go_weekly"]), "opencode/big-pickle")).toBe(
168+
"opencode/big-pickle",
169+
);
170+
});
140171
});
141172

142173
describe("pace", () => {

‎packages/shared/src/usageLimits.ts‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,45 @@ const MINUTE = 60_000;
2121
const HOUR = 60 * MINUTE;
2222
const DAY = 24 * HOUR;
2323

24-
export function exhaustedUsageWindow(provider: ServerProvider | undefined, now: number) {
25-
return provider?.usageLimits?.windows.find(
24+
export function exhaustedUsageWindow(
25+
provider: ServerProvider | undefined,
26+
now: number,
27+
model?: string,
28+
) {
29+
const exhausted = (provider?.usageLimits?.windows ?? []).filter(
2630
(window) =>
2731
window.usedPercent !== undefined &&
2832
window.usedPercent >= 100 &&
2933
(!window.resetsAt ||
3034
!Number.isFinite(Date.parse(window.resetsAt)) ||
3135
Date.parse(window.resetsAt) > now),
3236
);
37+
if (model === undefined) return exhausted[0];
38+
// Some drivers report separate allowances per model family; only the ones this model draws from count.
39+
switch (provider?.driver) {
40+
case "cursor": {
41+
const api = exhausted.find((window) => window.id === "apiPercentUsed");
42+
const auto = exhausted.find((window) => window.id === "autoPercentUsed");
43+
return /^(auto|default|grok|composer)/.test(model) ? auto && api : api;
44+
}
45+
case "antigravity": {
46+
const family = model.match(/^(gemini|claude|gpt)-/)?.[1];
47+
return exhausted.find((window) => {
48+
const pool = window.id.match(/^(gemini|claude|gpt|3p)-/)?.[1];
49+
return (
50+
pool === undefined ||
51+
pool === family ||
52+
(pool === "3p" && (family === "claude" || family === "gpt"))
53+
);
54+
});
55+
}
56+
case "opencode":
57+
return exhausted.find(
58+
(window) => !window.id.startsWith("go_") || model.startsWith("opencode-go/"),
59+
);
60+
default:
61+
return exhausted[0];
62+
}
3363
}
3464

3565
export function resolveNewThreadModelSelection(input: {
@@ -45,7 +75,7 @@ export function resolveNewThreadModelSelection(input: {
4575
const unchanged = { selection, fallback: null };
4676
if (input.explicitSelection || !selection || !input.fallbackSelection) return unchanged;
4777
const provider = input.providers.find((entry) => entry.instanceId === selection.instanceId);
48-
const window = exhaustedUsageWindow(provider, input.now);
78+
const window = exhaustedUsageWindow(provider, input.now, selection.model);
4979
const fallback = input.providers.find(
5080
(entry) => entry.instanceId === input.fallbackSelection?.instanceId,
5181
);
@@ -56,7 +86,7 @@ export function resolveNewThreadModelSelection(input: {
5686
!fallback.installed ||
5787
fallback.auth.status === "unauthenticated" ||
5888
!isProviderAvailable(fallback) ||
59-
exhaustedUsageWindow(fallback, input.now) ||
89+
exhaustedUsageWindow(fallback, input.now, input.fallbackSelection.model) ||
6090
(input.settings &&
6191
!resolveProjectProviderInstanceEnabled(
6292
input.settings,

0 commit comments

Comments
 (0)