版本说明:本文依据 2026 年 8 月 5 日读取的 OpenAI 官方 Codex 文档,以及
openai/codex官方仓库main分支源码撰写。Codex 的网络实现仍在快速演进,文中的默认超时、重试次数和内部 Header 属于版本化实现细节,发布或复现时应再次核对源码。本文范围:讨论本地 Codex 客户端——CLI、桌面端或 IDE 扩展中的模型通信路径。Codex Cloud 的任务调度、云端容器生命周期和浏览器回传不在本文范围内。
Codex 的网络层很容易被误解成“把一次提示词 POST 到 Responses API,然后读取流式结果”。这个描述只适用于最简单的一轮问答,却无法解释真实编码任务中的几个核心现象:
- 为什么一次用户任务会连续发起多次模型请求;
- 为什么同一条 WebSocket 可以承载多次
response.create; - 为什么连接能够复用,而
x-codex-turn-state又绝不能跨 Turn 复用; - 为什么有时只发送新增的 Tool Result,有时必须重新发送完整上下文;
- 为什么 WebSocket 中断后先重连,重试耗尽后却切换到 HTTPS;
- 为什么一旦发生传输降级,后续 Turn 也会继续使用 HTTPS。
这些行为并不是彼此独立的优化。它们共享同一套状态模型:Session 级客户端保存长期稳定的 Provider、认证和降级状态;Turn 级客户端保存当前任务轮次的连接、Sticky Routing Token、上一请求与上一响应;工具执行结果再作为新的 Input Item 推动下一次采样。
OpenAI 在介绍 Responses API WebSocket 模式时指出,复杂 Agent 任务会产生大量“模型决策—本地工具执行—结果回填”的往返请求,持久连接和增量请求的目的,就是降低这些高频往返的固定开销。1
版本演进说明:OpenAI 在 2026 年 1 月公开 Codex Agent Loop 时,Codex 仍以 HTTPS 请求与 SSE 响应为主,并刻意不使用
previous_response_id,以保持请求无状态并兼容 ZDR。2026 年 4 月公开的 WebSocket 方案随后改用持久连接、连接内存缓存和增量response.create。本文描述的是后者以及当前官方仓库中的实现,不应把两篇官方文章理解为同一时间点的架构。21
1. Codex 网络模块划分

先从源码中的对象生命周期入手。Codex 没有把所有网络状态塞进一个全局 HTTP Client,而是明确拆成 Session、Turn、Provider/Transport 和 Tool Runtime 四个层次。
1.1 Session 级 ModelClient
源码把 ModelClient 定义为 Session 生命周期对象。它保存的是在多个 Turn 之间应保持稳定的内容:
- 当前线程标识;
- 选中的模型 Provider;
- ChatGPT、API Key、AWS 等认证来源;
- Session 来源,例如 CLI、VS Code、子 Agent;
- HTTP Client Factory,以及由它解析出的代理、TLS 和自定义 CA 策略;
- 是否启用请求压缩、计时指标和 attestation;
disable_websockets:本 Session 是否已经永久降级到 HTTP;cached_websocket_session:可被后续 Turn 取走的 WebSocket 传输缓存。
其中最值得注意的是 disable_websockets。它不是单次请求的临时变量,而是放在由 Arc 共享的 ModelClientState 中。一个 Turn 如果确认当前环境中的 WebSocket 不可靠并激活 HTTPS 降级,后续 Turn 会看到同一个禁用状态,不再重复尝试同一条失败路径。3
这体现了一个重要判断:某次网络抖动属于 Request;某种传输在当前网络环境中不可用,则属于 Session。
1.2 Turn 级 ModelClientSession
每次用户任务轮次会新建一个 ModelClientSession。它负责在同一 Turn 内发起一次或多次 Responses 请求,并保存当前轮次需要的连续性状态:
pub struct ModelClientSession { client: ModelClient, websocket_session: WebsocketSession, turn_state: Arc<OnceLock<String>>,}WebsocketSession 进一步保存:
- 当前
ResponsesWebsocketConnection; - 上一次完整
ResponsesApiRequest; - 上一次响应结束后得到的
response_id和服务端新增 Items; - 上一次响应是否来自未写入 rollout trace 的预热;
- 当前连接是否被复用。
这里有一个容易混淆的细节:ModelClientSession 是 Turn 级对象,但其持有的物理 WebSocket 连接在对象析构时会归还给 Session 级 ModelClient 缓存,下一 Turn 可以再次取出。与此同时,turn_state 每次 new_session() 都重新创建,不会随连接一起跨 Turn 传播。
因此,Codex 同时实现了两件看似矛盾、其实层次不同的事:
- 传输连接可以跨对象复用,避免重复握手;
- Turn 语义状态严格隔离,避免把上一轮 Sticky Routing Token 带到下一轮。
1.3 Provider、认证与 Transport
Provider 层负责回答四个问题:
- 请求发往哪个 Base URL;
- 使用什么认证材料;
- 后端使用哪种 Wire API;
- 后端是否支持 WebSocket、独立搜索等能力。
当前源码只保留 responses 这一种 Wire API。旧的 chat 配置会直接报错,引导用户迁移到 Responses API。Provider 配置还可以声明:
base_url;- API Key 所在环境变量;
- 固定 Header 和从环境变量读取的 Header;
- 命令生成的 Bearer Token;
- AWS Profile 与 Region;
- Request Retry、Stream Retry、Stream Idle Timeout、WebSocket Connect Timeout;
supports_websockets。
Transport 则是具体的承载方式:
- Responses over WebSocket;
- Responses over HTTPS 流;
- Bedrock 的 Responses 兼容实现;
- Ollama、LM Studio 等本地 Responses 兼容端点。
Provider 能力决定是否有资格使用 WebSocket,Session 降级状态决定当前是否还允许使用 WebSocket,两者共同构成:
websocket_enabled= provider.supports_websockets AND NOT session.disable_websockets1.4 Tool Executor

