mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
Add the constrained sampling API and provider plumbing for OpenAI and Anthropic tool calls, including grammar sampling variants and model capability metadata.
2525 lines
77 KiB
TypeScript
2525 lines
77 KiB
TypeScript
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { zstdDecompressSync } from "node:zlib";
|
|
import { Type } from "typebox";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
closeOpenAICodexWebSocketSessions,
|
|
getOpenAICodexWebSocketDebugStats,
|
|
resetOpenAICodexWebSocketDebugStats,
|
|
stream as streamOpenAICodexResponses,
|
|
streamSimple as streamSimpleOpenAICodexResponses,
|
|
} from "../src/api/openai-codex-responses.ts";
|
|
import type { Context, Model } from "../src/types.ts";
|
|
|
|
const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
if (originalAgentDir === undefined) {
|
|
delete process.env.PI_CODING_AGENT_DIR;
|
|
} else {
|
|
process.env.PI_CODING_AGENT_DIR = originalAgentDir;
|
|
}
|
|
closeOpenAICodexWebSocketSessions();
|
|
resetOpenAICodexWebSocketDebugStats();
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function mockToken(): string {
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
|
|
"utf8",
|
|
).toString("base64");
|
|
return `aaa.${payload}.bbb`;
|
|
}
|
|
|
|
function decodeCodexRequestBody(body: RequestInit["body"] | undefined): Record<string, unknown> | null {
|
|
if (typeof body === "string") {
|
|
return JSON.parse(body) as Record<string, unknown>;
|
|
}
|
|
if (body instanceof Uint8Array) {
|
|
return JSON.parse(Buffer.from(zstdDecompressSync(body)).toString("utf8")) as Record<string, unknown>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function buildSSEPayload({
|
|
status,
|
|
includeDone = false,
|
|
}: {
|
|
status: "completed" | "incomplete";
|
|
includeDone?: boolean;
|
|
}): string {
|
|
const terminalType = status === "incomplete" ? "response.incomplete" : "response.completed";
|
|
const events = [
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: terminalType,
|
|
response: {
|
|
status,
|
|
incomplete_details: status === "incomplete" ? { reason: "max_output_tokens" } : null,
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
];
|
|
|
|
if (includeDone) {
|
|
events.push("data: [DONE]");
|
|
}
|
|
|
|
return `${events.join("\n\n")}\n\n`;
|
|
}
|
|
|
|
describe("openai-codex streaming", () => {
|
|
it("streams SSE responses into AssistantMessageEventStream", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
|
|
"utf8",
|
|
).toString("base64");
|
|
const token = `aaa.${payload}.bbb`;
|
|
|
|
const sse = `${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`;
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
const headers = init?.headers instanceof Headers ? init.headers : undefined;
|
|
expect(headers?.get("Authorization")).toBe(`Bearer ${token}`);
|
|
expect(headers?.get("chatgpt-account-id")).toBe("acc_test");
|
|
expect(headers?.get("OpenAI-Beta")).toBe("responses=experimental");
|
|
expect(headers?.get("originator")).toBe("pi");
|
|
expect(headers?.get("accept")).toBe("text/event-stream");
|
|
expect(headers?.has("x-api-key")).toBe(false);
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" });
|
|
let sawTextDelta = false;
|
|
let sawDone = false;
|
|
|
|
for await (const event of streamResult) {
|
|
if (event.type === "text_delta") {
|
|
sawTextDelta = true;
|
|
}
|
|
if (event.type === "done") {
|
|
sawDone = true;
|
|
expect(event.message.content.find((c) => c.type === "text")?.text).toBe("Hello");
|
|
}
|
|
}
|
|
|
|
expect(sawTextDelta).toBe(true);
|
|
expect(sawDone).toBe(true);
|
|
});
|
|
|
|
it("completes after response.completed even when the SSE body stays open", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed", includeDone: true });
|
|
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const result = await Promise.race([
|
|
streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }).result(),
|
|
new Promise<never>((_, reject) => {
|
|
setTimeout(() => reject(new Error("Timed out waiting for completed SSE stream")), 1000);
|
|
}),
|
|
]);
|
|
|
|
expect(result.content.find((c) => c.type === "text")?.text).toBe("Hello");
|
|
expect(result.stopReason).toBe("stop");
|
|
});
|
|
|
|
it("maps response.incomplete to stopReason length even when the SSE body stays open", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "incomplete" });
|
|
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const result = await Promise.race([
|
|
streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" }).result(),
|
|
new Promise<never>((_, reject) => {
|
|
setTimeout(() => reject(new Error("Timed out waiting for incomplete SSE stream")), 1000);
|
|
}),
|
|
]);
|
|
|
|
expect(result.content.find((c) => c.type === "text")?.text).toBe("Hello");
|
|
expect(result.stopReason).toBe("length");
|
|
});
|
|
|
|
it("aborts SSE fetch after the configured HTTP timeout when response headers do not arrive", async () => {
|
|
const token = mockToken();
|
|
|
|
const fetchMock = vi.fn((input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
|
|
const signal = init?.signal;
|
|
if (!signal) {
|
|
throw new Error("Expected SSE fetch to receive an abort signal");
|
|
}
|
|
|
|
return new Promise<Response>((_, reject) => {
|
|
const onAbort = () => {
|
|
const reason = signal.reason;
|
|
reject(reason instanceof Error ? reason : new Error("SSE fetch aborted"));
|
|
};
|
|
if (signal.aborted) {
|
|
onAbort();
|
|
return;
|
|
}
|
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
});
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const result = await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
timeoutMs: 10,
|
|
}).result();
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(result.stopReason).toBe("error");
|
|
expect(result.errorMessage).toBe("Codex SSE response headers timed out after 10ms");
|
|
});
|
|
|
|
it("aborts SSE body reads after response headers arrive", async () => {
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
let cancelled = false;
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
const enqueue = (chunk: string) => {
|
|
if (!cancelled) controller.enqueue(encoder.encode(chunk));
|
|
};
|
|
enqueue(
|
|
`${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "one" })}`,
|
|
].join("\n\n")}\n\n`,
|
|
);
|
|
timers.push(
|
|
setTimeout(() => {
|
|
enqueue(`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "two" })}\n\n`);
|
|
}, 10),
|
|
);
|
|
timers.push(
|
|
setTimeout(() => {
|
|
if (cancelled) return;
|
|
enqueue(
|
|
`${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "onetwo" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`,
|
|
);
|
|
controller.close();
|
|
}, 20),
|
|
);
|
|
},
|
|
cancel() {
|
|
cancelled = true;
|
|
for (const timer of timers) clearTimeout(timer);
|
|
},
|
|
});
|
|
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } })),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
const controller = new AbortController();
|
|
const events: string[] = [];
|
|
|
|
const resultStream = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
signal: controller.signal,
|
|
});
|
|
for await (const event of resultStream) {
|
|
events.push(event.type === "text_delta" ? `text_delta:${event.delta}` : event.type);
|
|
if (event.type === "text_delta" && event.delta === "one") {
|
|
controller.abort();
|
|
}
|
|
}
|
|
|
|
const result = await resultStream.result();
|
|
expect(result.stopReason).toBe("aborted");
|
|
expect(result.errorMessage).toBe("Request was aborted");
|
|
expect(events).toContain("text_delta:one");
|
|
expect(events).not.toContain("text_delta:two");
|
|
expect(cancelled).toBe(true);
|
|
});
|
|
|
|
it("sets session-id/x-client-request-id headers and prompt_cache_key when sessionId is provided", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
|
|
"utf8",
|
|
).toString("base64");
|
|
const token = `aaa.${payload}.bbb`;
|
|
|
|
const sse = `${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`;
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const sessionId = "test-session-123";
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
const headers = init?.headers instanceof Headers ? init.headers : undefined;
|
|
// Verify sessionId is set in headers
|
|
expect(headers?.get("session-id")).toBe(sessionId);
|
|
expect(headers?.has("session_id")).toBe(false);
|
|
expect(headers?.get("x-client-request-id")).toBe(sessionId);
|
|
|
|
// Verify sessionId is set in request body as prompt_cache_key
|
|
const body = decodeCodexRequestBody(init?.body);
|
|
expect(body?.prompt_cache_key).toBe(sessionId);
|
|
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, sessionId, transport: "sse" });
|
|
await streamResult.result();
|
|
});
|
|
|
|
it("omits SSE cache affinity when cacheRetention is none", async () => {
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
let capturedHeaders: Headers | undefined;
|
|
let capturedBody: Record<string, unknown> | null = null;
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (_input: string | URL, init?: RequestInit) => {
|
|
capturedHeaders = init?.headers instanceof Headers ? init.headers : undefined;
|
|
capturedBody = decodeCodexRequestBody(init?.body);
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" })));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
}),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
cacheRetention: "none",
|
|
sessionId: "one-off-summary",
|
|
transport: "sse",
|
|
}).result();
|
|
|
|
expect(capturedHeaders?.has("session-id")).toBe(false);
|
|
expect(capturedHeaders?.has("x-client-request-id")).toBe(false);
|
|
expect(capturedBody).not.toHaveProperty("prompt_cache_key");
|
|
});
|
|
|
|
it("clamps prompt_cache_key to OpenAI's 64-character limit", async () => {
|
|
const token = mockToken();
|
|
const sessionId = "x".repeat(67);
|
|
let capturedPayload: { prompt_cache_key?: string } | undefined;
|
|
const encoder = new TextEncoder();
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(
|
|
async () =>
|
|
new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" })));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
),
|
|
),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
sessionId,
|
|
onPayload: (payload) => {
|
|
capturedPayload = payload as { prompt_cache_key?: string };
|
|
},
|
|
}).result();
|
|
|
|
expect(capturedPayload?.prompt_cache_key).toBe("x".repeat(64));
|
|
});
|
|
|
|
it("clamps Codex session-id header to 64 characters", async () => {
|
|
const token = mockToken();
|
|
const sessionId = "x".repeat(67);
|
|
let capturedHeaders: Headers | undefined;
|
|
const encoder = new TextEncoder();
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (_input: string | URL, init?: RequestInit) => {
|
|
capturedHeaders = init?.headers instanceof Headers ? init.headers : undefined;
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" })));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
}),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
sessionId,
|
|
}).result();
|
|
|
|
expect(capturedHeaders?.get("session-id")).toBe("x".repeat(64));
|
|
expect(capturedHeaders?.get("x-client-request-id")).toBe("x".repeat(64));
|
|
});
|
|
|
|
it("preserves gpt-5.5 xhigh reasoning effort from simple options", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
const token = mockToken();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
let requestedReasoning: unknown;
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
const body = decodeCodexRequestBody(init?.body);
|
|
requestedReasoning = body?.reasoning;
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.5",
|
|
name: "GPT-5.5",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
thinkingLevelMap: { xhigh: "xhigh" },
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
await streamSimpleOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
reasoning: "xhigh",
|
|
transport: "sse",
|
|
}).result();
|
|
|
|
expect(requestedReasoning).toEqual({ effort: "xhigh", summary: "auto" });
|
|
});
|
|
|
|
it("forwards required tool choice", async () => {
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
let requestedToolChoice: unknown;
|
|
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (_input: string | URL, init?: RequestInit) => {
|
|
requestedToolChoice = decodeCodexRequestBody(init?.body)?.tool_choice;
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
}),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.5",
|
|
name: "GPT-5.5",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
await streamOpenAICodexResponses(
|
|
model,
|
|
{
|
|
messages: [
|
|
{ role: "user", content: "Do not call ping. Respond with text instead.", timestamp: Date.now() },
|
|
],
|
|
tools: [
|
|
{
|
|
name: "ping",
|
|
description: "Ping",
|
|
parameters: Type.Object({ value: Type.String() }),
|
|
},
|
|
],
|
|
},
|
|
{ apiKey: token, transport: "sse", toolChoice: "required" },
|
|
).result();
|
|
|
|
expect(requestedToolChoice).toBe("required");
|
|
});
|
|
|
|
it("sets Codex strict mode explicitly and honors constrained sampling", async () => {
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
let requestedTools: Array<{ type?: string; name?: string; strict?: boolean | null }> | undefined;
|
|
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(
|
|
async () =>
|
|
new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
),
|
|
),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.5",
|
|
name: "GPT-5.5",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
await streamOpenAICodexResponses(
|
|
model,
|
|
{
|
|
messages: [{ role: "user", content: "Use a tool", timestamp: Date.now() }],
|
|
tools: [
|
|
{
|
|
name: "optional",
|
|
description: "Optional constrained sampling",
|
|
parameters: Type.Object({ value: Type.String() }),
|
|
constrainedSampling: false,
|
|
},
|
|
{
|
|
name: "strict",
|
|
description: "Strict constrained sampling",
|
|
parameters: Type.Object({ value: Type.String() }, { additionalProperties: false }),
|
|
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
apiKey: token,
|
|
transport: "sse",
|
|
onPayload: (payload) => {
|
|
requestedTools = (payload as { tools?: typeof requestedTools }).tools;
|
|
},
|
|
},
|
|
).result();
|
|
|
|
expect(requestedTools).toMatchObject([
|
|
{ type: "function", name: "optional", strict: null },
|
|
{ type: "function", name: "strict", strict: true },
|
|
]);
|
|
});
|
|
|
|
it.each(["gpt-5.3-codex", "gpt-5.4", "gpt-5.5"])("clamps %s minimal reasoning effort to low", async (modelId) => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
|
|
"utf8",
|
|
).toString("base64");
|
|
const token = `aaa.${payload}.bbb`;
|
|
|
|
const sse = `${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`;
|
|
|
|
let requestedReasoning: unknown;
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
const body = decodeCodexRequestBody(init?.body);
|
|
requestedReasoning = body?.reasoning;
|
|
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: modelId,
|
|
name: modelId,
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
thinkingLevelMap: { minimal: "low" },
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const streamResult = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
reasoningEffort: "minimal",
|
|
transport: "sse",
|
|
});
|
|
await streamResult.result();
|
|
expect(requestedReasoning).toEqual({ effort: "low", summary: "auto" });
|
|
});
|
|
|
|
it.each([
|
|
["gpt-5.1-codex", "flex", 0.5],
|
|
["gpt-5.1-codex", "priority", 2],
|
|
["gpt-5.5", "flex", 0.5],
|
|
["gpt-5.5", "priority", 2.5],
|
|
] as const)(
|
|
"uses the client-sent %s service tier for %s when Codex echoes default",
|
|
async (modelId, serviceTier, multiplier) => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
const token = mockToken();
|
|
const sse = `${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
service_tier: "default",
|
|
usage: {
|
|
input_tokens: 1000000,
|
|
output_tokens: 1000000,
|
|
total_tokens: 2000000,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`;
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: modelId,
|
|
name: modelId === "gpt-5.5" ? "GPT-5.5" : "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const result = await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
serviceTier,
|
|
transport: "sse",
|
|
}).result();
|
|
|
|
expect(result.usage.cost.input).toBe(1 * multiplier);
|
|
expect(result.usage.cost.output).toBe(2 * multiplier);
|
|
expect(result.usage.cost.total).toBe(3 * multiplier);
|
|
},
|
|
);
|
|
|
|
it("does not set session-id/x-client-request-id headers when sessionId is not provided", async () => {
|
|
const tempDir = mkdtempSync(join(tmpdir(), "pi-codex-stream-"));
|
|
process.env.PI_CODING_AGENT_DIR = tempDir;
|
|
|
|
const payload = Buffer.from(
|
|
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
|
|
"utf8",
|
|
).toString("base64");
|
|
const token = `aaa.${payload}.bbb`;
|
|
|
|
const sse = `${[
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
})}`,
|
|
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
|
|
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello" })}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
})}`,
|
|
`data: ${JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
})}`,
|
|
].join("\n\n")}\n\n`;
|
|
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url === "https://api.github.com/repos/openai/codex/releases/latest") {
|
|
return new Response(JSON.stringify({ tag_name: "rust-v0.0.0" }), { status: 200 });
|
|
}
|
|
if (url.startsWith("https://raw.githubusercontent.com/openai/codex/")) {
|
|
return new Response("PROMPT", { status: 200, headers: { etag: '"etag"' } });
|
|
}
|
|
if (url === "https://chatgpt.com/backend-api/codex/responses") {
|
|
const headers = init?.headers instanceof Headers ? init.headers : undefined;
|
|
// Verify headers are not set when sessionId is not provided
|
|
expect(headers?.has("session-id")).toBe(false);
|
|
expect(headers?.has("session_id")).toBe(false);
|
|
expect(headers?.has("x-client-request-id")).toBe(false);
|
|
|
|
return new Response(stream, {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
});
|
|
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
// No sessionId provided
|
|
const streamResult = streamOpenAICodexResponses(model, context, { apiKey: token, transport: "sse" });
|
|
await streamResult.result();
|
|
});
|
|
it("forwards auto transport from streamSimple options and uses cached websocket context", async () => {
|
|
const token = mockToken();
|
|
const sentBodies: unknown[] = [];
|
|
let capturedWebSocketHeaders: Record<string, string> | undefined;
|
|
|
|
const fetchMock = vi.fn(async () => new Response("unexpected fetch", { status: 500 }));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
class MockWebSocket {
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
if (protocols && typeof protocols === "object" && !Array.isArray(protocols)) {
|
|
capturedWebSocketHeaders = protocols.headers;
|
|
}
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(data: string): void {
|
|
sentBodies.push(JSON.parse(data));
|
|
const events = [
|
|
{
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
},
|
|
{ type: "response.content_part.added", part: { type: "output_text", text: "" } },
|
|
{ type: "response.output_text.delta", delta: "Hello" },
|
|
{
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "message",
|
|
id: "msg_1",
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: "Hello" }],
|
|
},
|
|
},
|
|
{
|
|
type: "response.completed",
|
|
response: {
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
queueMicrotask(() => {
|
|
for (const event of events) {
|
|
this.dispatch("message", { data: JSON.stringify(event) });
|
|
}
|
|
});
|
|
}
|
|
|
|
close(): void {}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
await streamSimpleOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
sessionId: "session-auto",
|
|
transport: "auto",
|
|
}).result();
|
|
|
|
expect(sentBodies).toHaveLength(1);
|
|
expect(capturedWebSocketHeaders?.["session-id"]).toBe("session-auto");
|
|
expect(capturedWebSocketHeaders?.session_id).toBeUndefined();
|
|
expect(capturedWebSocketHeaders?.["x-client-request-id"]).toBe("session-auto");
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
expect(getOpenAICodexWebSocketDebugStats("session-auto")).toMatchObject({
|
|
cachedContextRequests: 1,
|
|
fullContextRequests: 1,
|
|
});
|
|
});
|
|
|
|
it("closes one-shot websockets when cacheRetention is none", async () => {
|
|
const token = mockToken();
|
|
const sentBodies: Array<{ prompt_cache_key?: string }> = [];
|
|
let connections = 0;
|
|
let closedConnections = 0;
|
|
|
|
class MockWebSocket {
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor() {
|
|
connections++;
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(data: string): void {
|
|
sentBodies.push(JSON.parse(data) as { prompt_cache_key?: string });
|
|
queueMicrotask(() => {
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
id: `resp_${connections}`,
|
|
status: "completed",
|
|
usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 },
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
close(): void {
|
|
closedConnections++;
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => new Response("unexpected fetch", { status: 500 })),
|
|
);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
const options = {
|
|
apiKey: token,
|
|
cacheRetention: "none" as const,
|
|
sessionId: "one-off-summary",
|
|
transport: "auto" as const,
|
|
};
|
|
|
|
await streamOpenAICodexResponses(model, context, options).result();
|
|
await streamOpenAICodexResponses(model, context, options).result();
|
|
|
|
expect(connections).toBe(2);
|
|
expect(closedConnections).toBe(2);
|
|
expect(sentBodies).toHaveLength(2);
|
|
expect(sentBodies.every((body) => body.prompt_cache_key === undefined)).toBe(true);
|
|
expect(getOpenAICodexWebSocketDebugStats("one-off-summary")).toBeUndefined();
|
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to SSE when websocket connect does not open before the connect timeout", async () => {
|
|
vi.useFakeTimers();
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
class MockWebSocket {
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(): void {
|
|
throw new Error("send should not be called before websocket open");
|
|
}
|
|
|
|
close(): void {}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
const resultPromise = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
sessionId: "ws-connect-timeout",
|
|
transport: "auto",
|
|
timeoutMs: 300_000,
|
|
websocketConnectTimeoutMs: 50,
|
|
}).result();
|
|
|
|
await vi.advanceTimersByTimeAsync(50);
|
|
|
|
const result = await resultPromise;
|
|
expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello");
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(getOpenAICodexWebSocketDebugStats("ws-connect-timeout")).toMatchObject({
|
|
websocketFailures: 1,
|
|
sseFallbacks: 1,
|
|
websocketFallbackActive: true,
|
|
lastWebSocketError: "WebSocket connect timeout after 50ms",
|
|
});
|
|
});
|
|
|
|
it("reconnects once when the websocket connection limit is reached before output starts", async () => {
|
|
const token = mockToken();
|
|
let connections = 0;
|
|
|
|
const fetchMock = vi.fn();
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
class MockWebSocket extends EventTarget {
|
|
private readonly limitReached = connections++ === 0;
|
|
|
|
constructor() {
|
|
super();
|
|
queueMicrotask(() => this.dispatchEvent(new Event("open")));
|
|
}
|
|
|
|
send(): void {
|
|
const event = this.limitReached
|
|
? { type: "error", error: { code: "websocket_connection_limit_reached" } }
|
|
: {
|
|
type: "response.completed",
|
|
response: {
|
|
id: "resp_1",
|
|
status: "completed",
|
|
usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 },
|
|
},
|
|
};
|
|
queueMicrotask(() => {
|
|
this.dispatchEvent(Object.assign(new Event("message"), { data: JSON.stringify(event) }));
|
|
});
|
|
}
|
|
|
|
close(): void {}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const result = await streamOpenAICodexResponses(
|
|
model,
|
|
{ systemPrompt: "", messages: [] },
|
|
{
|
|
apiKey: token,
|
|
},
|
|
).result();
|
|
|
|
expect(result.stopReason).toBe("stop");
|
|
expect(connections).toBe(2);
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to SSE when a websocket is idle before the first event", async () => {
|
|
vi.useFakeTimers();
|
|
const token = mockToken();
|
|
const sentBodies: unknown[] = [];
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
class MockWebSocket {
|
|
static OPEN = 1;
|
|
readyState = MockWebSocket.OPEN;
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, _protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(data: string): void {
|
|
sentBodies.push(JSON.parse(data));
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = 3;
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
const resultPromise = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
sessionId: "ws-idle-before-start",
|
|
transport: "auto",
|
|
timeoutMs: 50,
|
|
}).result();
|
|
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(sentBodies).toHaveLength(1);
|
|
await vi.advanceTimersByTimeAsync(50);
|
|
|
|
const result = await resultPromise;
|
|
expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello");
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
expect(getOpenAICodexWebSocketDebugStats("ws-idle-before-start")).toMatchObject({
|
|
websocketFailures: 1,
|
|
sseFallbacks: 1,
|
|
websocketFallbackActive: true,
|
|
});
|
|
});
|
|
|
|
it("errors when a websocket is idle after the stream started", async () => {
|
|
vi.useFakeTimers();
|
|
const token = mockToken();
|
|
|
|
const fetchMock = vi.fn(async () => new Response("unexpected fetch", { status: 500 }));
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
class MockWebSocket {
|
|
static OPEN = 1;
|
|
readyState = MockWebSocket.OPEN;
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, _protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(): void {
|
|
queueMicrotask(() => {
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({
|
|
type: "response.output_item.added",
|
|
item: { type: "message", id: "msg_1", role: "assistant", status: "in_progress", content: [] },
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = 3;
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
const resultPromise = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "auto",
|
|
timeoutMs: 50,
|
|
}).result();
|
|
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
await vi.advanceTimersByTimeAsync(50);
|
|
|
|
const result = await resultPromise;
|
|
expect(result.stopReason).toBe("error");
|
|
expect(result.errorMessage).toBe("WebSocket idle timeout after 50ms");
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("opens a fresh cached websocket before the backend connection age limit", async () => {
|
|
vi.useFakeTimers();
|
|
const startedAt = new Date("2026-07-03T00:00:00Z");
|
|
vi.setSystemTime(startedAt);
|
|
const token = mockToken();
|
|
const sentConnectionIds: number[] = [];
|
|
let connections = 0;
|
|
|
|
class MockWebSocket {
|
|
static OPEN = 1;
|
|
static CLOSED = 3;
|
|
readyState = MockWebSocket.OPEN;
|
|
private readonly connectionId = ++connections;
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, _protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(): void {
|
|
sentConnectionIds.push(this.connectionId);
|
|
const responseId = `resp_${this.connectionId}`;
|
|
queueMicrotask(() => {
|
|
this.dispatch("message", {
|
|
data: JSON.stringify({
|
|
type: "response.completed",
|
|
response: {
|
|
id: responseId,
|
|
status: "completed",
|
|
usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 },
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = MockWebSocket.CLOSED;
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const sessionId = "aged-ws-session";
|
|
const firstContext: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
const first = await streamOpenAICodexResponses(model, firstContext, {
|
|
apiKey: token,
|
|
sessionId,
|
|
transport: "websocket-cached",
|
|
}).result();
|
|
vi.setSystemTime(new Date(startedAt.getTime() + 56 * 60 * 1000));
|
|
const secondContext: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [...firstContext.messages, first, { role: "user", content: "Now finish", timestamp: 2 }],
|
|
};
|
|
|
|
await streamOpenAICodexResponses(model, secondContext, {
|
|
apiKey: token,
|
|
sessionId,
|
|
transport: "websocket-cached",
|
|
}).result();
|
|
|
|
expect(connections).toBe(2);
|
|
expect(sentConnectionIds).toEqual([1, 2]);
|
|
expect(getOpenAICodexWebSocketDebugStats(sessionId)).toMatchObject({
|
|
connectionsCreated: 2,
|
|
connectionsReused: 0,
|
|
});
|
|
});
|
|
|
|
it("sends only response input deltas in websocket-cached mode", async () => {
|
|
const token = mockToken();
|
|
const sentBodies: unknown[] = [];
|
|
|
|
class MockWebSocket {
|
|
static OPEN = 1;
|
|
readyState = MockWebSocket.OPEN;
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, _protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(data: string): void {
|
|
sentBodies.push(JSON.parse(data));
|
|
const responseId = `resp_${sentBodies.length}`;
|
|
const outputEvents =
|
|
sentBodies.length === 1
|
|
? [
|
|
{
|
|
type: "response.output_item.added",
|
|
item: {
|
|
type: "custom_tool_call",
|
|
id: "ctc_1",
|
|
call_id: "call_1",
|
|
name: "sample_tool",
|
|
input: "",
|
|
},
|
|
},
|
|
{ type: "response.custom_tool_call_input.delta", item_id: "ctc_1", delta: "abc" },
|
|
{ type: "response.custom_tool_call_input.done", item_id: "ctc_1", input: "abc" },
|
|
{
|
|
type: "response.output_item.done",
|
|
item: {
|
|
type: "custom_tool_call",
|
|
id: "ctc_1",
|
|
call_id: "call_1",
|
|
name: "sample_tool",
|
|
input: "abc",
|
|
},
|
|
},
|
|
]
|
|
: [];
|
|
const events = [
|
|
{ type: "response.created", response: { id: responseId } },
|
|
...outputEvents,
|
|
{
|
|
type: "response.completed",
|
|
response: {
|
|
id: responseId,
|
|
status: "completed",
|
|
usage: {
|
|
input_tokens: 5,
|
|
output_tokens: 3,
|
|
total_tokens: 8,
|
|
input_tokens_details: { cached_tokens: 0 },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
queueMicrotask(() => {
|
|
for (const event of events) {
|
|
this.dispatch("message", { data: JSON.stringify(event) });
|
|
}
|
|
});
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = 3;
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
compat: { supportsOpenAIGrammarTools: true },
|
|
};
|
|
const firstContext: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Use the tool", timestamp: 1 }],
|
|
tools: [
|
|
{
|
|
name: "sample_tool",
|
|
description: "Sample tool",
|
|
parameters: Type.Object({ payload: Type.String() }),
|
|
constrainedSampling: { type: "grammar", variants: { openai_lark: "start: /[a-z]+/" } },
|
|
},
|
|
],
|
|
};
|
|
|
|
const first = await streamOpenAICodexResponses(model, firstContext, {
|
|
apiKey: token,
|
|
sessionId: "session-1",
|
|
transport: "websocket-cached",
|
|
}).result();
|
|
|
|
const secondContext: Context = {
|
|
...firstContext,
|
|
messages: [
|
|
...firstContext.messages,
|
|
first,
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "call_1|ctc_1",
|
|
toolName: "sample_tool",
|
|
content: [{ type: "text", text: "real result" }],
|
|
isError: false,
|
|
timestamp: 2,
|
|
},
|
|
{ role: "user", content: "Now finish", timestamp: 3 },
|
|
],
|
|
};
|
|
await streamOpenAICodexResponses(model, secondContext, {
|
|
apiKey: token,
|
|
sessionId: "session-1",
|
|
transport: "websocket-cached",
|
|
}).result();
|
|
|
|
expect(sentBodies).toHaveLength(2);
|
|
const firstBody = sentBodies[0] as { input: unknown[]; previous_response_id?: string; store?: boolean };
|
|
const secondBody = sentBodies[1] as { input: unknown[]; previous_response_id?: string; store?: boolean };
|
|
expect(firstBody.store).toBe(false);
|
|
expect(firstBody.previous_response_id).toBeUndefined();
|
|
expect(firstBody.input).toEqual([{ role: "user", content: [{ type: "input_text", text: "Use the tool" }] }]);
|
|
expect(secondBody.store).toBe(false);
|
|
expect(secondBody.previous_response_id).toBe("resp_1");
|
|
expect(secondBody.input).toEqual([
|
|
{ type: "custom_tool_call_output", call_id: "call_1", output: "real result" },
|
|
{ role: "user", content: [{ type: "input_text", text: "Now finish" }] },
|
|
]);
|
|
expect(getOpenAICodexWebSocketDebugStats("session-1")).toMatchObject({
|
|
requests: 2,
|
|
connectionsCreated: 1,
|
|
connectionsReused: 1,
|
|
cachedContextRequests: 2,
|
|
storeTrueRequests: 0,
|
|
fullContextRequests: 1,
|
|
deltaRequests: 1,
|
|
lastDeltaInputItems: 2,
|
|
lastPreviousResponseId: "resp_1",
|
|
});
|
|
});
|
|
|
|
it.each(["websocket", "sse"] as const)(
|
|
"recovers a missing cached websocket continuation via %s",
|
|
async (recoveryTransport) => {
|
|
const token = mockToken();
|
|
const sessionId = `missing-continuation-${recoveryTransport}`;
|
|
const encoder = new TextEncoder();
|
|
const fetchMock = vi.fn(
|
|
async () =>
|
|
new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(buildSSEPayload({ status: "completed" })));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
),
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
const sentBodies: Array<{
|
|
connectionId: number;
|
|
input: unknown[];
|
|
previous_response_id?: string;
|
|
}> = [];
|
|
let connections = 0;
|
|
|
|
class MockWebSocket {
|
|
static OPEN = 1;
|
|
static CLOSED = 3;
|
|
readyState = MockWebSocket.OPEN;
|
|
private readonly connectionId = ++connections;
|
|
private listeners = new Map<string, Set<(event: unknown) => void>>();
|
|
|
|
constructor(_url: string, _protocols?: string | string[] | { headers?: Record<string, string> }) {
|
|
queueMicrotask(() => this.dispatch("open", {}));
|
|
}
|
|
|
|
addEventListener(type: string, listener: (event: unknown) => void): void {
|
|
let listeners = this.listeners.get(type);
|
|
if (!listeners) {
|
|
listeners = new Set();
|
|
this.listeners.set(type, listeners);
|
|
}
|
|
listeners.add(listener);
|
|
}
|
|
|
|
removeEventListener(type: string, listener: (event: unknown) => void): void {
|
|
this.listeners.get(type)?.delete(listener);
|
|
}
|
|
|
|
send(data: string): void {
|
|
const body = JSON.parse(data) as { input: unknown[]; previous_response_id?: string };
|
|
sentBodies.push({ ...body, connectionId: this.connectionId });
|
|
if (sentBodies.length === 2) {
|
|
this.dispatchEvents([
|
|
{
|
|
type: "codex.rate_limits",
|
|
plan_type: "plus",
|
|
rate_limits: {
|
|
allowed: true,
|
|
limit_reached: false,
|
|
primary: {
|
|
used_percent: 7,
|
|
window_minutes: 10080,
|
|
reset_after_seconds: 556112,
|
|
reset_at: 1785269351,
|
|
},
|
|
secondary: null,
|
|
},
|
|
code_review_rate_limits: null,
|
|
additional_rate_limits: null,
|
|
credits: { has_credits: false, unlimited: false, balance: "0" },
|
|
promo: null,
|
|
},
|
|
{
|
|
type: "error",
|
|
status: 400,
|
|
error: {
|
|
code: "previous_response_not_found",
|
|
message: "Previous response with id 'resp_1' not found.",
|
|
param: "previous_response_id",
|
|
},
|
|
},
|
|
]);
|
|
return;
|
|
}
|
|
if (sentBodies.length === 3 && recoveryTransport === "sse") {
|
|
queueMicrotask(() => this.dispatch("error", { message: "retry websocket failed" }));
|
|
return;
|
|
}
|
|
|
|
const response =
|
|
sentBodies.length === 1
|
|
? { responseId: "resp_1", messageId: "msg_1", text: "Hello" }
|
|
: { responseId: "resp_2", messageId: "msg_2", text: "Recovered" };
|
|
this.dispatchEvents([
|
|
{ type: "response.created", response: { id: response.responseId } },
|
|
{
|
|
type: "response.output_item.added",
|
|
output_index: 0,
|
|
item: {
|
|
type: "message",
|
|
id: response.messageId,
|
|
role: "assistant",
|
|
status: "in_progress",
|
|
content: [],
|
|
},
|
|
},
|
|
{
|
|
type: "response.output_item.done",
|
|
output_index: 0,
|
|
item: {
|
|
type: "message",
|
|
id: response.messageId,
|
|
role: "assistant",
|
|
status: "completed",
|
|
content: [{ type: "output_text", text: response.text }],
|
|
},
|
|
},
|
|
{
|
|
type: "response.completed",
|
|
response: {
|
|
id: response.responseId,
|
|
status: "completed",
|
|
usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8 },
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
|
|
close(): void {
|
|
this.readyState = MockWebSocket.CLOSED;
|
|
}
|
|
|
|
private dispatchEvents(events: unknown[]): void {
|
|
queueMicrotask(() => {
|
|
for (const event of events) {
|
|
this.dispatch("message", { data: JSON.stringify(event) });
|
|
}
|
|
});
|
|
}
|
|
|
|
private dispatch(type: string, event: unknown): void {
|
|
for (const listener of this.listeners.get(type) ?? []) {
|
|
listener(event);
|
|
}
|
|
}
|
|
}
|
|
|
|
vi.stubGlobal("WebSocket", MockWebSocket);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const firstContext: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: 1 }],
|
|
};
|
|
|
|
const first = await streamOpenAICodexResponses(model, firstContext, {
|
|
apiKey: token,
|
|
sessionId,
|
|
transport: "websocket-cached",
|
|
}).result();
|
|
const secondContext: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [...firstContext.messages, first, { role: "user", content: "Now finish", timestamp: 2 }],
|
|
};
|
|
const eventTypes: string[] = [];
|
|
const secondStream = streamOpenAICodexResponses(model, secondContext, {
|
|
apiKey: token,
|
|
sessionId,
|
|
transport: "websocket-cached",
|
|
});
|
|
for await (const event of secondStream) {
|
|
eventTypes.push(event.type);
|
|
}
|
|
const second = await secondStream.result();
|
|
|
|
expect(second.stopReason).toBe("stop");
|
|
expect(second.content.find((content) => content.type === "text")?.text).toBe(
|
|
recoveryTransport === "sse" ? "Hello" : "Recovered",
|
|
);
|
|
expect(eventTypes.filter((type) => type === "start")).toHaveLength(1);
|
|
expect(eventTypes).not.toContain("error");
|
|
expect(connections).toBe(2);
|
|
expect(sentBodies).toHaveLength(3);
|
|
expect(sentBodies.map((body) => body.connectionId)).toEqual([1, 1, 2]);
|
|
expect(sentBodies[1].previous_response_id).toBe("resp_1");
|
|
expect(sentBodies[1].input).toEqual([{ role: "user", content: [{ type: "input_text", text: "Now finish" }] }]);
|
|
expect(sentBodies[2].previous_response_id).toBeUndefined();
|
|
expect(sentBodies[2].input).toHaveLength(3);
|
|
expect(sentBodies[2].input.at(-1)).toEqual({
|
|
role: "user",
|
|
content: [{ type: "input_text", text: "Now finish" }],
|
|
});
|
|
expect(fetchMock).toHaveBeenCalledTimes(recoveryTransport === "sse" ? 1 : 0);
|
|
expect(getOpenAICodexWebSocketDebugStats(sessionId)).toMatchObject({
|
|
requests: 3,
|
|
connectionsCreated: 2,
|
|
connectionsReused: 1,
|
|
fullContextRequests: 2,
|
|
deltaRequests: 1,
|
|
websocketFailures: recoveryTransport === "sse" ? 1 : 0,
|
|
sseFallbacks: recoveryTransport === "sse" ? 1 : 0,
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["retry-after-ms", () => ({ "content-type": "application/json", "retry-after-ms": "1500" }), 1500],
|
|
["retry-after seconds", () => ({ "content-type": "application/json", "retry-after": "60" }), 60_000],
|
|
[
|
|
"retry-after HTTP date",
|
|
() => ({ "content-type": "application/json", "retry-after": new Date(Date.now() + 45_000).toUTCString() }),
|
|
45_000,
|
|
],
|
|
] as const)("uses %s for SSE retries", async (_name, makeHeaders, expectedDelay) => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-05-13T00:00:00Z"));
|
|
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
let codexRequests = 0;
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
|
|
codexRequests++;
|
|
if (codexRequests === 1) {
|
|
return new Response(JSON.stringify({ error: { code: "rate_limit_exceeded", message: "rate limited" } }), {
|
|
status: 429,
|
|
headers: makeHeaders(),
|
|
});
|
|
}
|
|
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const resultPromise = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
maxRetries: 1,
|
|
}).result();
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedDelay);
|
|
|
|
await vi.advanceTimersToNextTimerAsync();
|
|
const result = await resultPromise;
|
|
expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello");
|
|
expect(codexRequests).toBe(2);
|
|
});
|
|
|
|
it.each([429, 503])("fails immediately when a %i retry delay exceeds the limit", async (status) => {
|
|
const token = mockToken();
|
|
const fetchMock = vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ error: { code: "temporarily_unavailable", message: "retry later" } }), {
|
|
status,
|
|
headers: { "content-type": "application/json", "retry-after": "2" },
|
|
}),
|
|
);
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const result = await streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
maxRetries: 3,
|
|
maxRetryDelayMs: 1000,
|
|
}).result();
|
|
|
|
expect(result.stopReason).toBe("error");
|
|
expect(result.errorMessage).toBe("Server requested 2s retry delay (max: 1s)");
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("zstd-compresses SSE request bodies", async () => {
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
|
|
let capturedEncoding: string | null = null;
|
|
let capturedBody: Uint8Array | string | undefined;
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL, init?: RequestInit) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
const headers = init?.headers instanceof Headers ? init.headers : undefined;
|
|
capturedEncoding = headers?.get("content-encoding") ?? null;
|
|
capturedBody = init?.body as Uint8Array | string | undefined;
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
|
|
const largeText = "compress me ".repeat(400);
|
|
await streamOpenAICodexResponses(
|
|
model,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: largeText, timestamp: 1 }],
|
|
},
|
|
{ apiKey: token, transport: "sse" },
|
|
).result();
|
|
|
|
expect(capturedEncoding).toBe("zstd");
|
|
expect(capturedBody).toBeInstanceOf(Uint8Array);
|
|
const decoded = JSON.parse(Buffer.from(zstdDecompressSync(capturedBody as Uint8Array)).toString("utf8")) as {
|
|
input: Array<{ content: Array<{ text: string }> }>;
|
|
};
|
|
expect(decoded.input[0].content[0].text).toBe(largeText);
|
|
|
|
capturedEncoding = null;
|
|
capturedBody = undefined;
|
|
await streamOpenAICodexResponses(
|
|
model,
|
|
{
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "hi", timestamp: 1 }],
|
|
},
|
|
{ apiKey: token, transport: "sse" },
|
|
).result();
|
|
|
|
expect(capturedEncoding).toBe("zstd");
|
|
expect(capturedBody).toBeInstanceOf(Uint8Array);
|
|
});
|
|
|
|
it("uses exponential backoff across repeated SSE retries without retry headers", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-05-13T00:00:00Z"));
|
|
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
|
|
const token = mockToken();
|
|
const encoder = new TextEncoder();
|
|
const sse = buildSSEPayload({ status: "completed" });
|
|
let codexRequests = 0;
|
|
|
|
const fetchMock = vi.fn(async (input: string | URL) => {
|
|
const url = typeof input === "string" ? input : input.toString();
|
|
if (url !== "https://chatgpt.com/backend-api/codex/responses") {
|
|
throw new Error(`Unexpected URL: ${url}`);
|
|
}
|
|
|
|
codexRequests++;
|
|
if (codexRequests <= 3) {
|
|
return new Response(JSON.stringify({ error: { code: "rate_limit_exceeded", message: "rate limited" } }), {
|
|
status: 429,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
return new Response(
|
|
new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(encoder.encode(sse));
|
|
controller.close();
|
|
},
|
|
}),
|
|
{ status: 200, headers: { "content-type": "text/event-stream" } },
|
|
);
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
const model: Model<"openai-codex-responses"> = {
|
|
id: "gpt-5.1-codex",
|
|
name: "GPT-5.1 Codex",
|
|
api: "openai-codex-responses",
|
|
provider: "openai-codex",
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
reasoning: true,
|
|
input: ["text"],
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 400000,
|
|
maxTokens: 128000,
|
|
};
|
|
const context: Context = {
|
|
systemPrompt: "You are a helpful assistant.",
|
|
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
|
|
};
|
|
|
|
const retryTimeoutDelays = () =>
|
|
setTimeoutSpy.mock.calls
|
|
.map((call) => call[1])
|
|
.filter((delay): delay is number => delay === 1000 || delay === 2000 || delay === 4000);
|
|
|
|
const resultPromise = streamOpenAICodexResponses(model, context, {
|
|
apiKey: token,
|
|
transport: "sse",
|
|
maxRetries: 3,
|
|
}).result();
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
expect(retryTimeoutDelays()).toEqual([1000]);
|
|
|
|
await vi.advanceTimersToNextTimerAsync();
|
|
expect(retryTimeoutDelays()).toEqual([1000, 2000]);
|
|
|
|
await vi.advanceTimersToNextTimerAsync();
|
|
expect(retryTimeoutDelays()).toEqual([1000, 2000, 4000]);
|
|
|
|
await vi.advanceTimersToNextTimerAsync();
|
|
const result = await resultPromise;
|
|
expect(result.content.find((content) => content.type === "text")?.text).toBe("Hello");
|
|
expect(codexRequests).toBe(4);
|
|
});
|
|
});
|