📝 本文记录一次从 Codex Desktop 第三方 API 接入问题出发的源码探索。回答几个实际工程问题:Codex 到底怎样维护上下文?第三方 Provider 为什么可以无状态?previous_response_id 和 WebSocket 是什么关系?supports_websockets 又是谁决定的?
记录时间:2026-08-09
1. 问题是怎么开始的
最开始遇到的问题其实和 Responses API 没有直接关系。
在 Codex Desktop 中接入第三方 Responses API 后,当前会话里 Tool Call 可以正常执行,也能在 UI 中看到类似 Ran command、文件读取、补丁应用等调用记录。但退出 Codex Desktop 再重新打开同一个会话后,普通 user / assistant 消息仍然存在,之前的 Tool Call 卡片却消失了。
第一反应很容易怀疑第三方 API:是不是 Tool Call 没有正确返回,或者兼容层没有把工具调用持久化?
检查本地 ~/.codex/sessions/.../rollout-*.jsonl 后发现,历史里实际上仍然存在 function_call、function_call_output、exec_command 等 Item。也就是说:
- Tool Call 执行成功;
- Tool Result 正常返回;
- Session 原始记录也已经落盘;
- 实时 UI 能正常显示;
- 只有 Desktop 重启后的历史重建没有把这些 Tool Item 正确恢复成 UI 卡片。
因此这更像 Codex Desktop 的 session rehydration / history replay / UI reconstruction 问题,而不是第三方 API 的 Tool Calling 本身失效。
这个问题反过来又引出了一个更有意思的问题:既然 Codex 本地有完整 Session,那么每一轮调用模型时,Codex 到底上传多少历史?
2. 先把三个概念拆开:协议、状态、传输
讨论 Responses API 时,最容易混在一起的其实是三个不同维度:
- 协议模型:Responses API 还是 Chat Completions API;
- 状态管理:上下文保存在服务端还是客户端;
- 传输方式:HTTP/SSE 还是 WebSocket。
这三个维度彼此有关,但不是一回事。
flowchart TD
A["Responses API"] --> B["协议 / Item 数据模型"]
A --> C["状态管理"]
A --> D["传输方式"]
C --> C1["Server-side state<br>previous_response_id / conversation"]
C --> C2["Client-side state<br>完整有效上下文 replay"]
D --> D1["HTTP + SSE"]
D --> D2["WebSocket"]
一个 Provider 完全可以:
- 使用 Responses API 的 JSON / Item 格式;
- 服务端完全无状态;
- 只支持 HTTP/SSE;
- 仍然被 Codex 正常使用。
DeepSeek 的 Responses API 就是一个很典型的例子。
3. Responses API 本身并不等于“有状态 API”
OpenAI Responses API 支持 previous_response_id。例如第一轮服务端返回:
{
"id": "resp_001"
}
第二轮客户端可以只发送新增内容:
{
"model": "...",
"previous_response_id": "resp_001",
"input": "继续刚才的问题"
}
这时历史上下文可以由 OpenAI 服务端根据 resp_001 恢复,因此客户端不必重新上传上一轮完整内容。
但 Responses API 同样可以工作在客户端维护状态的模式下:
{
"model": "...",
"store": false,
"input": [
{"role": "user", "content": "第一轮问题"},
{"role": "assistant", "content": "第一轮回答"},
{"role": "user", "content": "第二轮问题"}
]
}
这种模式下,会话状态由客户端维护,服务端只处理当前请求携带的有效上下文。
因此更准确的理解是:
Responses API 定义的是协议与运行时对象模型;Stateful / Stateless 是具体 Provider 的状态实现策略。
4. store: false 不是“无状态能力声明”
另一个容易误解的字段是:
{
"store": false
}
它不能简单理解为:
supports_stateful = false
store 表达的是本次 Response 的持久化行为,而不是 Provider 的 Capability 声明。
即使请求设置 store: false,也不能据此判断服务器是否支持:
previous_response_id;- 临时连接状态;
- conversation state;
- WebSocket connection-local state。
所以在排查第三方兼容性时,不要用 store 去判断“这个 Provider 有没有状态”。
5. DeepSeek 为什么可以兼容 Responses API,同时又是 Stateless
DeepSeek 官方 Responses API 文档明确说明它是无状态 API,并且当前并不支持:
previous_response_id;conversation。
这并不和“兼容 Responses API”冲突。
因为它兼容的主要是 Responses 的协议形态,例如:
input/outputItem;message;reasoning;function_call;function_call_output;- Responses 风格的流式事件。
它没有完整实现 OpenAI 的服务端会话状态语义。
因此更准确地描述第三方兼容层时,我会区分两件事:
Wire / Object Model Compatible:请求、响应、Item、事件格式兼容。
Runtime Semantics Compatible:状态管理、
previous_response_id、WebSocket 增量、内置工具等运行时语义也兼容。
很多所谓的“OpenAI Responses API Compatible”其实主要满足前者。
6. Codex 自己维护会话状态,这才是第三方 Stateless Provider 能工作的关键
Codex 并不会把会话状态完全交给 Provider。
本地 rollout-*.jsonl 可以看成 Session Event Log,而 Codex 内部还会维护当前模型真正需要看到的 Conversation / Prompt History。
因此对于一个无状态 Provider,Codex 可以重新构造当前有效上下文:
{
"input": [
{"role": "user", "content": "第一轮问题"},
{"role": "assistant", "content": "第一轮回答"},
{
"type": "function_call",
"name": "exec_command",
"call_id": "call_1",
"arguments": "..."
},
{
"type": "function_call_output",
"call_id": "call_1",
"output": "..."
},
{"role": "user", "content": "第二轮问题"}
]
}
这样 Provider 不需要记住上一轮发生过什么。
sequenceDiagram
participant U as User
participant C as Codex
participant P as Stateless Provider
U->>C: 第 1 轮问题
C->>P: 完整当前 input
P-->>C: message / reasoning / tool call
C->>C: 保存到本地会话历史
U->>C: 第 2 轮问题
C->>C: 根据本地历史重建 Effective Context
C->>P: 历史有效上下文 + 新问题
P-->>C: 第 2 轮结果
这里真正保存“记忆”的是 Codex,而不是第三方 Provider。
7. “完整上传”不是把 rollout.jsonl 原样上传
这个表述也需要精确一点。
Codex 并不是每轮直接把整个 rollout-*.jsonl 文件原封不动 POST 给 Provider。
至少要区分三层数据:
- Session Event Log:
rollout.jsonl,偏原始运行记录; - Model Effective Context:当前真正需要给模型看的有效历史;
- Responses Request input:把 Effective Context 转换成 Responses Item 后得到的请求体。
长会话还可能经过:
- compaction;
- summary;
- truncation;
- normalization;
- provider-specific metadata cleanup。
所以本文说“全量重放”时,准确含义是:
重新发送当前完整 Effective Model Context,而不是重新上传完整原始 Session 文件。
8. Codex 怎么知道第三方 Provider 是否支持 WebSocket
继续看 Codex Provider 定义,可以找到一个非常关键的 Capability:
#[serde(default)]
pub supports_websockets: bool,
对于自定义 Provider,可以在配置中声明:
[model_providers.my_provider]
name = "My Provider"
base_url = "https://api.example.com/v1"
wire_api = "responses"
supports_websockets = true
关键点是:
Codex 当前不是通过网络自动探测第三方是否支持 Responses WebSocket,而是依赖 Provider 配置 / 内置 Provider 能力表。
自定义 Provider 如果不写这个字段,因为 bool 使用默认值,所以默认就是 false。
OpenAI 官方 Provider 则由 Codex 内置配置直接声明为支持 WebSocket。
flowchart TD
A["读取 ModelProviderInfo"] --> B{"supports_websockets ?"}
B -->|false| C["HTTP / SSE"]
B -->|true| D["尝试 Responses WebSocket"]
D --> E{"WebSocket 可正常使用?"}
E -->|否| F["降级到 HTTP / SSE"]
E -->|是| G["进入 WebSocket Session 路径"]
因此 supports_websockets = true 更像一种 Capability Contract:配置是在告诉 Codex,“这个上游实现了你期待的 Responses WebSocket 语义,可以尝试使用”。
它并不是说只要服务器能接受 wss:// 连接就够了。
9. 真正兼容 WebSocket,还必须支持 response state
如果第三方只是简单给 HTTP Responses API 套了一层 WebSocket,但没有真正实现 previous_response_id 对应的状态恢复,那么增量请求会直接丢上下文。
例如第一轮:
Full Context -> resp_001
第二轮 Codex 发送:
{
"previous_response_id": "resp_001",
"input": ["新增内容"]
}
服务端必须知道 resp_001 对应哪一份上一轮 Response State。
真正的增量语义至少是:
sequenceDiagram
participant C as Codex
participant P as Provider
participant S as Provider State
C->>P: response.create + Full Context
P->>S: 保存 / 缓存 Response State
P-->>C: resp_001
C->>P: previous_response_id=resp_001 + delta
P->>S: 根据 resp_001 找回上一轮状态
S-->>P: Previous Context
P->>P: Previous Context + delta
P-->>C: 新 Response
所以“支持 WebSocket”和“支持 Codex 的 WebSocket 增量状态语义”是两个不同等级。
10. Codex 的 WebSocket 增量并不是每轮都一定生效
当 WebSocket 路径可用时,Codex 会保存上一轮请求与 Response 信息,然后判断当前请求是不是上一轮请求的增量扩展。
如果只是增加新的 Item,并且关键属性没有变化,就可以使用:
previous_response_id + delta input
但如果以下内容发生变化:
- model;
- instructions;
- tools;
- tool choice;
- reasoning 配置;
- 其它影响请求语义的关键字段;
- history 被重新 compaction;
当前请求就可能不再是上一轮请求的 append-only extension,此时需要重新发送完整 Effective Context。
flowchart TD
A["当前 WebSocket Request"] --> B{"存在可复用的上一轮 Response?"}
B -->|否| F["发送 Full Effective Context"]
B -->|是| C{"请求参数保持兼容?"}
C -->|否| F
C -->|是| D{"当前 input 是上一轮的追加扩展?"}
D -->|否| F
D -->|是| E["previous_response_id + delta input"]
因此 WebSocket 的意义不是“永远只上传新消息”,而是:
在满足复用条件时,允许 Codex 避免重复上传已经存在的上下文。
11. 一个很重要的源码结论:HTTP Responses 本来支持 previous_response_id,但 Codex 当前 HTTP 路径没用
这一点是这次探索中最容易被误判的地方。
OpenAI Responses API 协议本身并不要求 WebSocket 才能使用 previous_response_id。
普通 HTTP 请求完全可以这样调用:
{
"model": "...",
"previous_response_id": "resp_001",
"input": "继续"
}
也就是说从 API 协议能力上看:
- HTTP +
previous_response_id:可以; - WebSocket +
previous_response_id:也可以。
但是进一步查看当前 Codex 实现后发现:
- 普通 HTTP/SSE 使用的 Responses Request 结构没有
previous_response_id; - WebSocket Request 结构才包含
previous_response_id; - Codex 当前的
上一轮 response_id + delta input优化主要实现于 WebSocket Session 路径。
因此,截至本文记录时,可以把 Codex 的行为概括成:
| 路径 | Responses 协议是否支持 previous_response_id | Codex 当前是否使用 |
|---|---|---|
| HTTP / SSE | 支持 | 当前未在普通 HTTP 请求路径使用 |
| WebSocket | 支持 | 使用,用于增量上下文复用 |
这也意味着:
一个第三方 Provider 即使自己的 HTTP
/responses已经完整支持previous_response_id,只要 Codex 没有进入对应的 WebSocket 增量路径,Codex 当前也不会因为这个能力而自动改成增量上传。
12. 第三方 Provider 默认可以怎么理解
如果配置只是:
[model_providers.example]
wire_api = "responses"
base_url = "https://example.com/v1"
没有显式写:
supports_websockets = true
可以采用下面这个心智模型:
flowchart LR
A["Codex 本地 History"] --> B["构造 Effective Context"]
B --> C["Responses input Items"]
C --> D["HTTP / SSE"]
D --> E["第三方 Stateless Provider"]
E --> F["Response Items"]
F --> A
也就是:
默认不依赖第三方服务端状态,由 Codex 自己管理会话历史,并在每一轮重新提交当前完整有效上下文。
这也是为什么 DeepSeek 这种 Stateless Responses API 可以和 Codex 配合工作。
13. 那么,不开 WebSocket 的 Responses API 和 Chat Completions 有什么区别?
继续往下想会发现一个很自然的问题:
如果第三方 Responses API 没有
previous_response_id、没有 WebSocket、没有 conversation state,而且每轮还是客户端重新带完整 History,那它和 Chat Completions 到底有什么区别?
如果只看“模型收到完整上下文然后生成结果”这一层,两者确实会非常接近。
Chat Completions 常见模型是:
messages[]
assistant.tool_calls[]
tool message
Responses 更强调:
input[] / output[]
message item
reasoning item
function_call item
function_call_output item
所以对于一个只实现基础兼容层的第三方 Provider,区别很大程度会落到 对象模型和事件模型 上,而不一定是模型推理能力本身。
14. Responses 相比 Chat Completions 更重要的变化其实是 Item Model
从 Agent 开发角度看,我认为 Responses API 真正值得关注的不是 messages 改名成 input,而是它把 Agent 执行过程拆成了更明确的一等 Item。
Codex 的真实执行过程往往是:
flowchart LR
U["User Message"] --> R1["Reasoning Item"]
R1 --> T1["Function / Tool Call"]
T1 --> O1["Tool Output"]
O1 --> R2["Reasoning Item"]
R2 --> T2["Next Tool Call"]
T2 --> O2["Tool Output"]
O2 --> A["Assistant Message"]
这比简单的:
user -> assistant -> user -> assistant
要丰富得多。
因此 Responses 的 Item[] 数据模型天然更适合作为 Agent Runtime 的底层协议。
15. 回头看最开始的 Tool Call UI Bug
理解 Codex 的状态模型以后,再看最初那个 Desktop Bug,就很容易区分了。
Codex 至少存在三类“历史”:
flowchart TD
A["Session Event Log<br>rollout.jsonl"] --> B["Model Effective Context"]
B --> C["Provider Request"]
A --> D["Desktop History Rehydration"]
D --> E["UI Message / Tool Cards"]
所以完全可能出现:
rollout.jsonl中 Tool Call 仍然存在;- 下一轮模型上下文中 Tool Call / Tool Output 也仍然存在;
- 但 Desktop 重启后 UI 卡片没有恢复。
因此:
UI 不回显 Tool Call,不等于模型已经丢失这部分上下文。
排查这类问题时,需要明确区分 Session Event Log、Model Context 和 Presentation History。
16. 实际接第三方 Responses Provider 时,我现在会检查什么
以后再看到某个服务声称“兼容 OpenAI Responses API”,我不会只看它能不能成功返回文本,而会至少检查以下几项:
- 是否支持完整的 Responses Item Model;
- 是否正确支持
function_call/function_call_output; - reasoning Item 的格式和回放语义是否兼容;
previous_response_id是否支持;previous_response_id是仅某种传输支持,还是 HTTP 也支持;- 是否支持 Responses WebSocket;
- WebSocket 是否真正实现 response state,而不是只提供一个 WebSocket Endpoint;
- 是否支持 conversation state;
- 不支持服务端状态时,客户端是否会 Full Effective Context Replay;
- 长上下文是否会触发 compaction / summary;
- Tool Call / Tool Result 是否会完整进入下一轮上下文;
- Provider 对未知或不支持字段是报错还是静默忽略。
这套检查比一句“OpenAI Compatible”更有实际意义。
17. 最终形成的几个结论
✅ Responses API、状态管理和 WebSocket 必须分开理解。 Responses 是协议模型,Stateful / Stateless 是 Provider 的状态实现,HTTP / WebSocket 是传输方式。
- Responses API 可以 Stateful,也可以 Stateless;
store: false不是“无状态 Provider”的能力标记;- DeepSeek 可以无状态但仍兼容 Responses,因为 Codex 自己维护 Conversation History;
- Codex 对普通第三方 Provider 默认更倾向于 Full Effective Context Replay;
supports_websockets是 Codex 明确存在的 Provider Capability;- 自定义 Provider 未配置
supports_websockets时默认是false; - Codex 当前不会通过标准 Capability 握手自动探测第三方是否支持 Responses WebSocket;
- 配置
supports_websockets = true只是允许 Codex 尝试,Provider 仍必须真正实现 Responses WebSocket 的状态语义; - Responses API 协议本身支持 HTTP +
previous_response_id; - 但当前 Codex 普通 HTTP/SSE 路径并没有利用这个字段,增量复用主要集中在 WebSocket 路径;
- 第三方 Responses 如果既没有 server state,也没有 WebSocket,那么从纯模型调用角度确实会和 Chat Completions 很接近;
- Responses 更重要的演进在于 Agent Item Model,而不只是请求字段不同;
- Codex Desktop UI 历史、模型 Effective Context 和本地 Session Event Log 是三层不同的数据,不应该混为一谈。
18. 结语
这次探索最开始只是为了确认一个很具体的 Codex Desktop Tool Call 显示问题,最后却一路追到了 Provider Capability、Responses API 状态模型和 Codex WebSocket Transport。
我觉得最有价值的不是记住某个字段,而是建立下面这组边界:
- Responses API ≠ WebSocket;
- Responses API ≠ 必然 Stateful;
- WebSocket ≠ Stateful 的必要条件;
- Stateless Provider ≠ Codex 无法多轮对话;
- OpenAI Compatible ≠ OpenAI Runtime Semantics 完全兼容;
- Session Log ≠ Model Context ≠ Desktop UI History。
以后接入第三方 Agent API 时,只要先把这几个维度拆开,大部分“为什么它明明兼容但行为不一样”的问题就容易解释得多。
参考资料
- OpenAI Responses API / Conversation state
- OpenAI Responses API migration guide
- DeepSeek Responses API 参考文档
- DeepSeek Responses API 指南
- OpenAI Codex 开源仓库
🔎 本文关于 Codex Transport 与 Provider Capability 的描述基于 2026-08-09 期间的源码行为。Codex 仍在快速迭代,后续版本可能调整 HTTP previous_response_id、WebSocket fallback 或 Provider capability 的实现。