Tool Executor 不属于模型 Transport,却决定了网络层为什么必须支持高频续接。
模型可能返回一个完整 Function Call。Codex 的 ToolCallRuntime 会把调用交给 Tool Router,再根据工具是否支持并行执行来获取不同的锁:
- 支持并行的工具获取读锁,可与其他并行工具同时执行;
- 不支持并行的工具获取写锁,独占执行区间;
- 用户取消时,Cancellation Token 会终止工具任务或等待运行时完成清理;
- 普通工具错误会转换为
FunctionCallOutput,而不是让整个 Turn 崩溃; - 工具输出作为新的
ResponseInputItem写回上下文,触发下一次模型请求。4
因此,Codex 一次 Turn 的真实循环是:
WebSocket 的价值不是“让一条回答显示得更流畅”,而是让这条循环中的多次请求共享连接,并尽可能只发送新增状态。
2. Provider 与请求路由
Codex 的上层 Agent Loop 不直接判断“这是 ChatGPT 还是 API Key”。它先通过 Provider 与 Auth Manager 解析出当前请求所需的 ApiProvider 和 AuthProvider,然后让 HTTP 与 WebSocket 路径共享这份解析结果。
2.1 ChatGPT 登录路径
Codex 的产品界面允许用户直接“Continue with ChatGPT”。在源码中,只要认证模式属于 ChatGPT、ChatGPT Auth Tokens、Headers、Agent Identity 或 Personal Access Token 一类,OpenAI Provider 的默认 Base URL 就会指向:
https://chatgpt.com/backend-api/codexChatGPT 路径的特点是:
- 认证由登录流程和 Auth Manager 管理;
- Token 可以自动刷新;
- 401 并不立即等价于永久失败,客户端会进入一次认证恢复流程;
- Session、Thread、Installation、Originator 等 Codex 产品元数据会随请求发送;
- 其配额和产品使用策略与直接 API Key 路径不同。
OpenAI 的 Codex 入门页面同时提供“Continue with ChatGPT”和“Enter API key”两种入口。5
2.2 OpenAI API Key 路径
当使用 OpenAI API Key 时,默认 Base URL 是:
https://api.openai.com/v1这一路径使用标准 OpenAI API 认证,并可附带 OpenAI-Organization、OpenAI-Project 等环境 Header。它更适合:
- 按 API 用量计费;
- 由企业统一分发 Project Key;
- 需要明确组织、项目和 API 配额边界的环境;
- 自动化或无浏览器登录的运行方式。
ChatGPT 登录与 API Key 最终都被解析成统一的 SharedAuthProvider,因此后面的 WebSocket 建连、HTTPS 请求和遥测代码不需要复制两套实现。
2.3 自定义兼容 Provider
用户可以在 ~/.codex/config.toml 中增加自定义 model_providers。一个典型配置可能包含:
model_provider = "corp-gateway"
[model_providers.corp-gateway]name = "Corporate Responses Gateway"base_url = "https://llm.example.com/v1"env_key = "CORP_LLM_TOKEN"wire_api = "responses"request_max_retries = 4stream_max_retries = 5stream_idle_timeout_ms = 300000supports_websockets = true
[model_providers.corp-gateway.http_headers]X-Client = "codex"需要注意四点:
- 兼容 Responses JSON 并不自动等于兼容 WebSocket。 Provider 必须显式声明
supports_websockets = true,服务端也必须实现 Responses WebSocket 语义。 - 自定义 Provider 可以使用环境变量 Key、固定 Bearer Token、命令生成的 Token 或无认证模式,但这些认证配置彼此有冲突检查。
- WebSocket Upgrade 同样要穿过企业代理、TLS 和自定义 CA,因此“普通 HTTPS 可用”不必然代表 WebSocket 可用。
- Provider、认证和遥测路由属于机器本地配置,不能被项目仓库中的
.codex/config.toml任意覆盖,避免恶意仓库重定向模型流量。6
2.4 Bedrock 和本地模型 Provider
Codex 官方源码内置四类主要 Provider ID:
| Provider | 默认地址/寻址 | 认证 | WebSocket |
|---|---|---|---|
| OpenAI | ChatGPT Backend 或 api.openai.com/v1 | ChatGPT / API Key | 支持 |
| Amazon Bedrock | 按 AWS Region 解析 Mantle/Responses 端点 | Bedrock API Key 或 AWS SDK 凭证链 | 当前源码不支持 |
| Ollama | http://localhost:11434/v1 | 默认无 | 不支持 |
| LM Studio | http://localhost:1234/v1 | 默认无 | 不支持 |
Amazon Bedrock 路径仍然使用 Responses 语义,但模型请求由 Bedrock 处理,OpenAI 托管 API 不在请求链路中。OpenAI 官方文档明确说明,Codex Runtime 仍在本地 CLI、桌面端或 IDE 中运行,AWS 负责身份、Region、配额、治理和推理请求。78
源码还明确禁止 AWS Provider 与 supports_websockets 同时启用,因为当前尚未为 WebSocket Upgrade 实现 AWS SigV4 签名。因此,本篇讨论的 WebSocket 预热、Sticky Routing 和 WS→HTTPS 降级主要适用于支持 Responses WebSocket 的 OpenAI 或兼容 Provider;Bedrock、Ollama 和 LM Studio 默认直接进入 HTTP Responses 路径。9
3. Responses API 请求结构

