第 43 讲:Responses WebSocket——模型流式输出的稳定通道

Codex 源码解析 · 第 43 讲

Codex 源码解析系列 第 43 讲:Responses WebSocket——模型流式输出的稳定通道 基于 OpenAI Codex 源码 · 2026-11-06 💡 本讲一句话: SSE 只能服务器单向推流,而"打断模型正在说的话"必须往回发一帧——这就是 Codex 为 Responses API 单独造一条 WebSocket 通道的原因。本讲逐行拆解这条长连接:单泵任务独占 socket、跨 turn 复用同一根管子(Mutex+Option 取出/放回)、中断帧与 ack 握手、JSON 文本帧里包着的可重试错误,以及本地代理 responses-api-proxy。 一、为什么需要 WebSocket?SSE 差在哪 第 12 讲我们拆过 SSE(Server-Sent Events)流式通信:服务器把 response.output_text.delta 一类的事件顺着 HTTP chunked 连接推下来,客户端只管读。这个模型有个天然短板—— 它是单向的 。用户中途想打断(steering),SSE 里没有任何"往回说一句话"的通道,只能掐断整条连接重开。 WebSocket 把这条管子变成双向:同一条 TCP 连接上,客户端可以随时发帧。Codex 在 codex-api/src/endpoint/responses_websocket.rs (1456 行)里实现了完整的 Responses WebSocket 客户端,核心能力就三件: 跨 turn 复用连接、流中中断并等 ack、把服务器包在文本帧里的错误映射成可重试错误 。先看两种传输的对照: 维度 SSE(HTTP 流式) Responses WebSocket 方向 服务器 → 客户端单向 双向,可随时回发帧 流中中断 只能掐断重连 response.interrupt 帧 + ack 连接复用 每 turn 重新握手 同一 socket 跨 turn 复用 压缩 依赖 HTTP 层 permessage-deflate 逐帧压缩 注意最后一行:WebSocket 的 deflate 是 逐消息 的(permessage-deflate),对"一条连接上跑几百个 JSON 事件帧"的场景比整条 HTTP 响应的压缩更细粒度。这个配置就藏在 websocket_config() 里,后面会看到。 二、WsStream:一个泵任务独占 socket 整个文件的地基是 WsStream (第 58-169 行)。它把一条原始 WebSocket 连接包装成"命令进、消息出"两个通道,并 spawn 一个 泵任务(pump task)独占 socket 。为什么不让调用方直接读写 socket?因为 tokio-tungstenite 的 WebSocketConnection 是单所有者类型——同一时刻只能有一个任务碰它。泵任务就是这个唯一的所有者,所有发送都变成"给泵发命令": 📄 codex-rs/codex-api/src/endpoint/responses_websocket.rs (第 58-70 行) struct WsStream { // 把一条 WebSocket 连接包装成"命令进、消息出"两个通道 routing_affinity: Option<codex_http_client::ChatGptRoutingAffinity>, // 记录服务端路由亲和,上层可据此把后续请求钉在同一后端 tx_command: mpsc::Sender<WsCommand>, // 发送命令通道(容量 32),所有写帧都走这里排队 rx_message: mpsc::UnboundedReceiver<Result<Message, WsError>>, // 接收消息通道,无界——泵转发时永不阻塞 pump_task: tokio::task::JoinHandle<()>, // 独占 socket 的泵任务句柄,Drop 时用来清理 } enum WsCommand { // 只有一种命令:发一帧并等结果 Send { message: Message, // 要发送的 WebSocket 帧(Text/Binary) tx_resul…

阅读全文:Codex 源码解析 第 43 讲