Skip to content

Add an opt-in WebSocket transport for Codex Responses - #523

Closed
mjlbach wants to merge 2 commits into
vercel-labs:mainfrom
mjlbach:perf/codex-websocket-transport
Closed

Add an opt-in WebSocket transport for Codex Responses#523
mjlbach wants to merge 2 commits into
vercel-labs:mainfrom
mjlbach:perf/codex-websocket-transport

Conversation

@mjlbach

@mjlbach mjlbach commented Aug 29, 2026

Copy link
Copy Markdown

Summary

  • add src/gateway/websocket.zig, an RFC 6455 client codec (handshake keys, masked frames, fragmentation, control-frame rules) over std.Io reader/writer streams
  • add src/gateway/openai_codex_websocket.zig: upgrade the Codex Responses endpoint to a WebSocket (OpenAI-Beta: responses_websockets=2026-02-06), send the request body as one response.create text frame, and feed each received frame into the same Responses reducer the SSE transport uses, with identical stream limits
  • cache the connection per (session, account) with a five-minute idle and 55-minute age limit, so later turns skip DNS, TCP, and TLS setup; a busy or key-mismatched entry gets a one-off connection, and a reused connection that fails before any model output is replaced by a fresh one once
  • opt in with FX_OPENAI_CODEX_TRANSPORT=websocket; the default transport is unchanged
  • a WebSocket failure before any model output falls back to SSE under the original invocation admission and latches the process to SSE so a broken proxy costs one failed handshake, not one per turn; after output has been emitted the failure is surfaced instead — a replay can never duplicate tool calls
  • request.deadline is respected end to end: it bounds the handshake and the stream phase (via the bounded cancel watcher), surfacing error.Timeout
  • masked server frames are rejected (RFC 6455 5.1: only clients mask)

The handshake is std.http.Client's existing 101/informational path, so TLS, proxies, and the cancel watcher are inherited rather than reimplemented. Protocol and policy mirror Codex (codex-rs/core/src/client.rs) and pi (openai-codex-responses.ts); pi's transport selection, fallback-before-output rule, and cache limits are followed exactly. Prewarm, previous_response_id continuation, and zstd compression on the SSE path are follow-ups.

Testing

  • zig fmt --check on touched files
  • zig build test — full suite passes; new tests cover RFC 6455 vectors and frame round trips, a loopback WebSocket server proving end-to-end streaming plus connection reuse (one TCP accept serving two requests), fallback-safe handshake rejection, and the fallback error classification
  • bun test acp.test.ts (116 tests) and tui-auth-source-selection.test.ts (56 tests) pass, including two new e2e tests that drive the built binary against a Bun WebSocket loopback server: a completion streamed over one upgraded connection with zero SSE requests, and pre-output fallback to SSE when the upgrade is rejected
  • live against chatgpt.com: single-shot and tool-using turns stream over the WebSocket; a saved session logs codex_ws_connection reused=true on the second step, removing the ~400 ms connect observed on fresh connections

🤖 Generated with Claude Code

mjlbach and others added 2 commits August 29, 2026 09:43
Upgrade the Codex Responses endpoint to a WebSocket
(OpenAI-Beta: responses_websockets=2026-02-06), send the request body as
one response.create text frame, and feed each received frame into the
same Responses reducer the SSE transport uses. Connections are cached
per (session, account) with a five-minute idle and 55-minute age limit,
so later turns skip DNS, TCP, and TLS setup; a busy or mismatched entry
gets a one-off connection, and a reused connection that fails before any
model output is replaced by a fresh one once.

Enabled with FX_OPENAI_CODEX_TRANSPORT=websocket; the default transport
is unchanged. A WebSocket transport failure before any model output
falls back to SSE under the original invocation admission and latches
the process to SSE; after output has been emitted the failure is
surfaced instead, so a replay can never duplicate tool calls.

Verified against the production Codex endpoint, including cross-turn
connection reuse in a saved session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Respect request.deadline across the WebSocket path: it bounds the
handshake alongside the connect timeout and bounds the stream phase via
the bounded cancel watcher, surfacing error.Timeout. Reject masked
server frames (RFC 6455 5.1: only clients mask). Add two e2e tests that
drive the built binary against a Bun WebSocket loopback server: a
completion streamed over one upgraded connection with zero SSE
requests, and pre-output fallback to SSE when the upgrade is rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mjlbach

mjlbach commented Aug 30, 2026

Copy link
Copy Markdown
Author

This PR has been folded into #521

@mjlbach mjlbach closed this Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant