authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-06 21:43:37-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 21:43:37-05:00
log1c1d38fcd40252f91a3ea9056a7323135d3452dd
treef69c6570a8e98b48352402d0fa8cbaf76c403dcd
parentde28fa1ff3ac3c3a96bebd9ec5ce23dd09f61af0
signature Signed by PGP key B5690EEEBB952194

fix(codex): keep Spark limits from replacing the main allowance (#10458)


3 files changed, 89 insertions(+), 1 deletions(-)

apps/server/src/provider/Layers/CodexProvider.ts+6
......@@ -49,6 +49,10 @@ const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;
4949type CodexRateLimitsProbe =
5050 | {
5151 readonly snapshot: CodexRateLimitSnapshot;
52 readonly rateLimitsByLimitId?:
53 | Readonly<Record<string, CodexRateLimitSnapshot>>
54 | null
55 | undefined;
5256 readonly resetCredits: CodexResetCreditsSummary | null | undefined;
5357 }
5458 | { readonly failure: string };
......@@ -434,6 +438,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
434438 client.request("account/rateLimits/read", undefined).pipe(
435439 Effect.map((response): CodexRateLimitsProbe => ({
436440 snapshot: response.rateLimits,
441 rateLimitsByLimitId: response.rateLimitsByLimitId,
437442 resetCredits: response.rateLimitResetCredits,
438443 })),
439444 Effect.timeoutOption(Duration.millis(RATE_LIMITS_PROBE_TIMEOUT_MS)),
......@@ -652,6 +657,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
652657 })
653658 : codexRateLimitsToLimits({
654659 snapshot: snapshot.rateLimits.snapshot,
660 rateLimitsByLimitId: snapshot.rateLimits.rateLimitsByLimitId,
655661 resetCredits: snapshot.rateLimits.resetCredits,
656662 checkedAt,
657663 });
apps/server/src/provider/Layers/codexUsageLimits.test.ts+72
......@@ -60,6 +60,54 @@ describe("codexRateLimitsToLimits", () => {
6060 },
6161 ]);
6262 });
63
64 it("selects the main Codex allowance and leaves Spark out", () => {
65 const spark = {
66 limitId: "codex_bengalfox",
67 primary: { usedPercent: 0, windowDurationMins: 300 },
68 secondary: { usedPercent: 90, windowDurationMins: 10080 },
69 };
70 expect(
71 codexRateLimitsToLimits({
72 checkedAt,
73 snapshot: spark,
74 rateLimitsByLimitId: {
75 codex_bengalfox: spark,
76 codex: { secondary: { usedPercent: 42, windowDurationMins: 10080 } },
77 },
78 }).windows,
79 ).toEqual([
80 {
81 id: "secondary",
82 kind: "weekly",
83 label: "Weekly",
84 usedPercent: 42,
85 windowDurationMins: 10080,
86 },
87 ]);
88 });
89
90 it.each([undefined, null, {}])(
91 "supports legacy reads with no bucket map: %j",
92 (rateLimitsByLimitId) => {
93 const snapshot = { primary: { usedPercent: 12, windowDurationMins: 300 } };
94 expect(codexRateLimitsToLimits({ checkedAt, snapshot, rateLimitsByLimitId })).toEqual(
95 codexRateLimitsToLimits({ checkedAt, snapshot }),
96 );
97 },
98 );
99
100 it("does not show a model-specific legacy snapshot as the main allowance", () => {
101 expect(
102 codexRateLimitsToLimits({
103 checkedAt,
104 snapshot: {
105 limitId: "codex_bengalfox",
106 secondary: { usedPercent: 90 },
107 },
108 }).windows,
109 ).toEqual([]);
110 });
63111});
64112
65113describe("codexRateLimitsToUpdate", () => {
......@@ -81,6 +129,30 @@ describe("codexRateLimitsToUpdate", () => {
81129 });
82130 expect(codexRateLimitsToUpdate({ planType: "plus" })).toBeUndefined();
83131 });
132
133 it("ignores Spark notifications so they cannot overwrite the main allowance", () => {
134 expect(
135 codexRateLimitsToUpdate({
136 limitId: "codex_bengalfox",
137 primary: { usedPercent: 0, windowDurationMins: 300 },
138 secondary: { usedPercent: 90, windowDurationMins: 10080 },
139 }),
140 ).toBeUndefined();
141 expect(
142 codexRateLimitsToUpdate({
143 limitId: "codex",
144 secondary: { usedPercent: 42, windowDurationMins: 10080 },
145 })?.windows,
146 ).toEqual([
147 {
148 id: "secondary",
149 kind: "weekly",
150 label: "Weekly",
151 usedPercent: 42,
152 windowDurationMins: 10080,
153 },
154 ]);
155 });
84156});
85157
86158describe("codexRateLimitsFailureMessage", () => {
apps/server/src/provider/Layers/codexUsageLimits.ts+11-1
......@@ -26,6 +26,7 @@ interface CodexRateLimitWindow {
2626
2727/** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */
2828export interface CodexRateLimitSnapshot {
29 readonly limitId?: string | null;
2930 readonly planType?: string | null;
3031 readonly primary?: CodexRateLimitWindow | null;
3132 readonly secondary?: CodexRateLimitWindow | null;
......@@ -68,6 +69,9 @@ function labelForKind(kind: ServerProviderUsageWindow["kind"]): string {
6869export function codexRateLimitsToWindows(
6970 snapshot: CodexRateLimitSnapshot,
7071): ReadonlyArray<ServerProviderUsageWindow> {
72 // Show the main allowance only. Model-specific notifications (such as Spark)
73 // must not replace its primary/secondary rows. Older CLIs omit the limit id.
74 if (snapshot.limitId && snapshot.limitId !== "codex") return [];
7175 const isMonthlyPlan = snapshot.planType === "free" || snapshot.planType === "go";
7276 const positions = [
7377 ["primary", snapshot.primary, isMonthlyPlan ? MONTH_MINS : SESSION_MINS],
......@@ -110,14 +114,20 @@ export function codexResetCreditsToContract(
110114
111115export function codexRateLimitsToLimits(input: {
112116 readonly snapshot: CodexRateLimitSnapshot;
117 readonly rateLimitsByLimitId?:
118 | Readonly<Record<string, CodexRateLimitSnapshot>>
119 | null
120 | undefined;
113121 readonly resetCredits?: CodexResetCreditsSummary | null | undefined;
114122 readonly checkedAt: string;
115123}): ServerProviderUsageLimits {
116124 const resetCredits = codexResetCreditsToContract(input.resetCredits);
125 // Select the main bucket explicitly; the legacy snapshot can name another limit.
126 const windows = codexRateLimitsToWindows(input.rateLimitsByLimitId?.codex ?? input.snapshot);
117127 return {
118128 ...makeUsageLimits({
119129 checkedAt: input.checkedAt,
120 windows: codexRateLimitsToWindows(input.snapshot),
130 windows,
121131 }),
122132 ...(resetCredits ? { resetCredits } : {}),
123133 };