Codex 在 HTTP 和 WebSocket 两条路径上共享同一个 ResponsesApiRequest。WebSocket 不是另一套业务协议,它是在相同字段基础上增加 type: response.create、previous_response_id 和 generate。10
3.1 Instructions
instructions 是模型的基础行为约束,来自 Codex 当前模型与任务环境的 Base Instructions。它定义模型在编码任务中的角色、工具使用方式、输出约束等。
在普通 Responses 模式中,它位于请求顶层:
{ "model": "...", "instructions": "...", "input": [...]}在 responses_lite 变体中,源码可能把工具定义和基础指令改写成 Developer Role 的 Input Items,并把顶层 instructions 置空。这是同一逻辑信息的不同封装,不改变本文讨论的连接与状态模型。
3.2 Input Items
input 是一组结构化 ResponseItem,而不是单纯的字符串消息。它可以包含:
- 用户消息;
- 助手消息;
- 模型产生的 Function Call;
- 客户端执行后的 Function Call Output;
- Reasoning 或其他服务端返回 Item;
- 当前 Turn 注入的环境、技能、工具和上下文记录。
这使 Codex 可以把“工具调用—工具结果”作为可验证的结构化事件回填,而不是拼成一段自然语言。
在增量 WebSocket 请求中,input 可能不是完整历史,而只是基于 previous_response_id 的新增 Items。例如上一轮模型请求执行测试后,下一次请求只需要补充:
{ "type": "function_call_output", "call_id": "call_test_1", "output": "3 tests passed"}3.3 Tools
工具定义会被序列化成 Responses API 可识别的 Tool JSON。每个工具通常包含名称、说明和参数 Schema。Codex 会将当前 Step 真正可用的工具暴露给模型,而不是假设每一轮都有完全相同的工具集合。
工具集合是否变化非常重要,因为增量请求要求非 Input 字段与上一次请求保持一致。若本轮因为权限、MCP 或模式切换导致工具列表变化,Codex 会放弃 previous_response_id + delta,改发完整请求。
3.4 Parallel Tool Calls
请求中的 parallel_tool_calls 来自当前 Prompt 能力与模型模式。它表示模型是否可以在同一次采样中提出多个并行工具调用。
但这不意味着所有工具都真的会并行执行。最终执行仍由 ToolCallRuntime 与 Tool Router 判定:
- 工具声明支持并行:进入共享读锁;
- 工具不支持并行:获取独占写锁;
- 同一 Turn 的文件修改、Shell 和其他有副作用工具因此可以被串行化。
换言之,parallel_tool_calls 是模型输出许可,Tool Runtime 才是客户端执行许可。
3.5 Reasoning 配置
Codex 的 reasoning 对象可包含:
effort:推理强度;summary:是否以及如何返回推理摘要;context:推理上下文范围。
某些模型还支持顺序交付 Reasoning Summary、输出 verbosity 和严格 JSON Schema。源码在构建请求时会根据模型能力过滤不支持的配置,避免把客户端设置机械地发送给所有 Provider。
值得注意的是,Reasoning 配置也参与增量请求兼容性比较。若用户在同一状态链中更改推理强度或摘要策略,客户端不能再假设新请求只是上一请求的简单追加。
3.6 Client Metadata
Codex 使用 client_metadata 携带产品侧和可观测性上下文。其典型字段包括:
installation_id;session_id;thread_id;turn_id;window_id;request_kind:Turn、Prewarm、Compaction、Memory;- Parent Thread / Parent Turn / Subagent 信息;
- Sandbox 和 Workspace 快照;
- Turn 开始时间;
- W3C
traceparent/tracestate; - WebSocket 请求开始时间;
x-codex-turn-state的兼容投影。
源码把完整 Turn Metadata 视为一个规范化快照,再按需要投影为 Client Metadata 或兼容 Header,而不是让多份元数据各自成为事实来源。11
这些字段通常不影响模型语义内容,却对以下能力至关重要:
- Sticky Routing;
- 端到端 Trace;
- 区分预热与正式推理;
- 统计连接是否复用;
- 调试某个 Session、Thread 和 Turn 的异常;
- 区分主 Agent 与子 Agent 请求。
4. WebSocket 建立和预热

