从 Codex 第三方 Provider 到 Responses API:状态管理、WebSocket 与上下文重放

📝 本文记录一次从 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_callfunction_call_outputexec_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 时,最容易混在一起的其实是三个不同维度:

  1. 协议模型:Responses API 还是 Chat Completions API;
  2. 状态管理:上下文保存在服务端还是客户端;
  3. 传输方式: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 / output Item;
  • 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 Logrollout.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_idCodex 当前是否使用
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”,我不会只看它能不能成功返回文本,而会至少检查以下几项:

  1. 是否支持完整的 Responses Item Model;
  2. 是否正确支持 function_call / function_call_output
  3. reasoning Item 的格式和回放语义是否兼容;
  4. previous_response_id 是否支持;
  5. previous_response_id 是仅某种传输支持,还是 HTTP 也支持;
  6. 是否支持 Responses WebSocket;
  7. WebSocket 是否真正实现 response state,而不是只提供一个 WebSocket Endpoint;
  8. 是否支持 conversation state;
  9. 不支持服务端状态时,客户端是否会 Full Effective Context Replay;
  10. 长上下文是否会触发 compaction / summary;
  11. Tool Call / Tool Result 是否会完整进入下一轮上下文;
  12. 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 时,只要先把这几个维度拆开,大部分“为什么它明明兼容但行为不一样”的问题就容易解释得多。

参考资料

🔎 本文关于 Codex Transport 与 Provider Capability 的描述基于 2026-08-09 期间的源码行为。Codex 仍在快速迭代,后续版本可能调整 HTTP previous_response_id、WebSocket fallback 或 Provider capability 的实现。

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