| 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,6 +49,10 @@ const RATE_LIMITS_PROBE_TIMEOUT_MS = 3_000; |
| 49 | type CodexRateLimitsProbe = | 49 | type 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 | }); |
| 64 | 112 | ||
| 65 | describe("codexRateLimitsToUpdate", () => { | 113 | describe("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 | }); |
| 85 | 157 | ||
| 86 | describe("codexRateLimitsFailureMessage", () => { | 158 | describe("codexRateLimitsFailureMessage", () => { |
apps/server/src/provider/Layers/codexUsageLimits.ts+11-1| ... | @@ -26,6 +26,7 @@ interface CodexRateLimitWindow { | ... | @@ -26,6 +26,7 @@ interface CodexRateLimitWindow { |
| 26 | 26 | ||
| 27 | /** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */ | 27 | /** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */ |
| 28 | export interface CodexRateLimitSnapshot { | 28 | export 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 { |
| 68 | export function codexRateLimitsToWindows( | 69 | export 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( |
| 110 | 114 | ||
| 111 | export function codexRateLimitsToLimits(input: { | 115 | export 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 | }; |