Codex 当前同时存在两个容易混淆的动作:
preconnect_websocket():只建立连接,不发送 Prompt;prewarm_websocket():建立连接后发送response.create,但设置generate=false,等待一个完整响应结束。
本文框架中的“预热”指第二种。
4.1 WebSocket Upgrade
WebSocket 客户端先根据 Provider 的 Responses URL 构造 WebSocket URL,再通过共享 HttpClientFactory 建立连接。这个工厂保证 HTTP 与 WebSocket 观察到同一套:
- 代理策略;
- DNS 与连接配置;
- TLS;
- 自定义 CA;
- 企业网络路由。
握手仍然从 HTTP 开始。成功时服务端返回 101 Switching Protocols,连接升级为 WebSocket。Codex 还提供一个 handshake probe:它使用与真实连接相同的 URL、Header、认证、TLS 与 CA,仅验证 Upgrade 是否成功,并短暂等待服务端是否立即返回 Close Frame,用来区分“握手成功但立刻被策略拒绝”的情况。12
源码为 WebSocket 启用了 permessage-deflate,以压缩重复的 JSON 事件与请求载荷。
4.2 认证 Header
WebSocket 握手 Header 来自多个层次的合并:
- Provider 固定 Header;
- 当前请求附加 Header;
- 默认客户端 Header;
- Auth Provider 添加的认证 Header;
- Session / Thread / Originator 信息;
- Codex 兼容性 Header;
- 可选 Attestation;
- WebSocket Beta Header。
当前源码会为 Responses WebSocket v2 加入:
OpenAI-Beta: responses_websockets=2026-02-06ChatGPT 登录与 API Key 使用不同的认证材料,但在这里都被统一成 AuthProvider.add_auth_headers(),因此连接逻辑无需感知具体登录模式。
4.3 Connect Timeout
Provider 配置单独暴露 websocket_connect_timeout_ms。当前源码默认值为 15 秒。连接函数被包裹在 tokio::time::timeout 中,超过时转换为 Transport Timeout。
这个超时与 Stream Idle Timeout 解决不同问题:
| 超时 | 阶段 | 典型原因 |
|---|---|---|
| Connect Timeout | DNS、TCP、TLS、Upgrade | 代理不支持 Upgrade、证书问题、网络不可达 |
| Stream Idle Timeout | 已连接并发送请求后 | 服务端挂起、半开连接、事件长期不再到达 |
4.4 response.create
WebSocket 上的业务请求仍然是 Responses API 结构,只是在最外层加入事件类型:
{ "type": "response.create", "model": "...", "instructions": "...", "input": [...], "tools": [...], "parallel_tool_calls": true, "reasoning": {...}, "client_metadata": {...}}如果可以增量续接,还会加入:
{ "previous_response_id": "resp_...", "input": [/* 仅新增 items */]}同一 WebSocket 在任意时刻只处理一个 Response Stream。源码在整个响应生命周期持有连接 Mutex,防止两个请求同时在同一 Socket 上交错消费事件。
4.5 generate=false 预热
prewarm_websocket() 调用与正式推理相同的 WebSocket 请求路径,但设置:
{ "type": "response.create", "generate": false}它会等待直到收到 ResponseEvent::Completed,才认为预热完成。源码明确把这次请求定义为连接准备,而不是模型推理:
- 不计入正常 inference trace;
- 不按正式推理记录 Session Telemetry;
- 但会占用本 Turn 的第一次 WebSocket 尝试预算;
- 失败后仍进入正常 Stream Retry / Fallback 逻辑。
与“只握手”的 preconnect 相比,generate=false 预热更进一步:它不仅证明 WebSocket 能连上,还验证 Responses 协议链路能够完成一次完整的 response.create → response.completed。
4.6 预热连接如何被正式请求复用
预热结束后,WebsocketSession 中会保留:
- 已建立的连接;
- 预热时的完整请求;
- 预热响应的
response_id; - 服务端新增的 Output Items;
- “该 Response ID 来自未写入推理 Trace 的预热”这一标记。
正式首轮请求到来时,Codex 会判断它是否与预热请求具有相同的非 Input 属性,并且 Input 是否只是严格扩展。如果成立,正式请求可以:
- 复用同一条 WebSocket;
- 使用预热得到的
previous_response_id; - 只发送新增 Input,甚至发送空 Delta;
- 在 Rollout Trace 中仍记录逻辑上的完整请求,避免传输优化破坏可复现性。
预热真正节省的不是模型计算本身,而是首轮正式请求中的:
- TCP/TLS/Upgrade 开销;
- 服务端连接初始化;
- 重复请求体传输;
- 某些固定路由与协议准备延迟。
5. Turn 内连接和状态复用
连接复用、Sticky Routing 和增量请求经常被混为一种状态。实际上,它们分别解决三个问题:
| 状态 | 解决的问题 | 作用域 |
|---|---|---|
| WebSocket Connection | 避免重复建连 | 传输层,可缓存 |
x-codex-turn-state | 保持同一 Turn 的服务端路由连续性 | 严格 Turn 级 |
previous_response_id | 引用服务端上一响应,只发送增量 Input | 请求链级 |
5.1 WebSocket Connection Cache
ModelClientSession 会优先使用已有连接:
- 若连接存在且内部 Stream 未关闭,标记为 reused;
- 若连接不存在或已关闭,清除上次请求与响应状态,重新建立连接;
- Turn 级对象销毁时,把
WebsocketSession归还到 Session 级缓存; - 下一 Turn 可以取出物理连接,但会获得新的
turn_state。
连接缓存的价值在于减少握手;它本身并不授权跨 Turn 复用任何语义 Token。
5.2 x-codex-turn-state
服务端可以通过两种方式返回 Turn State:
- WebSocket Upgrade Response Header;
- 后续 Responses Stream Event。
客户端把第一次得到的值写入 OnceLock<String>,之后在当前 Turn 的:
- 重试;
- 增量追加;
- 工具结果回填;
- 续写请求;
中反复发送同一个值。HTTP 路径把它放在 x-codex-turn-state Header 中;WebSocket 路径也会把它加入 Client Metadata 兼容字段。
5.3 Sticky Routing
从客户端视角看,x-codex-turn-state 是一个不透明 Token。Codex 不解析其内部内容,只遵守“收到后原样回放”的协议。
它的作用可以理解为:让同一 Turn 的后续请求尽量保持服务端侧的路由与状态连续性。注意,这并不意味着服务器保存了完整聊天历史;真正的 Input 与 previous_response_id 仍有各自职责。
Sticky Routing 的工程意义在于:工具循环会在几秒内连续发出多次请求,如果每次都被当成完全无关的新请求,服务端可能失去本轮专用的路由上下文、缓存或调度状态。
5.4 为什么 Turn State 不能跨 Turn 使用
源码注释直接把它定义为 Client/Server Contract:
- Turn 开始时接收;
- 当前 Turn 的所有后续请求保持不变;
- 不得发送到另一个 Turn。
如果跨 Turn 使用,可能产生三类问题:
- 路由污染:新用户任务被误认为上一 Turn 的延续;
- 状态错配:新 Turn 的 Model、Reasoning 或安全上下文与旧 Token 指向的服务端状态不一致;
- 调试失真:Session ID 相同,但 Turn ID 与 Sticky Token 的归属冲突。
所以 new_session() 每次都创建新的 OnceLock,即使物理 WebSocket 来自 Session Cache,也不会继承旧 Turn State。
5.5 previous_response_id
previous_response_id 是 Responses API 层的引用。它告诉服务端:当前请求建立在某个已完成 Response 之上。
OpenAI 当前公开文档进一步限定了这一状态的边界:活动 WebSocket 只在连接内存中保留最近一个 previous-response state;该机制与 store=false 和 ZDR 兼容。若 ID 不在连接缓存中,store=true 时服务端可能从持久状态恢复,store=false/ZDR 下则返回 previous_response_not_found。单条 WebSocket 当前最长持续 60 分钟,到期后必须重连。13
Codex 从 response.completed 中获取 response_id,同时记录服务端在该响应中新增的 Output Items。下一次请求构造时,客户端会把以下三部分视为上一状态基线:
上一完整请求的 input+ 上一响应新增的 output items+ 当前新增 input items若当前请求的前缀完全等于“上一请求 + 上一响应”,剩余部分就是可以单独发送的 Delta。
5.6 增量请求和完整请求的切换条件

