Skip to content

Commit aef44bd

Browse files
committed
fix(codex): tighten quota rejection classification
1 parent 64f63b7 commit aef44bd

2 files changed

Lines changed: 118 additions & 20 deletions

File tree

src/codex/quota-rejection.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,42 @@ function rejection(
4545
};
4646
}
4747

48+
function hasOwnField(container: Record<string, unknown>, field: string): boolean {
49+
return Object.prototype.hasOwnProperty.call(container, field);
50+
}
51+
52+
function exactResetEligibleCode(
53+
container: Record<string, unknown>,
54+
): CodexResetEligibleExhaustionCode | undefined {
55+
const hasCode = hasOwnField(container, "code");
56+
const hasType = hasOwnField(container, "type");
57+
if (!hasCode && !hasType) return undefined;
58+
59+
const code = hasCode ? container.code : undefined;
60+
const type = hasType ? container.type : undefined;
61+
if ((hasCode && typeof code !== "string") || (hasType && typeof type !== "string")) {
62+
return undefined;
63+
}
64+
if (hasCode && hasType && code !== type) return undefined;
65+
66+
const value = hasCode ? code : type;
67+
if (typeof value !== "string") return undefined;
68+
return RESET_ELIGIBLE_CODES.has(value as CodexResetEligibleExhaustionCode)
69+
? value as CodexResetEligibleExhaustionCode
70+
: undefined;
71+
}
72+
4873
function structuredResetEligibleCode(payload: unknown): CodexResetEligibleExhaustionCode | undefined {
4974
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
5075
const root = payload as Record<string, unknown>;
51-
const containers: Record<string, unknown>[] = [root];
52-
if (root.error && typeof root.error === "object" && !Array.isArray(root.error)) {
53-
containers.push(root.error as Record<string, unknown>);
54-
}
55-
for (const container of containers) {
56-
for (const field of ["code", "type"] as const) {
57-
const value = container[field];
58-
if (typeof value !== "string") continue;
59-
const normalized = value.trim().toLowerCase();
60-
if (RESET_ELIGIBLE_CODES.has(normalized as CodexResetEligibleExhaustionCode)) {
61-
return normalized as CodexResetEligibleExhaustionCode;
62-
}
63-
}
64-
}
65-
return undefined;
76+
const hasRootDiscriminator = hasOwnField(root, "code") || hasOwnField(root, "type");
77+
78+
if (!hasOwnField(root, "error")) return exactResetEligibleCode(root);
79+
if (hasRootDiscriminator) return undefined;
80+
81+
const nested = root.error;
82+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) return undefined;
83+
return exactResetEligibleCode(nested as Record<string, unknown>);
6684
}
6785

6886
async function resetEligibleCodeFromResponse(

tests/codex-quota-rejection.test.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,27 @@ function jsonRejection(status: number, error: Record<string, unknown>): Response
55
return Response.json({ error }, { status });
66
}
77

8+
function jsonPayload(status: number, payload: Record<string, unknown>): Response {
9+
return Response.json(payload, { status });
10+
}
11+
812
describe("Codex pre-stream quota rejection classification", () => {
913
test.each([
10-
[429, "code", "usage_limit_exceeded"],
11-
[429, "type", "insufficient_quota"],
12-
[402, "code", "insufficient_quota"],
13-
] as const)("accepts structured reset-eligible exhaustion on HTTP %i", async (status, field, code) => {
14-
const result = await classifyCodexPreStreamRejection(jsonRejection(status, { [field]: code }));
14+
[429, "nested code", { error: { code: "usage_limit_exceeded" } }, "usage_limit_exceeded"],
15+
[429, "nested type", { error: { type: "insufficient_quota" } }, "insufficient_quota"],
16+
[402, "nested code", { error: { code: "insufficient_quota" } }, "insufficient_quota"],
17+
[429, "root code", { code: "usage_limit_exceeded" }, "usage_limit_exceeded"],
18+
[402, "root type", { type: "insufficient_quota" }, "insufficient_quota"],
19+
[429, "matching code and type", {
20+
error: { code: "usage_limit_exceeded", type: "usage_limit_exceeded" },
21+
}, "usage_limit_exceeded"],
22+
] as const)("accepts exact %s reset-eligible exhaustion on HTTP %i", async (
23+
status,
24+
_schema,
25+
payload,
26+
code,
27+
) => {
28+
const result = await classifyCodexPreStreamRejection(jsonPayload(status, payload));
1529
expect(result).toEqual({
1630
kind: "reset-eligible-exhaustion",
1731
status,
@@ -21,6 +35,72 @@ describe("Codex pre-stream quota rejection classification", () => {
2135
});
2236
});
2337

38+
test.each([
39+
["leading and trailing whitespace", { error: { code: " usage_limit_exceeded " } }],
40+
["uppercase", { error: { code: "USAGE_LIMIT_EXCEEDED" } }],
41+
["mixed case", { type: "Insufficient_Quota" }],
42+
["trailing whitespace at the root", { code: "insufficient_quota " }],
43+
] as const)("rejects the %s near-miss", async (_case, payload) => {
44+
const result = await classifyCodexPreStreamRejection(jsonPayload(429, payload));
45+
expect(result).toMatchObject({
46+
kind: "generic-rate-limit",
47+
alternateRetryEligible: true,
48+
resetCreditEligible: false,
49+
});
50+
expect(result).not.toHaveProperty("semanticCode");
51+
});
52+
53+
test.each([
54+
["primitive error with a root code", {
55+
error: "opaque",
56+
code: "usage_limit_exceeded",
57+
}],
58+
["matching root and nested codes", {
59+
code: "usage_limit_exceeded",
60+
error: { code: "usage_limit_exceeded" },
61+
}],
62+
["unknown root and eligible nested codes", {
63+
code: "unknown",
64+
error: { code: "usage_limit_exceeded" },
65+
}],
66+
["eligible root code with an empty nested error", {
67+
code: "usage_limit_exceeded",
68+
error: {},
69+
}],
70+
] as const)("fails closed for ambiguous schemas: %s", async (_case, payload) => {
71+
const result = await classifyCodexPreStreamRejection(jsonPayload(429, payload));
72+
expect(result).toMatchObject({
73+
kind: "generic-rate-limit",
74+
alternateRetryEligible: true,
75+
resetCreditEligible: false,
76+
});
77+
expect(result).not.toHaveProperty("semanticCode");
78+
});
79+
80+
test.each([
81+
["nested unknown code and eligible type", {
82+
error: { code: "unknown", type: "insufficient_quota" },
83+
}],
84+
["root eligible code and unrelated type", {
85+
code: "usage_limit_exceeded",
86+
type: "rate_limit_error",
87+
}],
88+
["two different eligible values", {
89+
error: { code: "usage_limit_exceeded", type: "insufficient_quota" },
90+
}],
91+
["eligible code and non-string type", {
92+
error: { code: "usage_limit_exceeded", type: null },
93+
}],
94+
] as const)("fails closed for code/type disagreement: %s", async (_case, payload) => {
95+
const result = await classifyCodexPreStreamRejection(jsonPayload(429, payload));
96+
expect(result).toMatchObject({
97+
kind: "generic-rate-limit",
98+
alternateRetryEligible: true,
99+
resetCreditEligible: false,
100+
});
101+
expect(result).not.toHaveProperty("semanticCode");
102+
});
103+
24104
test("keeps a generic 429 with Retry-After out of reset-credit eligibility", async () => {
25105
const response = jsonRejection(429, {
26106
type: "rate_limit_error",

0 commit comments

Comments
 (0)