Codex 间歇性 400 之谜:一条对话里,它为什么有时候用 chat/completions,有时候切到 responses?

Codex 间歇性 400 之谜:一条对话里,它为什么有时候用 chat/completions,有时候切到 responses

一、问题描述

我的 Codex 接了第三方模型网关(如 Agnes AI、OneAPI、OpenRouter),大部分时候能正常对话,但时不时突然报 code: 400 错误,重试一次可能又好了。报错看起来像是"入参格式不对",但明明同一段对话、同一个模型,什么都没改。

如果你遇到过这个现象,你不是一个人。这篇文章就来拆解这个"间歇性 400"的真相。


二、快速结论

Codex 内部不是只用一种 API 协议,而是按功能场景动态切换。 它在两种协议之间跳跃:

协议 什么时候用
/v1/chat/completions 普通对话、Agent 工具调用等绝大多数场景
/v1/responses 调推理模型、传 thinking 参数、文件上传、状态恢复等部分高级功能

而第三方网关(包括你的 Agnes / OneAPI)通常只实现了 chat/completions 协议 。当 Codex 一跳到 responses 协议,网关收到不认识格式的请求,直接返回 400。

"间歇性"的原因是:你并不是每次对话都会触发这些"高级功能"。


三、为什么 Codex 不只用一套协议?

要理解这个问题,先看 Codex 的架构设计:

bash 复制代码
┌─────────────────────────────────────┐
│              Codex                   │
│  ┌─────────────────────────────┐    │
│  │   功能模块调度层              │    │
│  │  - 普通对话 → chat/completions │    │
│  │  - 推理模型 → responses       │    │
│  │  - 文件分析 → responses       │    │
│  │  - 工具调用 → chat/completions │    │
│  └──────────┬──────────────────┘    │
│             │                        │
│  ┌──────────▼──────────────────┐    │
│  │   底层 HTTP 客户端            │    │
│  │   (根据功能模块决定协议和格式)    │    │
│  └─────────────────────────────┘    │
└─────────────────────────────────────┘

Codex(以及其他现代 AI 工具如 Cursor、Windsurf)需要同时支持:

  • 传统模型 :通过 chat/completions 协议调用
  • 推理模型(o1/o3) :需要 responses 协议才能开启 thinking 机制
  • 多模态能力:文件上传、图片理解,不同协议对 content 格式要求不同
  • 状态管理responses 协议内置了 previous_response_id,Codex 可以用它做会话恢复

问题在于:Codex 没有一个统一的"协议抽象层"------它没有在底层把两种协议归一化,而是在上层功能模块里直接写死了各自用的协议。


四、协议对比:差异到底有多大?

输入格式

jsonc 复制代码
// chat/completions(第三方网关通常只实现了这个)
{
  "model": "claude-sonnet-4-6",
  "messages": [
    { "role": "system", "content": "你是一个助手" },
    { "role": "user", "content": "你好" }
  ],
  "tools": [...],
  "stream": true,
  "max_tokens": 4096
}
jsonc 复制代码
// responses(Codex 在某些分支下切过去)
{
  "model": "claude-sonnet-4-6",
  "input": "你好",
  // ^^^ 注意这层:没有 messages,是 input
  // 可以传字符串或消息数组,但格式和 messages 不同
  "tools": [...],
  "stream": true,
  "max_tokens": 4096,
  "previous_response_id": "resp_xxx",
  // ^^^ 服务端管理历史,不需要客户端传历史消息
  "stream_options": {
    "include_usage": true
  }
}

看明白了吗?这两个协议从顶层的字段名到底层的数据结构都不同,不是简单的"字段改名"能解决的。

流式输出格式

阶段 chat/completions responses
SSE 事件名 chat.completion.chunk response.output_item.added
内容路径 choices[0].delta.content output[0].content[0].text
工具调用 delta.tool_calls output[0].tool_use
用量信息 单独一个 chunk response.completed 事件内嵌

一个网关如果只实现了 chat/completions 的 SSE 解析流,碰到 responses 的流事件结构,就会直接解析失败。


五、Codex 什么时候会触发 responses 协议?(诊断表)