Codex 不会因为“有 previous response id”就盲目使用增量请求。源码首先比较所有会影响模型语义的非 Input 属性:
- model;
- instructions;
- tools;
- tool choice;
- parallel tool calls;
- reasoning;
- store;
- stream;
- include;
- service tier;
- prompt cache key;
- text controls。
然后再确认:
- 当前 Input 长度至少覆盖上一请求 Input 与服务端新增 Items;
- 该前缀逐项相等;
- 上一响应具有非空
response_id; - 新增 Items 可以被安全切分。
client_metadata 不参与语义匹配;stream_options 也被有意排除,因为它控制当前响应如何交付,而不是 previous_response_id 所引用的上下文内容。
任一条件不满足,Codex 就发送完整请求。这个机制的本质是:
增量是有证明条件的优化,完整请求是语义正确性的保底路径。
6. WebSocket Event Pump

Codex 的 WebSocket 不是在业务代码中直接 send() / recv()。底层 WsStream 创建了一个专门 Pump Task,把 Socket I/O 与上层响应处理解耦。
6.1 请求发送 Channel
上层发送请求时,不直接持有 Socket Writer,而是把 WsCommand::Send 投递到容量为 32 的 MPSC Channel,并通过 One-shot Channel 等待发送结果。
有界 Channel 的意义是:如果上层异常地产生过多发送命令,它会形成背压,而不是无限积压写请求。
6.2 事件接收 Channel
Pump Task 使用 tokio::select! 同时监听:
- 上层发送命令;
- Socket 下行消息。
收到 Text、Binary、Close 或 Frame 后,它会转发给内部接收 Channel;上层的 run_websocket_response_stream() 再把 Text JSON 解析成 Responses 事件,并放进容量为 1600 的 Response Event Channel。
这种分层使网络读取、协议解析和 Agent Loop 消费彼此解耦。值得注意的是,Socket 到内部消息的第一段 Channel 是无界的,因此它更倾向于优先读走网络数据,真正面向业务消费者的背压主要发生在后面的有界 Response Event Channel。
6.3 Ping/Pong
Pump 收到 Ping 后立即在同一 Socket 上发送 Pong,收到 Pong 则忽略。这样可以:
- 满足标准 WebSocket 保活协议;
- 避免某些代理、NAT 或服务端因长时间无控制帧而回收连接;
- 把保活逻辑与 Responses 业务事件隔离。
Ping/Pong 成功只能说明连接仍可双向传输控制帧,不代表模型请求一定会完成。因此 Stream Idle Timeout 仍然需要观察业务事件是否持续到达。
6.4 Close Frame
Close Frame 是明确的协议终止信号。Codex 会把它转交上层,然后结束 Pump。
在 Responses 处理层,只有先收到 response.completed 才算正常完成;如果服务端在此之前发出 Close,客户端返回:
websocket closed by server before response.completed如果底层流直接结束而没有 Close,也会被归类为:
stream closed before response.completed6.5 response.completed
response.completed 是一次 Responses 请求的事务边界。收到它后,Codex 会:
- 提取
response_id; - 记录 Token Usage 与
end_turn; - 汇总本响应所有
OutputItemDone; - 把
LastResponse通过 One-shot Channel 交给WebsocketSession; - 为下一次增量请求保存基线;
- 结束当前 WebSocket Response Stream。
在此事件之前出现的文本看起来再完整,也不能被当作请求已经成功提交。
6.6 Tool Call 事件
Responses 事件层暴露:
OutputItemAdded;ToolCallInputDelta;OutputItemDone;OutputTextDelta;- Reasoning Summary / Content Delta。
ToolCallInputDelta 允许 UI 或调试层观察参数逐步生成,但真正进入 Tool Router 的应是完成的 Tool Call Item。完成后,ToolCallRuntime 执行工具并把结果转换成下一次请求的 FunctionCallOutput。
因此要区分:
- 参数 Delta:网络流中的未完成数据;
- 完成 Tool Call:可调度的执行单元;
- FunctionCallOutput:工具执行后回填模型的结构化 Input。
6.7 Rate Limit 事件
Codex 对 codex.rate_limits 做专门处理:解析成 RateLimitSnapshot 并通过 ResponseEvent::RateLimits 发送给上层,而不是把它混进普通文本事件。
WebSocket 中还可能包裹 HTTP 风格错误,例如:
{ "type": "error", "status": 429, "error": { "type": "usage_limit_reached" }, "headers": { "x-codex-primary-used-percent": "100.0" }}这类事件会被转换成 Transport HTTP Error,让统一错误映射逻辑继续处理。也就是说,使用 WebSocket 不会让 HTTP 状态语义消失,只是它们可能以内嵌错误事件的形式出现。
7. Codex 的断线恢复状态机

