Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/codex/warmup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class CodexWarmupError extends Error {
code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "invalid_sse" | "no_terminal" | "transport";
code: "http_status" | "missing_body" | "stream_failed" | "stream_incomplete" | "stream_error" | "stream_too_large" | "invalid_sse" | "no_terminal" | "transport";
status?: number;
/** Upstream error detail extracted from the response body (truncated to 512 chars). */
upstreamDetail?: string;
Expand Down Expand Up @@ -30,6 +30,7 @@ const DEFAULT_MODEL = "gpt-5.4-mini";
const FALLBACK_MODELS = ["gpt-5.5"];
const DEFAULT_TIMEOUT_MS = 30_000;
const MAX_ERROR_BODY_BYTES = 2048;
const MAX_WARMUP_STREAM_BYTES = 1024 * 1024;

/** Read the first MAX_ERROR_BODY_BYTES of a response body and extract an error message. */
async function readErrorDetail(res: Response): Promise<string | undefined> {
Expand Down Expand Up @@ -93,11 +94,16 @@ async function drainWarmupSse(body: ReadableStream<Uint8Array>): Promise<void> {
const reader = body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let bytesRead = 0;

try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
bytesRead += value.byteLength;
if (bytesRead > MAX_WARMUP_STREAM_BYTES) {
throw new CodexWarmupError("stream_too_large", "Codex warmup stream exceeded the size limit");
}
buffer += decoder.decode(value, { stream: true });

for (;;) {
Expand Down
14 changes: 14 additions & 0 deletions tests/codex-warmup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ describe("codex warmup", () => {
.rejects.toMatchObject({ name: "CodexWarmupError", code: "invalid_sse" });
});

test("rejects an oversized unterminated SSE stream", async () => {
const oversizedBody = new ReadableStream<Uint8Array>({
start(controller) {
const chunk = new Uint8Array(256 * 1024).fill(65);
for (let index = 0; index < 5; index += 1) controller.enqueue(chunk);
controller.close();
},
});
globalThis.fetch = (async () => new Response(oversizedBody, { status: 200 })) as typeof fetch;

await expect(warmCodexAccount({ accessToken: "a", chatgptAccountId: "c" }))
.rejects.toMatchObject({ name: "CodexWarmupError", code: "stream_too_large" });
});

test("rejects EOF before success terminal", async () => {
globalThis.fetch = (async () => sseResponse('event: response.created\ndata: {"type":"response.created"}\n\n')) as typeof fetch;
await expect(warmCodexAccount({ accessToken: "a", chatgptAccountId: "c" }))
Expand Down
Loading