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