Skip to content

Commit b45a69f

Browse files
fix(server): cool a Codex rate-limit burst for a minute, not fifteen
Co-Authored-By: GPT-5.6 Sol via Traex <noreply@openai.com>
1 parent cc427f6 commit b45a69f

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { assert, describe, it } from "@effect/vitest";
2+
import type { OrchestrationV2ProviderFailure } from "@t3tools/contracts";
3+
4+
import { limitCooldownUntilMs } from "./ProviderPoolFailover.ts";
5+
6+
const nowMs = Date.parse("2026-10-20T12:00:00.000Z");
7+
const futureResetAt = "2026-10-20T12:10:00.000Z";
8+
const pastResetAt = "2026-10-20T11:50:00.000Z";
9+
10+
const failure = (code: "rateLimitExceeded" | "usageLimitExceeded" | null, resetAt?: string) =>
11+
({
12+
class: "usage_limit",
13+
message: "Provider limit reached",
14+
code,
15+
retryable: null,
16+
...(resetAt === undefined ? {} : { resetAt }),
17+
}) satisfies OrchestrationV2ProviderFailure;
18+
19+
describe("limitCooldownUntilMs", () => {
20+
it.each(["rateLimitExceeded", "usageLimitExceeded"] as const)(
21+
"uses a future reset for %s",
22+
(code) => {
23+
assert.strictEqual(
24+
limitCooldownUntilMs(failure(code, futureResetAt), nowMs),
25+
Date.parse(futureResetAt),
26+
);
27+
},
28+
);
29+
30+
it.each([pastResetAt, undefined])(
31+
"uses a one-minute cooldown for a rate-limit burst with reset %s",
32+
(resetAt) => {
33+
assert.strictEqual(
34+
limitCooldownUntilMs(failure("rateLimitExceeded", resetAt), nowMs),
35+
nowMs + 60_000,
36+
);
37+
},
38+
);
39+
40+
it.each(["usageLimitExceeded", null] as const)(
41+
"uses the default cooldown for code %s without a future reset",
42+
(code) => {
43+
assert.strictEqual(limitCooldownUntilMs(failure(code), nowMs), nowMs + 15 * 60_000);
44+
},
45+
);
46+
});

‎apps/server/src/orchestration-v2/ProviderPoolFailover.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ export type UsageLimitResolution =
3434
| { readonly type: "finalize"; readonly resetAt: IsoDateTime | null };
3535

3636
export const DEFAULT_LIMIT_COOLDOWN_MS = 15 * 60_000;
37+
// Per-minute burst limits recover quickly and should not park a healthy account.
38+
export const BURST_LIMIT_COOLDOWN_MS = 60_000;
39+
40+
export function limitCooldownUntilMs(
41+
failure: OrchestrationV2ProviderFailure,
42+
nowMs: number,
43+
): number {
44+
const resetMs = failure.resetAt == null ? Number.NaN : Date.parse(failure.resetAt);
45+
if (resetMs > nowMs) return resetMs;
46+
return (
47+
nowMs +
48+
(failure.code === "rateLimitExceeded" ? BURST_LIMIT_COOLDOWN_MS : DEFAULT_LIMIT_COOLDOWN_MS)
49+
);
50+
}
3751

3852
/**
3953
* Runs between a usage-limit terminal and the run going terminal. A reference
@@ -59,14 +73,11 @@ const make = Effect.gen(function* () {
5973
input: UsageLimitedAttempt,
6074
) {
6175
const nowMs = DateTime.toEpochMillis(yield* DateTime.now);
62-
const resetMs = input.failure.resetAt == null ? Number.NaN : Date.parse(input.failure.resetAt);
6376
// Every driver cools through this one site, pooled or not, so the
6477
// Providers page shows "Limited until" for any account.
6578
yield* catalog.markLimited({
6679
instanceId: input.providerInstanceId,
67-
until: DateTime.formatIso(
68-
DateTime.makeUnsafe(resetMs > nowMs ? resetMs : nowMs + DEFAULT_LIMIT_COOLDOWN_MS),
69-
),
80+
until: DateTime.formatIso(DateTime.makeUnsafe(limitCooldownUntilMs(input.failure, nowMs))),
7081
});
7182
const asToday = { type: "finalize", resetAt: input.failure.resetAt ?? null } as const;
7283
const owner = findProviderPool(yield* catalog.getPublishedProviders, input.providerInstanceId);

0 commit comments

Comments
 (0)