这是最关键的排查对照表:

你在 Codex 里的操作 Codex 用的协议 网关只支持 chat/completions 时
普通对话、问问题 chat/completions ✅ 正常
Agent 调工具、读写文件 chat/completions ✅ 正常
代码审查、编辑 chat/completions ✅ 正常
调用了 thinking / 推理模式 responses 400
上传文件让 AI 分析 responses 400
流式输出 + 实时用量统计 responsesstream_options 400
恢复上一次对话上下文 responsesprevious_response_id 400
调用了 o1/o3 系列模型 responses 400
非流式批量调用 chat/completions ✅ 正常

这就是"间歇性"的根源: 你在这次对话中,可能前 10 轮都是普通问答 → 一直用 chat/completions → 第 11 轮你上传了一张截图 → Codex 切到 responses → 报 400。

并不是网络不稳定,也不是模型挂了,而是 Codex 换了一个你的网关不认识的"方言"。


六、如何验证?

方法一:抓请求体

在 Codex 日志中搜索发送给模型的实际请求:

bash 复制代码
# Codex 日志位置因版本而异,一般在
~/.codex/logs/

# 或者直接在网关侧看日志
# Agnes / OneAPI 等通常有请求日志,看收到的 body

正常请求的关键特征 :有 messages 字段 触发了 responses 的请求特征 :有 input 字段,或 previous_response_id

方法二:复现触发条件

尝试以下操作顺序,看 400 是否出现:

  1. ✅ 普通对话 → 正常
  2. ✅ 让 AI 读代码 → 正常
  3. ❌ 上传一张图片 → 可能 400
  4. ❌ 打开"推理模式" / "深度思考" → 可能 400
  5. ❌ 恢复一个历史会话 → 可能 400

方法三:看错误信息细节

400 返回的 body 里通常会写明哪个字段不合法:

json 复制代码
{
  "error": {
    "message": "Unknown field: 'input'",
    "type": "invalid_request_error"
  }
}

看到 Unknown field: 'input' → 100% 是 Codex 切到了 responses


七、怎么修?(分角色)

🧑 如果你是 Codex 用户(最终用户)

方案 A(推荐):强制 Codex 使用 OpenAI 兼容模式

在 Codex 配置中明确指定 provider 类型:

jsonc 复制代码
// ~/.codex/config.json 或 settings.json
{
  "provider": "openai-compatible",
  // 不要设为 openai 或 auto
  "baseUrl": "https://your-gateway.com/v1"
}

当 Codex 认到是 openai-compatible 时,它会保守地只使用 chat/completions 协议 ,不会切到 responses

代价thinkingo1/o3 推理模型、previous_response_id 等功能不可用。


方案 B:在网关和 Codex 之间架一个翻译代理

写一个轻量中间件(约 100-300 行代码),把 Codex 发出的 responses 请求翻译成 chat/completions 再发给网关,反向再把响应翻译回去。

但这不是长久之计------随着 responses 生态扩大,翻译层会越来越复杂。


🛠️ 如果你是网关开发者(提供第三方模型接入)

长期方案:在网关上实现 responses 协议

需要改造三个模块:

python 复制代码
# 1. 新增路由
router.add("/v1/responses", handle_responses)

# 2. 输入翻译层
def responses_to_messages(body):
    """把 responses 格式转成内部统一的 messages 格式"""
    if isinstance(body.get("input"), str):
        msgs = [{"role": "user", "content": body["input"]}]
    else:
        msgs = body["input"]  # 数组形式
    
    # 处理 previous_response_id
    if body.get("previous_response_id"):
        history = session_store.get(body["previous_response_id"])
        msgs = history + msgs
    
    # 处理 stream_options
    return {
        "messages": msgs,
        "stream": body.get("stream", False),
        "include_usage": body.get("stream_options", {}).get("include_usage", False)
    }

# 3. 输出翻译层
def messages_to_responses(model_output, response_id):
    """把模型的标准输出转成 responses 格式"""
    return {
        "id": response_id,
        "object": "response",
        "output": [{
            "type": "message",
            "role": "assistant",
            "content": [{
                "type": "output_text",
                "text": model_output["content"]
            }]
        }],
        "usage": model_output.get("usage")
    }