Codex 把“初始请求失败”和“流已开始后断开”分开预算,并在 Stream Retry 耗尽后尝试切换传输。14
7.1 Request Retry 与 Stream Retry
当前 Provider 配置分别暴露:
request_max_retries:默认 4;stream_max_retries:默认 5。
源码中的通用 HTTP Retry Policy:
- 重试 Transport Error;
- 重试 5xx;
- 不在这一层自动重试 429;
- 基础延迟 200ms。
Stream Retry 则用于已经进入流式处理后的中断,包括 WebSocket、HTTP SSE 采样流和远程压缩流。它的恢复循环位于 Turn 层,因此可以复用同一个 ModelClientSession、Turn State 和工具上下文。
这两个预算不能合并:
- Request Retry 关注“请求有没有成功建立”;
- Stream Retry 关注“已建立的响应有没有完整到达终止事件”。
7.2 Stream Idle Timeout
当前源码默认 stream_idle_timeout_ms = 300000,即 300 秒。Provider 可以覆盖,硬编码默认值也可能随版本变化。
WebSocket 路径在两个位置使用这一超时:
- 发送
response.create时等待 Socket Send 完成; - 循环等待下一条 WebSocket Message。
只要连续 300 秒没有下一条消息,就返回:
idle timeout waiting for websocket配置文档中这一字段有时仍以 SSE Idle Timeout 描述,但当前源码把同一个 Provider stream_idle_timeout 传给 WebSocket Connection。这是理解“配置名与实际作用范围”时需要注意的版本细节。
7.3 退避等待
处理可重试流错误时,Codex优先使用错误对象携带的 Retry Delay;如果服务端没有提供,则使用本地 Backoff 函数。
正确流程是:
错误分类→ 计算 delay→ 记录 retry_count / max_retries / turn_id→ 必要时通知 UI→ sleep(delay)→ 使用同一 Turn ClientSession 再次请求退避发生在 Turn 层而不是 Socket Pump 内。Pump 只负责报告“这条连接已失败”,恢复策略由更高层决定。
7.4 首次重连静默处理
在 Release 构建中,如果当前使用 WebSocket,Codex默认隐藏第一次重连通知。理由很现实:移动网络抖动、代理瞬时切换或服务端短暂停顿可能在第一次快速重试中恢复,立刻向用户展示红色错误只会制造噪声。
下列情况会立即显示:
- 已是第二次或更多次重试;
- Debug 构建;
- 当前并未启用 WebSocket。
这是“隐藏瞬时错误”和“避免用户误以为卡死”之间的折中。
7.5 “Reconnecting”状态上报
当需要向上层报告时,Session 会收到类似:
Reconnecting... 2/5的状态事件,同时保留原始错误信息。UI 因此可以:
- 显示当前重试次数;
- 区分正在恢复与已经失败;
- 避免长时间无反馈;
- 在最终降级时显示传输切换 Warning。
7.6 失败连接销毁
WebSocket Response Stream 遇到终止错误时,不会等待可能永远无法完成的优雅 Close Handshake。源码会:
- 从连接对象中
take()出失败的WsStream; - 释放 Mutex;
- 直接 Drop 失败流,终止 Pump Task;
- 立即把错误发送给调用方。
这是一个重要的“先销毁污染连接,再做恢复”原则。如果保留半开连接,下一次重试可能继续复用一个已经失去协议同步的 Socket。
7.7 新 WebSocket 建立
下一次重试进入 websocket_connection() 时,会检测:
- Connection 不存在;或
- 外层对象还在,但内部 Stream 已被清空。
若需要新连接,客户端先清除:
last_request;last_response_rx;- warmup 标记;
- connection reused 标记。
然后重新执行与初始连接相同的 Provider、认证、Header、TLS 和 Telemetry 逻辑。
清除 Last Request/Response 非常关键。它确保一次连接级故障不会让新 Socket 继续引用旧连接上的不可靠 previous_response_id 链。
8. WebSocket 向 HTTPS 降级
HTTPS 降级不是简单地把 ws:// 改成 https:// 再发同一份帧。它会改变 Session 的传输决策,并清空所有 WebSocket 专属续接状态。
8.1 何时触发降级
源码中有两类明确入口。
入口一:服务端返回 HTTP 426 Upgrade Required
如果 WebSocket 建连返回 426,客户端直接得到 FallbackToHttp,不再把它当成普通网络抖动反复重连。
入口二:Stream Retry 达到上限
当 retries >= max_retries 时,恢复器调用:
client_session.try_switch_fallback_transport(...)若当前仍可从 WebSocket 切换到 HTTP,就向 UI 发送:
Falling back from WebSockets to HTTPS transport.8.2 重试计数如何重置
降级激活后,恢复函数把当前 Stream Retry 计数重置为 0,再返回请求循环。
这是必要的,因为:
- 前面的失败预算属于 WebSocket;
- HTTPS 是新的 Transport;
- 若沿用已耗尽的计数,HTTPS 会在第一次错误时立即终止,等于没有真正给降级路径恢复机会。
8.3 降级为什么保持在 Session 级
force_http_fallback() 会原子地把 Session 级 disable_websockets 设为 true,并清空 Session 中缓存的 WebSocket 状态。
这反映了源码对故障范围的判断:
当 WebSocket 已经连续失败到需要切换 Transport 时,问题更可能来自当前企业代理、TLS Inspection、网络策略或 Provider 能力,而不是某一个 Response 的偶发错误。
如果只在当前请求临时降级,下一 Tool Call 或下一 Turn 又会重新尝试 WebSocket,形成:
WS 重试 → HTTP 成功 → 再次 WS 重试 → HTTP 成功 → ...这样的抖动。Session 级熔断避免了重复付出握手和失败延迟。
8.4 后续 Turn 如何选择 Transport
每次 ModelClientSession.stream() 都先检查:
provider.supports_websocketsAND NOT session.disable_websockets如果 Session 已降级,后续 Turn 会直接调用 HTTPS Responses Path,而不是重新探测 WebSocket。
只有创建新的 Codex Session,或明确重置相应客户端状态,才会重新获得 WebSocket 尝试机会。
这是一种轻量的 Session 级 Circuit Breaker:它不永久修改用户配置,只在当前会话生命周期内记住失败结论。
8.5 previous_response_not_found 后的完整请求回退
WebSocket 服务端可能返回错误码:
previous_response_not_foundCodex 将其映射成 Retryable Error,并给出“Retrying the full request”的语义。完整回退路径由两部分共同完成:
- 终止流错误会使当前内部 WebSocket Stream 被取出并销毁;
- 下一次请求发现连接已关闭,重建连接前清空 Last Request 与 Last Response。
重新建连后,prepare_websocket_request() 无法取得可用 Last Response,因此不会附带 previous_response_id,而是发送完整 request.input。
即使没有发生连接重建,只要以下任一条件失效,增量构造器也会自然退回完整请求:
- Request 属性不一致;
- Input 前缀不匹配;
- Response ID 为空;
- Last Response 不可用。
因此,previous_response_not_found 的恢复不是“伪造另一个 ID”,而是放弃服务端引用优化,用客户端持有的完整上下文重建语义状态。
9. 完整案例时序

