
1. 从一次“对话丢失”说起Thread、Turn、Item 到底解决什么问题如果你正在读 codex cli 源码大概率已经翻过codex-app-server-protocol这个 crate。我第一次看的时候也有点懵Thread、Turn、Item 三层对象看起来就是普通结构体为什么协议要设计得这么绕直到我在本地跑 codex cli 时遇到一个真实问题——客户端界面上明明已经流式打印出了 Agent 的完整回复可一旦触发turn/completed之前收集的内容全被清空了。排查了半天才发现问题出在我把turn/completed通知里的turn对象直接覆盖了本地状态而那个对象里的items是空的。这就是理解这套协议的关键Thread、Turn、Item 不是简单的嵌套数据结构而是一套“快照 事件”的混合模型。快照负责描述当前状态事件负责驱动增量更新两者必须配合使用任何一方单独使用都会出问题。具体来说Thread 代表一段可以继续、恢复、分叉的会话资源Turn 代表 Thread 上一次有边界的用户任务Item 则是 Turn 内部最小粒度的内容单元可能是一条用户消息、一段 Agent 回复、一次命令执行、一次 MCP 工具调用。三者通过 ID 层层关联一个 Thread 包含多个 Turn一个 Turn 包含多个 Item每个 Item 只属于一个 Turn。这套设计要同时回答四个问题每层对象保存什么状态Request/Response 与流式 Notification 如何配合客户端如何从增量事件还原最终界面断线恢复时持久化历史如何变回相同的数据模型把这四个问题想清楚你再看 codex cli 的源码就不会迷路。本篇会结合 TaoToken 的统一 Key/API 通道把 codex 的auth.json改到 TaoToken然后跑一次完整的对话链路从协议解析一路验证到实际请求。适合已经读过前几篇、想深入协议内部并动手跑通的开发者。2. TaoToken 前置准备统一 Key 与 API 通道在动手改auth.json之前先把 TaoToken 这边的准备工作做完。TaoToken 提供的是统一的 API 通道你只需要一个 Key 就能访问多种模型不用为每个模型单独维护一套凭证。这对我们调试 codex cli 特别方便因为 codex 的modelProvider字段可以指向同一个 Base URL切换模型只改 Model ID 就行。第一步是拿到 API Key。打开控制台页面登录后进入 API Keys 管理https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite在页面里创建一个新的 Key复制出来保存好。这个 Key 就是后面auth.json里要填的凭证。注意不要把它提交到 Git 仓库建议放在环境变量或本地配置文件里。第二步是确认 Base URL。TaoToken 的 API 入口是https://taotoken.net/api这个地址不加任何 UTM 参数直接作为 codex 的base_url使用。如果你用的是 OpenAI 兼容协议codex cli 会在这个地址后面拼接/v1/chat/completions或/v1/responses之类的路径具体取决于你选的模型和协议模式。第三步是选模型。TaoToken 支持多种模型你可以先在模型对话页面里试一下确认哪个模型适合你的场景https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite选好之后记下 Model ID比如gpt-4o、claude-3-5-sonnet这类标识。这个 ID 后面要写进 codex 的配置里。如果你打算长期用 codex 做编码或 Agent 任务可以看一下 Coding Plan它针对高频调用做了优化https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入文档在这里遇到字段不确定的时候可以对照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite准备工作就这些。接下来我们进入 codex cli 的配置环节把auth.json改到 TaoToken。3. 可复制配置把 codex auth.json 改到 TaoTokencodex cli 的凭证和模型配置分散在两个地方auth.json负责 API Keyconfig.toml负责模型和 Provider。我们先看auth.json。默认情况下codex 会把凭证存在~/.codex/auth.json。如果你之前用官方登录方式跑过这个文件里可能是 OAuth 相关的字段。我们要把它改成 TaoToken 的 API Key 模式。先备份原文件cp ~/.codex/auth.json ~/.codex/auth.json.bak然后用编辑器打开~/.codex/auth.json替换成下面这个结构{ OPENAI_API_KEY: sk-你的TaoToken密钥, tokens: null, last_refresh: null }这里的关键是OPENAI_API_KEY字段。codex cli 在 API Key 模式下会读取这个值并把它作为Authorization: Bearer头发送出去。tokens和last_refresh设为null是为了避免 codex 尝试走 OAuth 刷新流程。如果你之前登录过这两个字段可能有值清空它们可以强制走 API Key 路径。接下来配置~/.codex/config.toml。这个文件控制模型选择和 Provider 地址model gpt-4o model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key OPENAI_API_KEY wire_api chat逐字段说明一下。model是你从 TaoToken 模型列表里选的 Model ID。model_provider指向下面定义的 Provider 名称。base_url就是 TaoToken 的 API 入口注意不要加/v1codex 会根据wire_api自动拼接路径。env_key告诉 codex 从哪个环境变量或auth.json字段读取 Key这里写OPENAI_API_KEY正好对应我们上面配的字段。wire_api有两个常用值chat走/v1/chat/completionsresponses走/v1/responses。大多数模型用chat就行如果你选的模型明确支持 Responses API可以改成responses。如果你用的是 Claude Code 风格的接入codex 这边也可以通过model_providers配置多个 Provider然后在不同项目里切换。比如再加一个[model_providers.taotoken_claude] name TaoToken Claude base_url https://taotoken.net/api env_key OPENAI_API_KEY wire_api chat然后在项目根目录放一个.codex/config.toml覆盖全局配置把model_provider改成taotoken_claudemodel改成对应的 Claude Model ID。这样全局用一套项目里用另一套互不干扰。配置写完后确认一下文件权限避免 Key 被其他用户读到chmod 600 ~/.codex/auth.json chmod 600 ~/.codex/config.toml到这里配置就完成了。三件套齐全Base URL 是https://taotoken.net/apiKey 是auth.json里的OPENAI_API_KEYModel ID 是config.toml里的model。接下来我们验证请求能不能跑通。4. 验证请求从 curl 到完整对话链路配置写好了不代表能跑通先用 curl 直接打一次 TaoToken 的接口排除网络和 Key 的问题。这个命令模拟 codex 在wire_api chat模式下发出的请求curl -sS https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $OPENAI_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o, messages: [ {role: user, content: 用一句话解释 Thread、Turn、Item 的关系} ], stream: false }把$OPENAI_API_KEY换成你实际的 Keymodel换成你配置里的 Model ID。如果返回一个包含choices数组的 JSON说明 Key 和 Base URL 都没问题。如果返回 401说明 Key 不对或没带上如果返回 404检查一下 Base URL 后面是不是多写了/v1。curl 通了之后跑 codex cli 本身。在终端里执行codex exec 用一句话解释 Thread、Turn、Item 的关系codex exec是非交互模式适合脚本化验证。如果配置正确你会看到 codex 输出模型回复。这时候打开另一个终端观察 codex 的日志输出。codex 在调试模式下会打印协议事件你可以加RUST_LOGdebug环境变量RUST_LOGdebug codex exec 用一句话解释 Thread、Turn、Item 的关系 21 | head -100在日志里你会看到类似这样的协议事件序列turn/start Response turn/started Notification item/completed UserMessage item/started AgentMessage item/agentMessage/delta item/completed AgentMessage turn/completed这就是我们前面讲的完整 Turn 事件序列。注意turn/start的 Response 里items是空的itemsView是notLoadedturn/started通知里startedAt才有值turn/completed通知里items依然是空的只有status、completedAt、durationMs这些终态字段。如果你在客户端里把turn/completed的turn对象直接覆盖本地状态之前通过item/agentMessage/delta收集的内容就会丢失。再验证一下流式模式。codex 默认走流式你可以用 curl 模拟curl -sS -N https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $OPENAI_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o, messages: [ {role: user, content: 数到五} ], stream: true }-N关闭 curl 的缓冲你会看到data: {...}一行行返回。codex 内部会把每个 chunk 映射成item/agentMessage/delta事件最后再发一个item/completed携带完整文本。这就是协议里说的“Delta 用于低延迟显示Completed Item 用于最终一致性”。如果你在日志里看到turn/completed之后 Agent 消息还在说明你的 Reducer 逻辑是对的。如果消息消失了回去检查是不是执行了turn.items event.turn.items这种覆盖操作。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把接入过程中最容易踩的坑列出来对照真实报错定位问题。401 Unauthorized。最常见的原因是auth.json里的OPENAI_API_KEY没填对或者config.toml里的env_key写成了别的名字。检查两点auth.json里字段名必须是OPENAI_API_KEYconfig.toml里env_key必须和它一致。另外确认 Key 没有多余空格复制的时候容易带上换行。如果 Key 是从环境变量读的确认export OPENAI_API_KEYsk-xxx在当前 shell 里生效。local proxy failed。这个报错通常出现在 codex 尝试走本地代理但代理没起来的时候。如果你之前配过HTTPS_PROXY之类的环境变量先清掉unset HTTPS_PROXY HTTP_PROXY ALL_PROXY然后确认config.toml里的base_url是https://taotoken.net/api没有指向localhost或127.0.0.1。codex 的 Provider 配置里如果残留了旧的本地地址就会报这个错。reading choices 相关报错。典型信息是error reading choices: unexpected end of JSON input或missing field choices。这通常说明返回的不是标准 OpenAI 格式可能是 Base URL 拼错了路径比如写成了https://taotoken.net/api/v1导致 codex 又拼了一次/v1/chat/completions变成/api/v1/v1/chat/completions。把base_url改回https://taotoken.net/api即可。另一个可能是wire_api和模型不匹配比如模型只支持 Responses API 但你配了chat换成responses试试。OAuth 相关报错。如果你看到OAuth token refresh failed或invalid_grant说明 codex 还在尝试走 OAuth 流程。检查auth.json里tokens和last_refresh是不是null。如果它们有值codex 会优先走 OAuth 而不是 API Key。清空这两个字段或者直接删掉auth.json重新写一份。Codex auth.json 三件套检查清单。出现任何连接问题时按这个顺序核对Base URL 是不是https://taotoken.net/api不带/v1Key 是不是写在auth.json的OPENAI_API_KEY字段Model ID 是不是和 TaoToken 模型列表里的一致。这三项任何一项不对都会导致请求失败。如果你用的是 CC Switch 或 Cline MCP 这类工具同样要保证这三件套一致MCP 配置里的baseUrl、apiKey、model要和 codex 这边对齐。turn/completed 覆盖问题。这个不是报错但会导致界面内容丢失。排查方法是看你的 Reducer 里有没有turn.items event.turn.items这样的赋值。正确做法是只合并status、error、startedAt、completedAt、durationMs这些终态字段保留本地已收集的items。时间戳单位混淆。Thread 和 Turn 的createdAt、updatedAt、startedAt、completedAt单位是秒Item 生命周期通知的startedAtMs、completedAtMs单位是毫秒Turn 的durationMs也是毫秒。如果你把所有数字都塞给同一个new Date()构造函数秒级时间戳会被当成 1970 年附近的毫秒值显示出来的时间完全不对。分开处理。6. 继续深入从协议到 Core 调用边界把上面的配置和验证跑通之后你对 Thread、Turn、Item 三层协议应该有了实感。核心规则再强调一遍Thread 是可恢复的会话资源Turn 是一次有终态的任务Item 是最小内容单元itemsView决定空items是“没有内容”还是“未加载”turn/start的 Response 只表示提交成功turn/started才表示 Runtime 真正开始turn/completed只携带终态元数据不携带完整 Item客户端必须按 ID Upsert用 Normalized State 避免覆盖。如果你想继续往下读源码下一篇会进入 App Server 到 Core 的调用边界跟踪一次 JSON-RPC Request 如何经过 MessageProcessor、ThreadRequestProcessor、ThreadManager最终创建并返回一个可运行的 Core Thread。那条链路会把我们今天讲的协议对象和实际的 Core 执行串起来。在继续之前建议你先把本篇的 curl 验证和 codex exec 跑一遍确认 TaoToken 通道是通的。如果遇到问题对照第 5 节的排查清单逐项检查。接入文档在这里可以随时查阅https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite需要新建或轮换 Key 的时候去 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite想先试试模型效果再决定用哪个去模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite长期跑编码和 Agent 任务的话Coding Plan 会更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite我自己的习惯是每次改完auth.json和config.toml之后先跑一遍 curl再跑codex exec最后看RUST_LOGdebug的日志确认事件序列完整。这三步走完基本不会出问题。