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;...@@ -49,6 +49,10 @@ const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000;
49type CodexRateLimitsProbe =49type CodexRateLimitsProbe =
50 | {50 | {
51 readonly snapshot: CodexRateLimitSnapshot;51 readonly snapshot: CodexRateLimitSnapshot;
52 readonly rateLimitsByLimitId?:
53 | Readonly<Record<string, CodexRateLimitSnapshot>>
54 | null
55 | undefined;
52 readonly resetCredits: CodexResetCreditsSummary | null | undefined;56 readonly resetCredits: CodexResetCreditsSummary | null | undefined;
53 }57 }
54 | { readonly failure: string };58 | { readonly failure: string };
...@@ -434,6 +438,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun...@@ -434,6 +438,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
434 client.request("account/rateLimits/read", undefined).pipe(438 client.request("account/rateLimits/read", undefined).pipe(
435 Effect.map((response): CodexRateLimitsProbe => ({439 Effect.map((response): CodexRateLimitsProbe => ({
436 snapshot: response.rateLimits,440 snapshot: response.rateLimits,
441 rateLimitsByLimitId: response.rateLimitsByLimitId,
437 resetCredits: response.rateLimitResetCredits,442 resetCredits: response.rateLimitResetCredits,
438 })),443 })),
439 Effect.timeoutOption(Duration.millis(RATE_LIMITS_PROBE_TIMEOUT_MS)),444 Effect.timeoutOption(Duration.millis(RATE_LIMITS_PROBE_TIMEOUT_MS)),
...@@ -652,6 +657,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu...@@ -652,6 +657,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
652 })657 })
653 : codexRateLimitsToLimits({658 : codexRateLimitsToLimits({
654 snapshot: snapshot.rateLimits.snapshot,659 snapshot: snapshot.rateLimits.snapshot,
660 rateLimitsByLimitId: snapshot.rateLimits.rateLimitsByLimitId,
655 resetCredits: snapshot.rateLimits.resetCredits,661 resetCredits: snapshot.rateLimits.resetCredits,
656 checkedAt,662 checkedAt,
657 });663 });
apps/server/src/provider/Layers/codexUsageLimits.test.ts+72
...@@ -60,6 +60,54 @@ describe("codexRateLimitsToLimits", () => {...@@ -60,6 +60,54 @@ describe("codexRateLimitsToLimits", () => {
60 },60 },
61 ]);61 ]);
62 });62 });
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 });
63});111});
64112
65describe("codexRateLimitsToUpdate", () => {113describe("codexRateLimitsToUpdate", () => {
...@@ -81,6 +129,30 @@ describe("codexRateLimitsToUpdate", () => {...@@ -81,6 +129,30 @@ describe("codexRateLimitsToUpdate", () => {
81 });129 });
82 expect(codexRateLimitsToUpdate({ planType: "plus" })).toBeUndefined();130 expect(codexRateLimitsToUpdate({ planType: "plus" })).toBeUndefined();
83 });131 });
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 });
84});156});
85157
86describe("codexRateLimitsFailureMessage", () => {158describe("codexRateLimitsFailureMessage", () => {
apps/server/src/provider/Layers/codexUsageLimits.ts+11-1
...@@ -26,6 +26,7 @@ interface CodexRateLimitWindow {...@@ -26,6 +26,7 @@ interface CodexRateLimitWindow {
2626
27/** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */27/** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */
28export interface CodexRateLimitSnapshot {28export interface CodexRateLimitSnapshot {
29 readonly limitId?: string | null;
29 readonly planType?: string | null;30 readonly planType?: string | null;
30 readonly primary?: CodexRateLimitWindow | null;31 readonly primary?: CodexRateLimitWindow | null;
31 readonly secondary?: CodexRateLimitWindow | null;32 readonly secondary?: CodexRateLimitWindow | null;
...@@ -68,6 +69,9 @@ function labelForKind(kind: ServerProviderUsageWindow["kind"]): string {...@@ -68,6 +69,9 @@ function labelForKind(kind: ServerProviderUsageWindow["kind"]): string {
68export function codexRateLimitsToWindows(69export function codexRateLimitsToWindows(
69 snapshot: CodexRateLimitSnapshot,70 snapshot: CodexRateLimitSnapshot,
70): ReadonlyArray<ServerProviderUsageWindow> {71): 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 [];
71 const isMonthlyPlan = snapshot.planType === "free" || snapshot.planType === "go";75 const isMonthlyPlan = snapshot.planType === "free" || snapshot.planType === "go";
72 const positions = [76 const positions = [
73 ["primary", snapshot.primary, isMonthlyPlan ? MONTH_MINS : SESSION_MINS],77 ["primary", snapshot.primary, isMonthlyPlan ? MONTH_MINS : SESSION_MINS],
...@@ -110,14 +114,20 @@ export function codexResetCreditsToContract(...@@ -110,14 +114,20 @@ export function codexResetCreditsToContract(
110114
111export function codexRateLimitsToLimits(input: {115export function codexRateLimitsToLimits(input: {
112 readonly snapshot: CodexRateLimitSnapshot;116 readonly snapshot: CodexRateLimitSnapshot;
117 readonly rateLimitsByLimitId?:
118 | Readonly<Record<string, CodexRateLimitSnapshot>>
119 | null
120 | undefined;
113 readonly resetCredits?: CodexResetCreditsSummary | null | undefined;121 readonly resetCredits?: CodexResetCreditsSummary | null | undefined;
114 readonly checkedAt: string;122 readonly checkedAt: string;
115}): ServerProviderUsageLimits {123}): ServerProviderUsageLimits {
116 const resetCredits = codexResetCreditsToContract(input.resetCredits);124 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);
117 return {127 return {
118 ...makeUsageLimits({128 ...makeUsageLimits({
119 checkedAt: input.checkedAt,129 checkedAt: input.checkedAt,
120 windows: codexRateLimitsToWindows(input.snapshot),130 windows,
121 }),131 }),
122 ...(resetCredits ? { resetCredits } : {}),132 ...(resetCredits ? { resetCredits } : {}),
123 };133 };