下面用一个真实编码任务串起全部机制:
用户要求 Codex 读取 GitHub Issue,定位代码问题,修改文件,执行测试,创建 Pull Request,并返回结果。执行期间 WebSocket 流中断,多次重连失败,最终切换到 HTTPS 后继续完成任务。
9.1 阶段一:预热不是“空连一下”
Codex 发送 generate=false,等待完整 response.completed,得到可被首轮正式请求引用的 Response ID。预热失败会占用本 Turn 的 Stream Retry 预算,不会被当作毫无成本的旁路操作。
9.2 阶段二:每次 Tool Call 都产生新的模型往返
读 Issue、搜索代码、编辑文件、运行测试、创建 PR 并不是一条连续模型输出,而是多次:
Responses → Tool Call → 本地执行 → FunctionCallOutput → ResponsesWebSocket 复用和 previous_response_id 正是为了减少这些往返中的重复连接与重复 Input。
9.3 阶段三:流中断后不能假设上一响应完成
即使模型已经输出了一部分修复建议,只要没有 response.completed,Codex 就不能把该请求写成稳定 Last Response,也不能安全地基于其 Response ID 做下一次增量追加。
失败连接被销毁,新连接清除上一请求链,必要时用完整上下文重建。
9.4 阶段四:HTTPS 降级不丢失 Agent 状态
WebSocket 状态被清空,不等于整个 Turn 被重启。以下状态仍在客户端:
- Conversation History;
- 已执行的工具结果;
- 本地文件修改;
- Turn Context;
- Tool Call Runtime 状态;
- 当前目标与测试输出。
所以 HTTPS 可以用完整 Input 继续任务,而不是从“修复 Issue”第一步重新开始。
9.5 阶段五:传输成功不等于工具副作用可随意重放
如果断线发生在“GitHub 已创建 PR,但 PR 结果尚未回填”这一窗口,仅靠模型传输重试仍可能重复创建 PR。这个问题属于下一篇要讨论的工具幂等与副作用恢复,而不是 WebSocket 本身能解决的问题。
Codex 的网络层保证模型请求尽量连续;外部工具是否可以安全重放,仍需要 Tool Call ID、业务唯一键和执行日志共同约束。
结论:Codex 的网络层是一台分层状态机
Codex 网络层最值得借鉴的不是“使用了 WebSocket”,而是它把不同寿命的状态放在了不同对象里:
- Session 级
ModelClient保存 Provider、认证、代理策略和传输熔断结论; - Turn 级
ModelClientSession保存 Sticky Routing Token、连接使用状态和请求续接基线; - Response 级状态 以
response.completed、response_id与 Output Items 为提交边界; - Tool Call 级状态 由 Tool Runtime 执行,再转换成下一次 Responses Input;
- 物理连接复用 与 逻辑 Turn 隔离 被明确分开;
- 增量请求 只是通过严格条件证明后启用的优化,完整请求始终是正确性回退;
- WebSocket 重试 解决瞬时故障,Session 级 HTTPS 降级 解决当前环境中的持续传输不兼容。
这也是为什么连接复用和故障恢复必须放在同一篇理解:恢复逻辑并不是附加在 WebSocket 外面的几个 retry,而是在决定哪些状态可以保留、哪些状态必须丢弃、下一次请求还能不能安全引用上一响应。
参考资料
Footnotes
-
OpenAI Engineering,Speeding up agentic workflows with WebSockets in the Responses API:https://openai.com/index/speeding-up-agentic-workflows-with-websockets/ ↩ ↩2
-
OpenAI Engineering,Unrolling the Codex agent loop:https://openai.com/index/unrolling-the-codex-agent-loop/ ↩
-
OpenAI Codex 官方源码,
codex-rs/core/src/client.rs:https://github.com/openai/codex/blob/main/codex-rs/core/src/client.rs ↩ -
OpenAI Codex 官方源码,Tool Call 并发、取消与结果回填:https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/parallel.rs ↩
-
OpenAI,Get started with Codex:https://openai.com/codex/get-started/ ↩
-
OpenAI Codex 官方配置参考:https://developers.openai.com/codex/config-reference ↩
-
OpenAI Help Center,Use Codex with Amazon Bedrock:https://help.openai.com/en/articles/20001252-use-codex-with-amazon-bedrock ↩
-
OpenAI Help Center,Configure Codex with Amazon Bedrock:https://help.openai.com/en/articles/20001253-configure-codex-with-amazon-bedrock ↩
-
OpenAI Codex 官方源码,
codex-rs/model-provider-info/src/lib.rs:https://github.com/openai/codex/blob/main/codex-rs/model-provider-info/src/lib.rs ↩ -
OpenAI Codex 官方源码,Responses 请求与事件类型:https://github.com/openai/codex/blob/main/codex-rs/codex-api/src/common.rs ↩
-
OpenAI Codex 官方源码,Responses Client Metadata:https://github.com/openai/codex/blob/main/codex-rs/core/src/responses_metadata.rs ↩
-
OpenAI Codex 官方源码,Responses WebSocket Endpoint:https://github.com/openai/codex/blob/main/codex-rs/codex-api/src/endpoint/responses_websocket.rs ↩
-
OpenAI API Docs,WebSocket Mode:https://developers.openai.com/api/docs/guides/websocket-mode ↩
-
OpenAI Codex 官方源码,Stream Retry 与 Transport Fallback:https://github.com/openai/codex/blob/main/codex-rs/core/src/responses_retry.rs ↩