# 4. 状态管理(新增!)
session_store = {}  # response_id → message_history

这意味着你的网关从"单纯转发"变成了"有状态代理"。


八、延伸思考:为什么会有这种割裂?

历史原因

  • chat/completions 是 GPT-3.5 时代的协议(2022 年),那时 LLM 只有"文本聊天"一种场景
  • responses 是 OpenAI 在 2025 年推出的新协议,为了统一聊天、推理、多模态、文件处理等能力
  • 两个协议共存不是设计好的,而是演进过程中留下的历史包袱

生态原因

Model provider(模型厂商)为什么不用自定义协议?

如果 DeepSeek 用自定义协议 如果 DeepSeek 兼容 OpenAI 格式
所有框架得单独适配 一行代码换 model 名就能用
用户接入成本高 生态工具天然兼容
开发者获取慢 门槛极低,拿来就用

自定义协议 ≠ 技术能力强,而是意味着你要重建整个开发者生态。 这是为什么即使各家模型架构差异很大,都优先兼容 OpenAI 格式------网络效应决定了接口标准。

工具责任

那 Codex 在这方面做得怎么样?坦白说,不够好

理想中的 Codex 应该在底层做一个统一的协议抽象层

markdown 复制代码
Codex 功能模块
      ↓
 ┌───────────────┐
 │ 协议抽象层     │ ← 在这里归一化:不管上层用哪种协议,底层都
 │ (Protocol Adapter)│   翻译成统一的内部格式,再按目标端点输出
 └───────┬───────┘
         ↓
 第三方网关 / 官方 API

但现实是 Codex 在上层各个功能模块里直接写死了协议选择,导致:

  1. 零散:协议选择逻辑散布在各模块中,没有中心化管控
  2. 不可预测:用户没法通过配置固定用一种协议
  3. 对第三方不友好:似乎默认了用户会用官方 API

九、总结

bash 复制代码
间歇性 400 的本质

不是:  网络问题 / 模型挂了 / 网关不稳定
而是:  Codex 在 chat/completions 和 responses 之间动态切换
        网关不认识 responses 格式,返回 400

"间歇性"的原因:
        只有特定操作(thinking、文件上传等)会触发切换
        大部分时候你碰不到这些操作,所以时好时坏

给你的建议

你的角色 做什么
普通用户 在 Codex 配置里把 provider 设为 openai-compatible,牺牲高级功能换稳定
网关开发者 在网关上实现 responses 协议翻译层,跟上生态演进
框架使用者 了解这个机制后,排查问题时直接看请求 body 里是 messages 还是 input,两秒定位

最后说一句: 这个问题不是"谁的锅"的问题,而是 AI 工具生态快速演进带来的兼容阵痛chat/completions 还没死,responses 也没完全铺开,两边共存期造成的摩擦,就是我们这些踩坑的人要买单的。

希望这篇文章帮你看清了问题的全貌。下次遇到 400,你至少知道不是玄学,而是 Codex 在你看不到的地方切了个协议

相关推荐
用户5191495848452 小时前
OpenSSL PKCS#12 PBMAC1 堆栈缓冲区溢出漏洞 (CVE-2025-11187) 分析与验证
人工智能·aigc
用户5191495848453 小时前
HP Sound Research SECOMNService 权限提升漏洞利用工具
人工智能·aigc
用户018349301693 小时前
给 AI 智能体能力包一层 BFF,前端只调一个接口
人工智能
这token有力气6 小时前
Function Calling 格式漂移
人工智能
onething3656 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
onething3657 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 6 —— 业务完善 + 会话消息预览
人工智能·后端·全栈
IT_陈寒8 小时前
SpringBoot自动配置的坑,我爬了三天才出来
前端·人工智能·后端
甲维斯9 小时前
笑抽了!DeepSeek识图,豆包完胜了!
人工智能·deepseek
Lei活在当下17 小时前
【AI手记系列-2026/6/18】iSparto & Harness,Caveman 以及AI时代的生存指南
人工智能·llm·openai