Files
oh-my-pi/packages/ai/test/openai-codex-stream.test.ts
T
can1357 0a099af4ab fix(ai): persist final tool-call args on output_item.done finalization
The per-delta streaming-parse throttle (parseStreamingJsonThrottled) can
skip the last partial re-parse of a tool-call's arguments. For OpenAI
Responses and Codex streams that finalize a function_call via
response.output_item.done WITHOUT a trailing
response.function_call_arguments.done event, the handler built a fresh
toolCall with the authoritative full-buffer parse for the emitted
toolcall_end event but never wrote those args back to the block stored in
output.content. The persisted assistant message therefore retained the
last throttled partial parse (often {}), so subsequent tool execution and
history replay used stale/empty arguments.

Write the authoritative final args onto the persisted block before
dropping the streaming bookkeeping in both handlers.

Also update the stale codex idle-timeout test: bc7afc143 began stripping
partialJson/lastParseLen on finalization, but its expectation still
required partialJson: "" on a block finalized via output_item.done.

Tests: add regression coverage in both providers driving throttled deltas
that finalize via output_item.done with no args.done event, asserting the
persisted block carries the full parsed arguments and no streaming
internals. Verified both fail without the source fix.
2026-05-31 05:37:52 +02:00

3079 lines
112 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "bun:test";
import { enrichModelThinking } from "@oh-my-pi/pi-ai/model-thinking";
import {
getOpenAICodexTransportDetails,
getOpenAICodexWebSocketDebugStats,
prewarmOpenAICodexResponses,
streamOpenAICodexResponses,
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import type { Context, Model, ProviderSessionState } from "@oh-my-pi/pi-ai/types";
import { getAgentDir, setAgentDir, TempDir } from "@oh-my-pi/pi-utils";
const originalFetch = global.fetch;
const originalAgentDir = getAgentDir();
const originalWebSocket = global.WebSocket;
const originalCodexWebSocketRetryBudget = Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET;
const originalCodexWebSocketRetryDelayMs = Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS;
const originalCodexWebSocketV2 = Bun.env.PI_CODEX_WEBSOCKET_V2;
const originalCodexWebSocketIdleTimeoutMs = Bun.env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS;
const originalCodexWebSocketFirstEventTimeoutMs = Bun.env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS;
const originalCodexWebSocketPingIntervalMs = Bun.env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS;
const originalCodexWebSocketPongTimeoutMs = Bun.env.PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS;
const originalCodexWebSocketMessageQueueCapacity = Bun.env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY;
const originalCodexWebSocketMaxIdleReuseMs = Bun.env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS;
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete Bun.env[name];
return;
}
Bun.env[name] = value;
}
afterEach(() => {
global.fetch = originalFetch;
global.WebSocket = originalWebSocket;
setAgentDir(originalAgentDir);
restoreEnv("PI_CODEX_WEBSOCKET_RETRY_BUDGET", originalCodexWebSocketRetryBudget);
restoreEnv("PI_CODEX_WEBSOCKET_RETRY_DELAY_MS", originalCodexWebSocketRetryDelayMs);
restoreEnv("PI_CODEX_WEBSOCKET_V2", originalCodexWebSocketV2);
restoreEnv("PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS", originalCodexWebSocketIdleTimeoutMs);
restoreEnv("PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS", originalCodexWebSocketFirstEventTimeoutMs);
restoreEnv("PI_CODEX_WEBSOCKET_PING_INTERVAL_MS", originalCodexWebSocketPingIntervalMs);
restoreEnv("PI_CODEX_WEBSOCKET_PONG_TIMEOUT_MS", originalCodexWebSocketPongTimeoutMs);
restoreEnv("PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY", originalCodexWebSocketMessageQueueCapacity);
restoreEnv("PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS", originalCodexWebSocketMaxIdleReuseMs);
vi.restoreAllMocks();
});
function createCodexTestToken(accountId = "acc_test"): string {
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: accountId } }),
"utf8",
).toBase64();
return `aaa.${payload}.bbb`;
}
function createCodexTestModel(baseUrl?: string): Model<"openai-codex-responses"> {
return {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: baseUrl ?? "",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
}
function createCodexTestContext(): Context {
return {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
}
function createCompletedCodexSse(text: string): string {
return `${[
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: text })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_1", role: "assistant", status: "completed", content: [{ type: "output_text", text }] } })}`,
`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`;
}
function getRequestSignal(input: string | URL | Request, init: RequestInit | undefined): AbortSignal | undefined {
if (init?.signal) return init.signal;
if (input instanceof Request) return input.signal;
return undefined;
}
function createNoProgressCodexSse(signal: AbortSignal | undefined): Response {
const encoder = new TextEncoder();
let interval: NodeJS.Timeout | undefined;
let abortListener: (() => void) | undefined;
const encode = (event: unknown): Uint8Array => encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(
encode({
type: "response.output_item.added",
item: {
type: "function_call",
id: "fc_stalled",
call_id: "call_stalled",
name: "todo_write",
arguments: "",
},
}),
);
interval = setInterval(() => {
controller.enqueue(
encode({
type: "response.in_progress",
response: { id: "resp_stalled", status: "in_progress" },
}),
);
}, 2);
abortListener = () => {
if (interval) clearInterval(interval);
if (abortListener) signal?.removeEventListener("abort", abortListener);
const reason = signal?.reason;
controller.error(reason instanceof Error ? reason : new Error("request aborted"));
};
if (signal?.aborted) {
queueMicrotask(() => abortListener?.());
} else {
signal?.addEventListener("abort", abortListener, { once: true });
}
},
cancel() {
if (interval) clearInterval(interval);
if (abortListener) signal?.removeEventListener("abort", abortListener);
},
});
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}
function encodeWebSocketMessage(value: Record<string, unknown>): Uint8Array {
return new TextEncoder().encode(JSON.stringify(value));
}
type WsHeaders = Record<string, string>;
type WsEventType = "open" | "message" | "error" | "close";
const DEFAULT_USAGE = {
input_tokens: 5,
output_tokens: 3,
total_tokens: 8,
input_tokens_details: { cached_tokens: 0 },
};
/**
* Drop-in mock for the global `WebSocket` used by the codex websocket transport.
*
* Production code wires lifecycle handlers via `onopen`/`onmessage`/`onerror`/`onclose`
* properties; tests drive the connection by calling `emit()`, `scheduleOpen()`,
* `sendJson()`, or the `emitCodexResponse()` convenience.
*/
class MockWebSocket {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSING = 2;
static readonly CLOSED = 3;
readyState: number = MockWebSocket.CONNECTING;
binaryType: "blob" | "arraybuffer" | "nodebuffer" = "blob";
onopen: ((event: Event) => void) | null = null;
onmessage: ((event: MessageEvent) => void) | null = null;
onerror: ((event: Event) => void) | null = null;
onclose: ((event: Event) => void) | null = null;
constructor(
public readonly url: string,
public readonly options?: { headers?: WsHeaders },
) {}
send(_data: string): void {}
close(): void {
this.readyState = MockWebSocket.CLOSED;
}
/** Dispatch an event to the matching `on{type}` handler. */
emit(type: WsEventType, event: Event): void {
const handler = (this as unknown as Record<string, unknown>)[`on${type}`];
if (typeof handler === "function") (handler as (e: Event) => void).call(this, event);
}
/** Asynchronously transition to OPEN and emit `open`. */
scheduleOpen(): void {
setTimeout(() => {
this.readyState = MockWebSocket.OPEN;
this.emit("open", new Event("open"));
}, 0);
}
/** Emit a message frame with arbitrary data. */
sendMessage(data: unknown): void {
this.emit("message", { data } as unknown as MessageEvent);
}
/** Emit a message frame with stringified-JSON data. */
sendJson(payload: Record<string, unknown>): void {
this.sendMessage(JSON.stringify(payload));
}
/** Emit the standard Codex completed-response sequence. */
emitCodexResponse(opts: {
messageId: string;
responseId: string;
text: string;
terminalType?: "response.done" | "response.completed";
includeCreated?: boolean;
}): void {
const { messageId, responseId, text, terminalType = "response.done", includeCreated = false } = opts;
if (includeCreated) {
this.sendJson({ type: "response.created", response: { id: responseId } });
}
this.sendJson({
type: "response.output_item.added",
item: { type: "message", id: messageId, role: "assistant", status: "in_progress", content: [] },
});
this.sendJson({ type: "response.content_part.added", part: { type: "output_text", text: "" } });
this.sendJson({ type: "response.output_text.delta", delta: text });
this.sendJson({
type: "response.output_item.done",
item: {
type: "message",
id: messageId,
role: "assistant",
status: "completed",
content: [{ type: "output_text", text }],
},
});
this.sendJson({
type: terminalType,
response: {
id: responseId,
status: "completed",
usage: DEFAULT_USAGE,
},
});
}
}
describe("openai-codex streaming", () => {
it("normalizes Codex response endpoint base URLs", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const context = createCodexTestContext();
const requestedUrls: string[] = [];
const sse = createCompletedCodexSse("Hello");
global.fetch = vi.fn(async (input: string | URL) => {
requestedUrls.push(typeof input === "string" ? input : input.toString());
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
}) as unknown as typeof fetch;
for (const baseUrl of [
undefined,
"https://chatgpt.com/backend-api",
"https://chatgpt.com/backend-api/codex",
"https://chatgpt.com/backend-api/codex/responses",
]) {
const model = { ...createCodexTestModel(baseUrl), preferWebsockets: false };
const result = await streamOpenAICodexResponses(model, context, { apiKey: token }).result();
expect(result.stopReason).toBe("stop");
}
expect(requestedUrls).toEqual([
"https://chatgpt.com/backend-api/codex/responses",
"https://chatgpt.com/backend-api/codex/responses",
"https://chatgpt.com/backend-api/codex/responses",
"https://chatgpt.com/backend-api/codex/responses",
]);
});
it("persists final tool-call args when SSE finalizes via output_item.done without an args.done event", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const context = createCodexTestContext();
// Two small arg deltas: the second grows the buffer far less than the
// throttle's min-growth threshold, so the throttled parser skips the final
// re-parse. No function_call_arguments.done is sent, leaving
// output_item.done as the sole finalization path; it must still persist the
// full arguments on the stored block rather than the stale partial parse.
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "read_file", arguments: "" } })}`,
`data: ${JSON.stringify({ type: "response.function_call_arguments.delta", item_id: "fc_1", delta: '{"path":"' })}`,
`data: ${JSON.stringify({ type: "response.function_call_arguments.delta", item_id: "fc_1", delta: 'README.md"}' })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "function_call", id: "fc_1", call_id: "call_1", name: "read_file", arguments: '{"path":"README.md"}' } })}`,
`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`;
global.fetch = vi.fn(
async () => new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
) as unknown as typeof fetch;
const model = { ...createCodexTestModel("https://chatgpt.com/backend-api"), preferWebsockets: false };
const result = await streamOpenAICodexResponses(model, context, { apiKey: token }).result();
const toolCall = result.content.find(c => c.type === "toolCall");
if (toolCall?.type !== "toolCall") throw new Error("expected a finalized toolCall block");
expect(toolCall.arguments).toEqual({ path: "README.md" });
expect("partialJson" in toolCall).toBe(false);
expect("lastParseLen" in toolCall).toBe(false);
});
it("waits for caller abort when SSE streams only no-progress status events", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const context = createCodexTestContext();
global.fetch = ((input: string | URL | Request, init?: RequestInit) =>
Promise.resolve(createNoProgressCodexSse(getRequestSignal(input, init)))) as typeof fetch;
const controller = new AbortController();
setTimeout(() => controller.abort(), 30);
const model = { ...createCodexTestModel("https://chatgpt.com/backend-api"), preferWebsockets: false };
const result = await streamOpenAICodexResponses(model, context, {
apiKey: token,
signal: controller.signal,
}).result();
expect(result.stopReason).toBe("aborted");
expect(result.errorMessage).not.toBe("OpenAI Codex SSE stream stalled while waiting for the next event");
expect(result.content as unknown[]).toEqual([
{
type: "toolCall",
id: "call_stalled|fc_stalled",
name: "todo_write",
arguments: {},
partialJson: "",
},
]);
});
it("parses websocket JSON from non-string payloads", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
class BinaryPayloadWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
const added = encodeWebSocketMessage({
type: "response.output_item.added",
item: { type: "message", id: "msg_ws", role: "assistant", status: "in_progress", content: [] },
});
const contentPart = encodeWebSocketMessage({
type: "response.content_part.added",
part: { type: "output_text", text: "" },
});
const delta = encodeWebSocketMessage({ type: "response.output_text.delta", delta: "Hello binary" });
const done = encodeWebSocketMessage({
type: "response.output_item.done",
item: {
type: "message",
id: "msg_ws",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Hello binary" }],
},
});
const completed = encodeWebSocketMessage({
type: "response.done",
response: { id: "resp_ws", status: "completed", usage: DEFAULT_USAGE },
});
// Exercise every payload shape the production decoder must accept.
this.sendMessage(added.buffer.slice(added.byteOffset, added.byteOffset + added.byteLength));
this.sendMessage(contentPart);
this.sendMessage(Buffer.from(delta));
this.sendMessage(Buffer.from(done));
this.sendMessage(completed.buffer.slice(completed.byteOffset, completed.byteOffset + completed.byteLength));
}
}
global.WebSocket = BinaryPayloadWebSocket as unknown as typeof WebSocket;
const result = await streamOpenAICodexResponses(
createCodexTestModel("https://chatgpt.com/backend-api"),
createCodexTestContext(),
{
apiKey: token,
sessionId: "ws-binary-payload-session",
providerSessionState: new Map<string, ProviderSessionState>(),
},
).result();
expect(result.content.find(block => block.type === "text")?.text).toBe("Hello binary");
expect(result.stopReason).toBe("stop");
});
it("forwards websocket frames through onSseEvent for the raw-SSE debug viewer", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
class ObservedWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
this.emitCodexResponse({ messageId: "msg_obs", responseId: "resp_obs", text: "Observed" });
}
}
global.WebSocket = ObservedWebSocket as unknown as typeof WebSocket;
const observed: Array<{ event: string | null; data: string; raw: string[] }> = [];
const result = await streamOpenAICodexResponses(
createCodexTestModel("https://chatgpt.com/backend-api"),
createCodexTestContext(),
{
apiKey: token,
sessionId: "ws-observer-session",
providerSessionState: new Map<string, ProviderSessionState>(),
onSseEvent: event => {
observed.push({ event: event.event, data: event.data, raw: [...event.raw] });
},
},
).result();
expect(result.stopReason).toBe("stop");
// First record is the outbound request frame (the JSON we sent).
const [outbound, ...inbound] = observed;
expect(outbound).toBeDefined();
expect(outbound.raw[0]).toMatch(/^: ws → /);
expect(outbound.data.length).toBeGreaterThan(0);
expect(() => JSON.parse(outbound.data)).not.toThrow();
// Inbound frames mirror the Codex response sequence emitted by `emitCodexResponse`.
expect(inbound.map(e => e.event)).toEqual([
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_item.done",
"response.done",
]);
for (const event of inbound) {
expect(event.raw[0]).toBe(`: ws ← ${event.event}`);
// Synthesized SSE wire shape: `event:` line then `data:` line.
expect(event.raw[1]).toBe(`event: ${event.event}`);
expect(event.raw[2]).toBe(`data: ${event.data}`);
expect(JSON.parse(event.data)).toMatchObject({ type: event.event });
}
});
it("sends websocket protocol pings while the connection is open", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS = "1";
const token = createCodexTestToken();
let pingCount = 0;
class HeartbeatWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
ping(): void {
pingCount += 1;
}
send(): void {
setTimeout(() => {
this.emitCodexResponse({ messageId: "msg_ping", responseId: "resp_ping", text: "Pinged" });
}, 10);
}
}
global.WebSocket = HeartbeatWebSocket as unknown as typeof WebSocket;
const providerSessionState = new Map<string, ProviderSessionState>();
const result = await streamOpenAICodexResponses(
createCodexTestModel("https://chatgpt.com/backend-api"),
createCodexTestContext(),
{
apiKey: token,
sessionId: "ws-heartbeat-session",
providerSessionState,
},
).result();
expect(result.stopReason).toBe("stop");
expect(pingCount).toBeGreaterThan(0);
for (const state of providerSessionState.values()) {
state.close();
}
});
it("falls back to SSE when the websocket inbound queue overflows", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_MESSAGE_QUEUE_CAPACITY = "1";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const token = createCodexTestToken();
const sse = createCompletedCodexSse("Recovered over SSE");
const fetchMock = vi.fn(async () => {
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
class QueueOverflowWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
this.sendJson({ type: "response.created", response: { id: "resp_overflow" } });
this.sendJson({
type: "response.output_item.added",
item: { type: "message", id: "msg_overflow", role: "assistant", status: "in_progress", content: [] },
});
}
}
global.WebSocket = QueueOverflowWebSocket as unknown as typeof WebSocket;
const providerSessionState = new Map<string, ProviderSessionState>();
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const result = await streamOpenAICodexResponses(model, createCodexTestContext(), {
apiKey: token,
sessionId: "ws-queue-overflow-session",
providerSessionState,
}).result();
expect(result.stopReason).toBe("stop");
expect(result.role).toBe("assistant");
expect(fetchMock).toHaveBeenCalled();
const details = getOpenAICodexTransportDetails(model, {
sessionId: "ws-queue-overflow-session",
providerSessionState,
});
expect(details.lastTransport).toBe("sse");
expect(details.websocketDisabled).toBe(true);
expect(details.fallbackCount).toBe(1);
});
it("omits request-body headers and replaces stale beta headers for websocket handshakes", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
let capturedHeaders: Record<string, string> | undefined;
class HeaderCaptureWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
capturedHeaders = options?.headers;
this.scheduleOpen();
}
send(): void {
this.sendJson({
type: "response.done",
response: {
id: "resp_ws",
status: "completed",
usage: {
input_tokens: 1,
output_tokens: 1,
total_tokens: 2,
input_tokens_details: { cached_tokens: 0 },
},
},
});
}
}
global.WebSocket = HeaderCaptureWebSocket as unknown as typeof WebSocket;
await streamOpenAICodexResponses(
createCodexTestModel("https://chatgpt.com/backend-api"),
createCodexTestContext(),
{
apiKey: token,
headers: {
accept: "application/json",
"content-type": "application/json",
"OpenAI-Beta": "responses=experimental",
"openai-beta": "responses=stale",
},
sessionId: "ws-header-session",
providerSessionState: new Map<string, ProviderSessionState>(),
},
).result();
expect(capturedHeaders?.accept).toBeUndefined();
expect(capturedHeaders?.["content-type"]).toBeUndefined();
expect(capturedHeaders?.["openai-beta"]).toBe("responses_websockets=2026-02-06");
expect(Object.keys(capturedHeaders ?? {}).filter(key => key.toLowerCase() === "openai-beta")).toHaveLength(1);
});
it("streams SSE responses into AssistantMessageEventStream", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 });
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("includes service_tier in SSE payloads when requested", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
let capturedBody: Record<string, unknown> | undefined;
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: 5, output_tokens: 3, total_tokens: 8, input_tokens_details: { cached_tokens: 0 } } } })}`,
].join("\n\n")}\n\n`;
const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
capturedBody = JSON.parse(String(init?.body)) as Record<string, unknown>;
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
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: 1, output: 2, cacheRead: 0.5, 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: "priority",
}).result();
expect(result.stopReason).toBe("stop");
expect(capturedBody?.service_tier).toBe("priority");
expect(result.usage.cost.input).toBeCloseTo(0.00001);
expect(result.usage.cost.output).toBeCloseTo(0.000012);
expect(result.usage.cost.total).toBeCloseTo(0.000022);
});
it("fails truncated SSE streams that never emit a terminal response event", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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" }],
},
})}`,
].join("\n\n")}\n\n`;
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") {
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 }).result();
expect(result.stopReason).toBe("error");
expect(result.errorMessage).toContain("terminal completion event");
});
it("stops reading SSE responses after a terminal response event", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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.done", response: { status: "completed", usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8, input_tokens_details: { cached_tokens: 0 } } } })}`,
`data: ${JSON.stringify({ type: "response.failed", code: "server_error", message: "late failure after terminal event" })}`,
].join("\n\n")}\n\n`;
global.fetch = vi.fn(
async () => new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
) as unknown as typeof fetch;
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 }).result();
expect(result.stopReason).toBe("stop");
expect(result.content.find(block => block.type === "text")?.text).toBe("Hello");
});
it("surfaces 429 errors after retry budget checks without body reuse failures", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
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") {
return new Response(
JSON.stringify({
error: {
code: "rate_limit_exceeded",
message: "too many requests",
},
}),
{
status: 429,
headers: {
"content-type": "application/json",
"retry-after": "600",
},
},
);
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 }).result();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(result.stopReason).toBe("error");
expect((result.errorMessage ?? "").toLowerCase()).toContain("rate limit");
expect(result.errorMessage).not.toContain("Body already used");
});
it("retries transient model_error SSE events before surfacing an error", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
let requestCount = 0;
const successSse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_retry", 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 after retry" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_retry", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Hello after retry" }] } })}`,
`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 errorSse = `${[
`data: ${JSON.stringify({
type: "error",
code: "model_error",
message: "An error occurred while processing your request. You can retry your request.",
})}`,
].join("\n\n")}\n\n`;
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") {
requestCount += 1;
return new Response(requestCount === 1 ? errorSse : successSse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 }).result();
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(result.stopReason).toBe("stop");
expect(result.content.find(block => block.type === "text")?.text).toBe("Hello after retry");
});
it("sets conversation_id/session_id headers and prompt_cache_key when sessionId is provided", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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({
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("conversation_id")).toBe(sessionId);
expect(headers?.get("session_id")).toBe(sessionId);
expect(headers?.get("x-client-request-id")).toBe(sessionId);
// Verify sessionId is set in request body as prompt_cache_key
const body = typeof init?.body === "string" ? (JSON.parse(init.body) as Record<string, unknown>) : null;
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 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 });
await streamResult.result();
});
it("keeps prompt_cache_key separate from Codex conversation headers", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const sessionId = "side-channel-session";
const promptCacheKey = "main-session-cache";
let capturedHeaders: Headers | undefined;
let capturedBody: Record<string, unknown> | undefined;
global.fetch = 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") {
capturedHeaders = init?.headers instanceof Headers ? init.headers : new Headers(init?.headers);
capturedBody =
typeof init?.body === "string" ? (JSON.parse(init.body) as Record<string, unknown>) : undefined;
return new Response(createCompletedCodexSse("Hello"), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
}) as unknown as typeof fetch;
await streamOpenAICodexResponses(model, createCodexTestContext(), {
apiKey: token,
sessionId,
promptCacheKey,
}).result();
expect(capturedHeaders?.get("conversation_id")).toBe(sessionId);
expect(capturedHeaders?.get("session_id")).toBe(sessionId);
expect(capturedHeaders?.get("x-client-request-id")).toBe(sessionId);
expect(capturedBody?.prompt_cache_key).toBe(promptCacheKey);
});
it("rejects gpt-5.3-codex minimal reasoning effort instead of clamping", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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 body = typeof init?.body === "string" ? (JSON.parse(init.body) as Record<string, unknown>) : null;
expect(body?.reasoning).toEqual({ effort: "low", summary: "auto" });
return new Response(stream, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
const model = enrichModelThinking({
id: "gpt-5.3-codex",
name: "GPT-5.3 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,
reasoning: "minimal",
});
const response = await streamResult.result();
expect(response.stopReason).toBe("error");
expect(response.errorMessage).toContain("Supported efforts: low, medium, high, xhigh");
});
it("does not set conversation_id/session_id headers when sessionId is not provided", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
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 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("conversation_id")).toBe(false);
expect(headers?.has("session_id")).toBe(false);
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 });
await streamResult.result();
});
it("falls back to SSE when websocket connect fails", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const sse = `${[
`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 fetchMock = vi.fn(async (input: string | URL) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://chatgpt.com/backend-api/codex/responses") {
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
class FailingWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
setTimeout(() => {
expect(this.options?.headers?.["OpenAI-Beta"] ?? this.options?.headers?.["openai-beta"]).toStartWith(
"responses_websockets=",
);
this.emit("error", new Event("error"));
this.emit("close", new Event("close"));
this.readyState = MockWebSocket.CLOSED;
}, 0);
}
}
global.WebSocket = FailingWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const streamResult = streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-session",
providerSessionState,
});
const result = await streamResult.result();
expect(result.role).toBe("assistant");
expect(fetchMock).toHaveBeenCalled();
const fallbackDetails = getOpenAICodexTransportDetails(model, { sessionId: "ws-session", providerSessionState });
expect(fallbackDetails.lastTransport).toBe("sse");
expect(fallbackDetails.websocketDisabled).toBe(true);
expect(fallbackDetails.fallbackCount).toBe(1);
});
it("immediately falls back to SSE on fatal websocket connection errors", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_sse", 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 SSE" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_sse", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Hello SSE" }] } })}`,
`data: ${JSON.stringify({ type: "response.done", response: { id: "resp_sse", status: "completed", usage: { input_tokens: 5, output_tokens: 3, total_tokens: 8, input_tokens_details: { cached_tokens: 0 } } } })}`,
].join("\n\n")}\n\n`;
const fetchMock = vi.fn(async () => {
return new Response(sse, { headers: { "content-type": "text/event-stream" } });
});
global.fetch = fetchMock as unknown as typeof fetch;
let constructorCount = 0;
class FailingConnectWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
setTimeout(() => {
this.emit("error", new Event("error"));
this.emit("close", new Event("close"));
this.readyState = MockWebSocket.CLOSED;
}, 0);
}
}
global.WebSocket = FailingConnectWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const result = await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-fatal-fallback-session",
providerSessionState,
}).result();
expect(result.role).toBe("assistant");
expect(constructorCount).toBe(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
const transportDetails = getOpenAICodexTransportDetails(model, {
sessionId: "ws-fatal-fallback-session",
providerSessionState,
});
expect(transportDetails.lastTransport).toBe("sse");
expect(transportDetails.websocketDisabled).toBe(true);
expect(transportDetails.fallbackCount).toBe(1);
});
it("captures websocket handshake metadata and replays it on later SSE requests", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_sse", 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 SSE" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_sse", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Hello SSE" }] } })}`,
`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 fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
const headers = init?.headers instanceof Headers ? init.headers : new Headers(init?.headers);
expect(headers.get("x-codex-turn-state")).toBe("ws-turn-state-1");
expect(headers.get("x-models-etag")).toBe("models-etag-1");
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
});
global.fetch = fetchMock as unknown as typeof fetch;
class HandshakeWebSocket extends MockWebSocket {
handshakeHeaders = {
"x-codex-turn-state": "ws-turn-state-1",
"x-models-etag": "models-etag-1",
"x-reasoning-included": "true",
};
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
this.emitCodexResponse({ messageId: "msg_ws", responseId: "resp_ws", text: "Hello WS" });
}
}
global.WebSocket = HandshakeWebSocket as unknown as typeof WebSocket;
const websocketModel: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const sseModel: Model<"openai-codex-responses"> = {
...websocketModel,
preferWebsockets: false,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
await streamOpenAICodexResponses(websocketModel, context, {
apiKey: token,
sessionId: "ws-handshake-session",
providerSessionState,
}).result();
await streamOpenAICodexResponses(sseModel, context, {
apiKey: token,
sessionId: "ws-handshake-session",
providerSessionState,
}).result();
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("includes service_tier in websocket payloads when requested", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sentRequests: Array<Record<string, unknown>> = [];
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
class ServiceTierWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(data: string): void {
sentRequests.push(JSON.parse(data) as Record<string, unknown>);
this.sendJson({
type: "response.output_item.added",
item: { type: "message", id: "msg_ws", role: "assistant", status: "in_progress", content: [] },
});
this.sendJson({ type: "response.content_part.added", part: { type: "output_text", text: "" } });
this.sendJson({ type: "response.output_text.delta", delta: "Hello WS" });
this.sendJson({
type: "response.output_item.done",
item: {
type: "message",
id: "msg_ws",
role: "assistant",
status: "completed",
content: [{ type: "output_text", text: "Hello WS" }],
},
});
this.sendJson({ type: "response.created", response: { id: "resp_ws" } });
this.sendJson({
type: "response.done",
response: { id: "resp_ws", status: "completed", usage: DEFAULT_USAGE },
});
}
}
global.WebSocket = ServiceTierWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
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: "priority",
sessionId: "ws-service-tier-session",
providerSessionState: new Map<string, ProviderSessionState>(),
}).result();
expect(result.stopReason).toBe("stop");
expect(fetchMock).not.toHaveBeenCalled();
expect(sentRequests[0]?.type).toBe("response.create");
expect(sentRequests[0]?.service_tier).toBe("priority");
expect(result.usage.premiumRequests).toBeUndefined();
});
it("sends websocket continuation deltas after prior assistant response items and records stats", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sentRequests: Array<Record<string, unknown>> = [];
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
class DeltaWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(data: string): void {
sentRequests.push(JSON.parse(data) as Record<string, unknown>);
const responseIndex = sentRequests.length;
this.emitCodexResponse({
messageId: `msg_${responseIndex}`,
responseId: `resp_${responseIndex}`,
text: responseIndex === 1 ? "First answer" : "Second answer",
terminalType: "response.completed",
includeCreated: true,
});
}
}
global.WebSocket = DeltaWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const providerSessionState = new Map<string, ProviderSessionState>();
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant.", "Use concise answers."],
messages: [{ role: "user", content: "First question", timestamp: Date.now() }],
};
const firstResponse = await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-delta-session",
providerSessionState,
}).result();
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant.", "Use concise answers."],
messages: [
...firstContext.messages,
firstResponse,
{ role: "user", content: "Second question", timestamp: Date.now() },
],
};
await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-delta-session",
providerSessionState,
}).result();
expect(fetchMock).not.toHaveBeenCalled();
expect(sentRequests).toHaveLength(2);
expect(sentRequests[0]?.previous_response_id).toBeUndefined();
expect(sentRequests[0]?.prompt_cache_key).toBe("ws-delta-session");
expect(sentRequests[0]?.instructions).toBe("You are a helpful assistant.");
const initialInput = sentRequests[0]?.input;
expect(Array.isArray(initialInput)).toBe(true);
const initialItems = initialInput as Array<{ role?: string; content?: unknown }>;
expect(initialItems).toHaveLength(2);
expect(initialItems[0]?.role).toBe("developer");
expect(JSON.stringify(initialItems[0]?.content)).toContain("Use concise answers.");
expect(initialItems[1]?.role).toBe("user");
expect(sentRequests[1]?.type).toBe("response.create");
expect(sentRequests[1]?.previous_response_id).toBe("resp_1");
expect(sentRequests[1]?.prompt_cache_key).toBe("ws-delta-session");
expect(sentRequests[1]?.instructions).toBe("You are a helpful assistant.");
const deltaInput = sentRequests[1]?.input;
expect(Array.isArray(deltaInput)).toBe(true);
const deltaItems = deltaInput as Array<{ role?: string }>;
expect(deltaItems).toHaveLength(1);
expect(deltaItems[0]?.role).toBe("user");
expect(JSON.stringify(deltaItems)).toContain("Second question");
expect(JSON.stringify(deltaItems)).not.toContain("First answer");
const stats = getOpenAICodexWebSocketDebugStats(model, {
sessionId: "ws-delta-session",
providerSessionState,
});
expect(stats).toEqual({
fullContextRequests: 1,
deltaRequests: 1,
lastInputItems: 1,
lastDeltaInputItems: 1,
lastPreviousResponseId: "resp_1",
});
});
it("retries websocket continuations with full context when previous_response_id expires", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const sentRequests: Array<Record<string, unknown>> = [];
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
class PreviousResponseMissingWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(data: string): void {
const request = JSON.parse(data) as Record<string, unknown>;
sentRequests.push(request);
const requestIndex = sentRequests.length;
if (requestIndex === 1) {
this.emitCodexResponse({
messageId: "msg_1",
responseId: "resp_1",
text: "First answer",
terminalType: "response.completed",
includeCreated: true,
});
return;
}
if (requestIndex === 2) {
expect(request.previous_response_id).toBe("resp_1");
this.sendJson({
type: "error",
code: "previous_response_not_found",
message: "Previous response with id 'resp_1' not found.",
});
return;
}
if (requestIndex === 3) {
expect(request.previous_response_id).toBeUndefined();
this.emitCodexResponse({
messageId: "msg_3",
responseId: "resp_3",
text: "Second answer",
terminalType: "response.completed",
includeCreated: true,
});
return;
}
throw new Error(`Unexpected websocket request index: ${requestIndex}`);
}
}
global.WebSocket = PreviousResponseMissingWebSocket as unknown as typeof WebSocket;
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const providerSessionState = new Map<string, ProviderSessionState>();
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "First question", timestamp: Date.now() }],
};
const firstResponse = await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-expired-previous-response-session",
providerSessionState,
}).result();
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
...firstContext.messages,
firstResponse,
{ role: "user", content: "Second question", timestamp: Date.now() + 1 },
],
};
const secondResponse = await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-expired-previous-response-session",
providerSessionState,
}).result();
expect(secondResponse.stopReason).toBe("stop");
expect(JSON.stringify(secondResponse.content)).toContain("Second answer");
expect(fetchMock).not.toHaveBeenCalled();
expect(sentRequests).toHaveLength(3);
expect(sentRequests[2]?.prompt_cache_key).toBe("ws-expired-previous-response-session");
const retryInput = sentRequests[2]?.input;
expect(Array.isArray(retryInput)).toBe(true);
expect(JSON.stringify(retryInput)).toContain("First question");
expect(JSON.stringify(retryInput)).toContain("Second question");
const stats = getOpenAICodexWebSocketDebugStats(model, {
sessionId: "ws-expired-previous-response-session",
providerSessionState,
});
expect(stats).toEqual({
fullContextRequests: 2,
deltaRequests: 1,
lastInputItems: (retryInput as unknown[]).length,
lastDeltaInputItems: undefined,
lastPreviousResponseId: undefined,
});
});
it("uses low Codex text verbosity by default while preserving explicit overrides", async () => {
const tempDir = TempDir.createSync("@pi-codex-verbosity-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const capturedBodies: Array<Record<string, unknown>> = [];
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_verbosity", 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_verbosity", 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 fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
capturedBodies.push(JSON.parse(String(init?.body)) as Record<string, unknown>);
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 }).result();
await streamOpenAICodexResponses(model, context, { apiKey: token, textVerbosity: "high" }).result();
expect((capturedBodies[0]?.text as { verbosity?: string } | undefined)?.verbosity).toBe("low");
expect((capturedBodies[1]?.text as { verbosity?: string } | undefined)?.verbosity).toBe("high");
});
it("uses websocket v2 beta header when v2 mode is enabled", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_V2 = "1";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
class WebSocketV2HeaderProbe extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
expect(options?.headers?.["OpenAI-Beta"] ?? options?.headers?.["openai-beta"]).toBe(
"responses_websockets=2026-02-06",
);
this.scheduleOpen();
}
send(): void {
this.emitCodexResponse({ messageId: "msg_v2", responseId: "resp_v2", text: "Hello v2" });
}
}
global.WebSocket = WebSocketV2HeaderProbe as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-v2-session",
providerSessionState,
}).result();
expect(fetchMock).not.toHaveBeenCalled();
});
it("waits for caller abort when a prewarmed websocket is silent before its first event", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
return new Response(createCompletedCodexSse("unexpected fallback"), {
headers: { "content-type": "text/event-stream" },
});
});
global.fetch = fetchMock as unknown as typeof fetch;
let sendCount = 0;
class IdleWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
sendCount += 1;
}
}
global.WebSocket = IdleWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
await prewarmOpenAICodexResponses(model, {
apiKey: token,
sessionId: "ws-idle-timeout-session",
providerSessionState,
});
const controller = new AbortController();
setTimeout(() => controller.abort(), 30);
const result = await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-idle-timeout-session",
providerSessionState,
signal: controller.signal,
}).result();
expect(sendCount).toBeGreaterThanOrEqual(1);
expect(result.stopReason).toBe("aborted");
expect(fetchMock).not.toHaveBeenCalled();
});
it("surfaces a websocket idle-timeout error when status events never make semantic progress", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const token = createCodexTestToken();
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not run once the websocket stream becomes replay-unsafe");
});
global.fetch = fetchMock as unknown as typeof fetch;
let sendCount = 0;
let interval: NodeJS.Timeout | undefined;
class NoProgressWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
sendCount += 1;
this.sendJson({
type: "response.output_item.added",
item: {
type: "function_call",
id: "fc_ws_stalled",
call_id: "call_ws_stalled",
name: "todo_write",
arguments: "",
},
});
this.sendJson({
type: "response.output_item.done",
item: {
type: "function_call",
id: "fc_ws_stalled",
call_id: "call_ws_stalled",
name: "todo_write",
arguments: "{}",
},
});
interval = setInterval(() => {
this.sendJson({
type: "response.in_progress",
response: { id: "resp_ws_stalled", status: "in_progress" },
});
}, 2);
}
close(): void {
if (interval) clearInterval(interval);
super.close();
}
}
global.WebSocket = NoProgressWebSocket as unknown as typeof WebSocket;
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const providerSessionState = new Map<string, ProviderSessionState>();
const result = await streamOpenAICodexResponses(model, createCodexTestContext(), {
apiKey: token,
sessionId: "ws-no-progress-session",
providerSessionState,
streamIdleTimeoutMs: 5,
}).result();
expect(sendCount).toBe(1);
expect(result.stopReason).toBe("error");
expect(result.errorMessage).toContain("idle timeout waiting for websocket");
expect(result.content).toEqual([
expect.objectContaining({
type: "toolCall",
id: "call_ws_stalled|fc_ws_stalled",
name: "todo_write",
arguments: {},
}),
]);
expect(fetchMock).not.toHaveBeenCalled();
});
it("retries websocket stream closes before surfacing transport errors", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "1";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called when websocket retry succeeds");
});
global.fetch = fetchMock as unknown as typeof fetch;
let constructorCount = 0;
const requestTypes: string[] = [];
class FlakyCloseWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
this.scheduleOpen();
}
send(data: string): void {
const request = JSON.parse(data) as { type?: string };
requestTypes.push(typeof request.type === "string" ? request.type : "");
if (requestTypes.length === 1) {
this.readyState = MockWebSocket.CLOSED;
this.emit("close", { code: 1012 } as unknown as Event);
return;
}
this.emitCodexResponse({
messageId: "msg_retry_close",
responseId: "resp_retry_close",
text: "Hello retry close",
});
}
}
global.WebSocket = FlakyCloseWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const result = await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-retry-close-session",
providerSessionState,
}).result();
expect(result.role).toBe("assistant");
expect(constructorCount).toBe(2);
expect(requestTypes).toEqual(["response.create", "response.create"]);
expect(fetchMock).not.toHaveBeenCalled();
});
it("falls back to SSE when websocket becomes unavailable before stream start", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sse = `${[
`data: ${JSON.stringify({ type: "response.content_part.added", part: { type: "output_text", text: "" } })}`,
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: "Hello fallback" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_ws_unavailable", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Hello fallback" }] } })}`,
`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 fetchMock = vi.fn(async (input: string | URL) => {
const url = typeof input === "string" ? input : input.toString();
if (url === "https://chatgpt.com/backend-api/codex/responses") {
return new Response(sse, {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
return new Response("not found", { status: 404 });
});
global.fetch = fetchMock as unknown as typeof fetch;
class UnavailableBeforeStreamWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
setTimeout(() => {
this.readyState = MockWebSocket.OPEN;
this.emit("open", new Event("open"));
this.readyState = MockWebSocket.CLOSED;
this.emit("close", { code: 1006 } as unknown as Event);
}, 0);
}
}
global.WebSocket = UnavailableBeforeStreamWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const context: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const result = await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "ws-unavailable-session",
providerSessionState,
}).result();
expect(result.role).toBe("assistant");
expect(result.stopReason).not.toBe("error");
expect(result.errorMessage).toBeUndefined();
expect(fetchMock).toHaveBeenCalledTimes(1);
const transportDetails = getOpenAICodexTransportDetails(model, {
sessionId: "ws-unavailable-session",
providerSessionState,
});
expect(transportDetails.lastTransport).toBe("sse");
expect(transportDetails.websocketDisabled).toBe(true);
expect(transportDetails.fallbackCount).toBe(1);
});
it("resets websocket append state after an aborted request closes the connection", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
const sentTypesByConnection: string[][] = [];
let constructorCount = 0;
let abortSecondRequest: (() => void) | undefined;
class AbortResetWebSocket extends MockWebSocket {
#connectionIndex: number;
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.#connectionIndex = constructorCount;
constructorCount += 1;
sentTypesByConnection[this.#connectionIndex] = [];
this.scheduleOpen();
}
send(data: string): void {
const request = JSON.parse(data) as { type?: string };
const requestType = typeof request.type === "string" ? request.type : "";
sentTypesByConnection[this.#connectionIndex]?.push(requestType);
const requestIndex = sentTypesByConnection[this.#connectionIndex]?.length ?? 0;
if (this.#connectionIndex === 0 && requestIndex === 1) {
this.emitCodexResponse({ messageId: "msg_1", responseId: "resp_1", text: "Hello one" });
return;
}
if (this.#connectionIndex === 0 && requestIndex === 2) {
this.sendJson({
type: "response.output_item.added",
item: { type: "message", id: "msg_2", role: "assistant", status: "in_progress", content: [] },
});
this.sendJson({ type: "response.content_part.added", part: { type: "output_text", text: "" } });
this.sendJson({ type: "response.output_text.delta", delta: "Still streaming" });
setTimeout(() => {
abortSecondRequest?.();
}, 0);
return;
}
if (this.#connectionIndex === 1 && requestIndex === 1) {
expect(requestType).toBe("response.create");
this.emitCodexResponse({ messageId: "msg_3", responseId: "resp_3", text: "Hello three" });
return;
}
throw new Error(`Unexpected websocket send sequence: ${this.#connectionIndex}:${requestIndex}`);
}
}
global.WebSocket = AbortResetWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "Say hello", timestamp: Date.now() },
{ role: "user", content: "Keep going", timestamp: Date.now() + 1 },
],
};
const thirdContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "Say hello", timestamp: Date.now() },
{ role: "user", content: "Keep going", timestamp: Date.now() + 1 },
{ role: "user", content: "Finish", timestamp: Date.now() + 2 },
],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const firstResult = await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-abort-reset-session",
providerSessionState,
}).result();
expect(firstResult.role).toBe("assistant");
const secondAbortController = new AbortController();
abortSecondRequest = () => {
secondAbortController.abort();
};
const secondResult = await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-abort-reset-session",
signal: secondAbortController.signal,
providerSessionState,
}).result();
expect(secondResult.stopReason).toBe("aborted");
const thirdResult = await streamOpenAICodexResponses(model, thirdContext, {
apiKey: token,
sessionId: "ws-abort-reset-session",
providerSessionState,
}).result();
expect(thirdResult.role).toBe("assistant");
expect(constructorCount).toBe(2);
expect(sentTypesByConnection[0]).toEqual(["response.create", "response.create"]);
expect(sentTypesByConnection[1]).toEqual(["response.create"]);
expect(fetchMock).not.toHaveBeenCalled();
});
it("resets websocket append state after websocket error events", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
const sentTypes: string[] = [];
let constructorCount = 0;
class ErrorResetWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
this.scheduleOpen();
}
send(data: string): void {
const request = JSON.parse(data) as { type?: string };
const requestType = typeof request.type === "string" ? request.type : "";
sentTypes.push(requestType);
const requestIndex = sentTypes.length;
if (requestIndex === 1) {
this.emitCodexResponse({ messageId: "msg_1", responseId: "resp_1", text: "Hello one" });
return;
}
if (requestIndex === 2) {
this.sendJson({
type: "error",
code: "invalid_request_error",
message: "simulated request error",
});
return;
}
if (requestIndex === 3) {
expect(requestType).toBe("response.create");
this.emitCodexResponse({ messageId: "msg_3", responseId: "resp_3", text: "Hello three" });
return;
}
throw new Error(`Unexpected websocket request index: ${requestIndex}`);
}
}
global.WebSocket = ErrorResetWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "Say hello", timestamp: Date.now() },
{ role: "user", content: "Keep going", timestamp: Date.now() + 1 },
],
};
const thirdContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "Say hello", timestamp: Date.now() },
{ role: "user", content: "Keep going", timestamp: Date.now() + 1 },
{ role: "user", content: "Finish", timestamp: Date.now() + 2 },
],
};
const providerSessionState = new Map<string, ProviderSessionState>();
const firstResult = await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-error-reset-session",
providerSessionState,
}).result();
expect(firstResult.role).toBe("assistant");
const secondResult = await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-error-reset-session",
providerSessionState,
}).result();
expect(secondResult.stopReason).toBe("error");
expect(secondResult.errorMessage).toContain("simulated request error");
const thirdResult = await streamOpenAICodexResponses(model, thirdContext, {
apiKey: token,
sessionId: "ws-error-reset-session",
providerSessionState,
}).result();
expect(thirdResult.role).toBe("assistant");
expect(constructorCount).toBe(1);
expect(sentTypes).toEqual(["response.create", "response.create", "response.create"]);
expect(fetchMock).not.toHaveBeenCalled();
});
it("falls back to SSE when websocket receives malformed JSON before completion", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_sse", 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: "Recovered over SSE" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_sse", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Recovered over SSE" }] } })}`,
`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 fetchMock = vi.fn(
async () => new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
);
global.fetch = fetchMock as unknown as typeof fetch;
class MalformedMessageWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
this.sendMessage("{");
}
}
global.WebSocket = MalformedMessageWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const result = await streamOpenAICodexResponses(
model,
{
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
},
{
apiKey: token,
sessionId: "ws-malformed-json-session",
providerSessionState: new Map<string, ProviderSessionState>(),
},
).result();
expect(result.stopReason).toBe("stop");
expect(result.content.find(c => c.type === "text")?.text).toBe("Recovered over SSE");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("replays over SSE when websocket closes after buffered output without a terminal event", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS = "1";
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_sse_replay", 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: "Replay succeeded" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_sse_replay", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Replay succeeded" }] } })}`,
`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 fetchMock = vi.fn(
async () => new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } }),
);
global.fetch = fetchMock as unknown as typeof fetch;
class BufferedCloseWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(): void {
this.sendJson({
type: "response.output_item.added",
item: {
type: "message",
id: "msg_ws_partial",
role: "assistant",
status: "in_progress",
content: [],
},
});
this.sendJson({ type: "response.content_part.added", part: { type: "output_text", text: "" } });
this.sendJson({ type: "response.output_text.delta", delta: "Partial output" });
this.readyState = MockWebSocket.CLOSED;
this.emit("close", { code: 1006 } as unknown as Event);
}
}
global.WebSocket = BufferedCloseWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const result = await streamOpenAICodexResponses(
model,
{
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
},
{
apiKey: token,
sessionId: "ws-buffered-close-session",
providerSessionState: new Map<string, ProviderSessionState>(),
},
).result();
expect(result.stopReason).toBe("stop");
expect(result.content.find(c => c.type === "text")?.text).toBe("Replay succeeded");
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it("resets append state and stale turn headers when websocket requests diverge", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const sseTurnStates: Array<string | null> = [];
const sseModelsEtags: Array<string | null> = [];
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: "msg_sse", 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 SSE" })}`,
`data: ${JSON.stringify({ type: "response.output_item.done", item: { type: "message", id: "msg_sse", role: "assistant", status: "completed", content: [{ type: "output_text", text: "Hello SSE" }] } })}`,
`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 fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
const headers = init?.headers instanceof Headers ? init.headers : new Headers(init?.headers);
sseTurnStates.push(headers.get("x-codex-turn-state"));
sseModelsEtags.push(headers.get("x-models-etag"));
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
});
global.fetch = fetchMock as unknown as typeof fetch;
const requestTypes: string[] = [];
class DivergedAppendWebSocket extends MockWebSocket {
handshakeHeaders = {
"x-codex-turn-state": "ws-turn-state-1",
"x-models-etag": "ws-models-etag-1",
};
#sendCount = 0;
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
this.scheduleOpen();
}
send(data: string): void {
this.#sendCount += 1;
const request = JSON.parse(data) as { type?: string };
requestTypes.push(typeof request.type === "string" ? request.type : "");
const idSuffix = String(this.#sendCount);
this.emitCodexResponse({
messageId: `msg_${idSuffix}`,
responseId: `resp_${idSuffix}`,
text: `Hello WS ${idSuffix}`,
});
}
}
global.WebSocket = DivergedAppendWebSocket as unknown as typeof WebSocket;
const websocketModel: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const sseModel: Model<"openai-codex-responses"> = {
...websocketModel,
preferWebsockets: false,
};
const firstContext: Context = {
systemPrompt: ["Prompt A"],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["Prompt B"],
messages: [{ role: "user", content: "Say hello", timestamp: Date.now() }],
};
const providerSessionState = new Map<string, ProviderSessionState>();
await streamOpenAICodexResponses(websocketModel, firstContext, {
apiKey: token,
sessionId: "ws-diverged-session",
providerSessionState,
}).result();
await streamOpenAICodexResponses(websocketModel, secondContext, {
apiKey: token,
sessionId: "ws-diverged-session",
providerSessionState,
}).result();
await streamOpenAICodexResponses(sseModel, secondContext, {
apiKey: token,
sessionId: "ws-diverged-session",
providerSessionState,
}).result();
expect(requestTypes).toEqual(["response.create", "response.create"]);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(sseTurnStates[0]).toBeNull();
expect(sseModelsEtags[0]).toBeNull();
});
it("reuses a prewarmed websocket connection across turns", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
let constructorCount = 0;
let sendCount = 0;
class ReusableWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
this.scheduleOpen();
}
send(data: string): void {
sendCount += 1;
const request = JSON.parse(data) as Record<string, unknown>;
expect(typeof request.type).toBe("string");
this.emitCodexResponse({
messageId: `msg_${sendCount}`,
responseId: `resp_${sendCount}`,
text: `Hello ${sendCount}`,
});
}
}
global.WebSocket = ReusableWebSocket as unknown as typeof WebSocket;
const model: Model<"openai-codex-responses"> = {
id: "gpt-5.3-codex-spark",
name: "GPT-5.3 Codex Spark",
api: "openai-codex-responses",
provider: "openai-codex",
baseUrl: "https://chatgpt.com/backend-api",
reasoning: true,
preferWebsockets: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 128000,
};
const providerSessionState = new Map<string, ProviderSessionState>();
await prewarmOpenAICodexResponses(model, { apiKey: token, sessionId: "ws-reuse-session", providerSessionState });
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "First", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "First", timestamp: Date.now() },
{ role: "user", content: "Second", timestamp: Date.now() },
],
};
await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-reuse-session",
providerSessionState,
}).result();
await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-reuse-session",
providerSessionState,
}).result();
expect(constructorCount).toBe(1);
expect(sendCount).toBe(2);
expect(fetchMock).not.toHaveBeenCalled();
const transportDetails = getOpenAICodexTransportDetails(model, {
sessionId: "ws-reuse-session",
providerSessionState,
});
expect(transportDetails.lastTransport).toBe("websocket");
expect(transportDetails.websocketConnected).toBe(true);
expect(transportDetails.prewarmed).toBe(true);
expect(transportDetails.canAppend).toBe(true);
});
it("replays x-codex-turn-state on subsequent SSE requests", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const requestTurnStates: Array<string | null> = [];
let callCount = 0;
const fetchMock = vi.fn(async (_input: string | URL, init?: RequestInit) => {
const headers = init?.headers instanceof Headers ? init.headers : new Headers(init?.headers);
requestTurnStates.push(headers.get("x-codex-turn-state"));
const sse = `${[
`data: ${JSON.stringify({ type: "response.output_item.added", item: { type: "message", id: `msg_${callCount}`, 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_${callCount}`, 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 responseHeaders = new Headers({ "content-type": "text/event-stream" });
if (callCount === 0) {
responseHeaders.set("x-codex-turn-state", "turn-state-1");
}
callCount += 1;
return new Response(sse, { status: 200, headers: responseHeaders });
});
global.fetch = fetchMock as unknown as typeof fetch;
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 providerSessionState = new Map<string, ProviderSessionState>();
await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "turn-state-session",
providerSessionState,
}).result();
await streamOpenAICodexResponses(model, context, {
apiKey: token,
sessionId: "turn-state-session",
providerSessionState,
}).result();
expect(requestTurnStates[0]).toBeNull();
expect(requestTurnStates[1]).toBe("turn-state-1");
});
it("forces a fresh websocket when the prior connection has been idle past PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
// Tight reuse window so the test doesn't have to sleep for seconds. Disable
// the heartbeat so it doesn't independently kill the idle socket and mask
// the reuse-gate behaviour we're trying to verify.
Bun.env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = "10";
Bun.env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
let constructorCount = 0;
let sendCount = 0;
class IdleReuseWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
this.scheduleOpen();
}
send(_data: string): void {
sendCount += 1;
this.emitCodexResponse({
messageId: `msg_${sendCount}`,
responseId: `resp_${sendCount}`,
text: `Hello ${sendCount}`,
});
}
}
global.WebSocket = IdleReuseWebSocket as unknown as typeof WebSocket;
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const providerSessionState = new Map<string, ProviderSessionState>();
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "First", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "First", timestamp: Date.now() },
{ role: "user", content: "Second", timestamp: Date.now() },
],
};
await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-idle-reuse-session",
providerSessionState,
}).result();
// Simulate the gap between a tool result and the continuation request: the
// socket sat quiet long enough that we shouldn't trust it without a fresh
// handshake. 30 ms > MAX_IDLE_REUSE_MS (10).
await new Promise(resolve => setTimeout(resolve, 30));
const second = await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-idle-reuse-session",
providerSessionState,
}).result();
expect(second.stopReason).toBe("stop");
expect(constructorCount).toBe(2);
expect(sendCount).toBe(2);
expect(fetchMock).not.toHaveBeenCalled();
});
it("drops stale frames from a prior response before sending the next websocket request", async () => {
const tempDir = TempDir.createSync("@pi-codex-stream-");
setAgentDir(tempDir.path());
// Generous reuse window so the connection is happily reused across turns —
// the queue-drain behaviour is the only variable here.
Bun.env.PI_CODEX_WEBSOCKET_MAX_IDLE_REUSE_MS = "60000";
Bun.env.PI_CODEX_WEBSOCKET_PING_INTERVAL_MS = "0";
Bun.env.PI_CODEX_WEBSOCKET_RETRY_BUDGET = "0";
const payload = Buffer.from(
JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "acc_test" } }),
"utf8",
).toBase64();
const token = `aaa.${payload}.bbb`;
const fetchMock = vi.fn(async () => {
throw new Error("SSE fallback should not be called");
});
global.fetch = fetchMock as unknown as typeof fetch;
let constructorCount = 0;
let sendCount = 0;
class LateFrameWebSocket extends MockWebSocket {
constructor(url: string, options?: { headers?: WsHeaders }) {
super(url, options);
constructorCount += 1;
this.scheduleOpen();
}
send(_data: string): void {
sendCount += 1;
if (sendCount === 1) {
this.emitCodexResponse({
messageId: "msg_1",
responseId: "resp_1",
text: "First",
});
// Stale frame that lands AFTER the consumer breaks on
// response.completed. Without the queue-drain at the top of
// streamRequest, this becomes the first frame of the next
// request: a stale terminal event would resolve the new turn
// with empty content, never reaching the model's real response.
this.sendJson({
type: "response.completed",
response: { id: "resp_stale", status: "completed", usage: DEFAULT_USAGE },
});
return;
}
this.emitCodexResponse({
messageId: "msg_2",
responseId: "resp_2",
text: "Second",
});
}
}
global.WebSocket = LateFrameWebSocket as unknown as typeof WebSocket;
const model = createCodexTestModel("https://chatgpt.com/backend-api");
const providerSessionState = new Map<string, ProviderSessionState>();
const firstContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [{ role: "user", content: "First", timestamp: Date.now() }],
};
const secondContext: Context = {
systemPrompt: ["You are a helpful assistant."],
messages: [
{ role: "user", content: "First", timestamp: Date.now() },
{ role: "user", content: "Second", timestamp: Date.now() },
],
};
const first = await streamOpenAICodexResponses(model, firstContext, {
apiKey: token,
sessionId: "ws-stale-frame-session",
providerSessionState,
}).result();
expect(first.stopReason).toBe("stop");
const second = await streamOpenAICodexResponses(model, secondContext, {
apiKey: token,
sessionId: "ws-stale-frame-session",
providerSessionState,
}).result();
expect(second.stopReason).toBe("stop");
expect(constructorCount).toBe(1);
expect(sendCount).toBe(2);
// Second turn must reflect the second response, not the stale terminal frame
// from the first turn's tail.
expect(second.responseId).toBe("resp_2");
const text = second.content
.filter((c): c is Extract<typeof c, { type: "text" }> => c.type === "text")
.map(c => c.text)
.join("");
expect(text).toBe("Second");
});
});