
最近折腾 OpenAI 的接口遇到一个很典型的报错[Error] Unexpected endpoint or method. (POST /chat/completions). Returning 2.当时第一反应是网关问题结果查下来发现是我本地装的新版 SDK 默认把请求路由到了/responses而我指向的兼容服务只实现了老版的/chat/completions。这事看起来是个小坑背后其实藏着一个值得聊清楚的话题OpenAI 的接口规范到底在怎么演进从 Completions 到 Responses 到底改了什么那些号称“开源兼容 OpenAI 接口”的方案到底兼容的是哪一层。这条演进线不只是 OpenAI 自己的事。现在大量开源框架、本地推理引擎、API 网关都以“兼容 OpenAI 规范”作为卖点但很多人没意识到OpenAI 规范本身是个移动靶。你抄作业的时候作业本已经换版本了。这篇就把我实际踩过的坑、翻过的源码、迁移时整理的参数对照以及关于开源兼容的一些判断一次说清楚。1. 从 Completions 到 Responses接口演进背后的设计逻辑1.1 补全接口的朴素年代OpenAI 最早对外开放的接口其实特别简单就是 Completions也就是常说的POST /v1/completions。那个年代的代表模型是text-davinci-003请求体里塞一个prompt字符串模型把它当成“前半句话”接着往下写。resp openai.Completion.create( modeltext-davinci-003, prompt写一首关于秋天的短诗, max_tokens100 )这个接口的设计思路是文本补全不是对话。你想做多轮聊天得自己把历史对话拼成一个长字符串塞进prompt中间手工加分隔符非常别扭。到了 2023 年OpenAI 推出 Chat Completions也就是POST /v1/chat/completions消息被结构化成了messages数组每个元素带role和contentsystem、user、assistant三种角色各司其职。这一步看着只是参数变了个形实际上是产品形态从“文本续写”转向了“任务型对话”。从技术实现角度看messages数组本质上是把原先由开发者手工拼接的历史上下文变成了一个结构化的输入协议。模型侧要做的事情其实差不多——把消息渲染成提示词再预测下一个 token但 API 的用户体验完全不同了角色分离让 system prompt、用户输入、模型历史回复都有了明确的边界多轮对话和 few-shot 示例的维护成本大幅下降。1.2 Chat Completions 的隐藏痛点Chat Completions 火了之后OpenAI 在这个接口上不断做加法加了函数调用Function Calling、结构化输出JSON Mode、视觉输入、流式返回优化。接口活了很久但使用过程中能明显感觉到一些设计上的别扭。最典型的是工具调用的状态管理。在 Chat Completions 里一次带函数调用的完整交互是这样的第一轮请求带上tools模型返回一个tool_calls里面包含要调用的函数名和参数开发者执行本地函数把结果以role: tool的消息追加到messages里然后再发一次请求模型才给出最终文本。整个过程完全靠开发者自己维护对话历史、自己衔接多轮工具调用。如果模型连续调用多个工具代码里就得反复拼接 messages、反复发起请求非常容易出错。还有一个让我印象深刻的坑messages里如果出现role: tool的消息必须严格带上对应的tool_call_id否则直接 400。而某些兼容服务对这个校验做得时严时松导致同一个 SDK、同一套代码切换服务商后行为完全不一致。这说明 Chat Completions 的“状态机”其实是靠开发者在客户端手工维护的协议层并没有提供官方状态管理。1.3 为什么还会有 Responses API到了 2024 年OpenAI 又拿出一个叫 Responses API 的新接口也就是POST /v1/responses。很多人以为这是 Chat Completions 的替代品看到新接口第一反应都是“又来折腾人”。但仔细看设计它更像是把 Chat Completions 和以前那个维护成本极高的 Assistants API 里的核心能力合并成了一个更统一的接口。Responses API 的核心变化是一次请求拿回一个response对象这个对象里的output数组可以包含多种类型的输出项包括文本、函数调用、网络搜索引用等。工具调用的生命周期被协议层接管了模型说要调用函数你执行完把结果填回同一个 response 上下文里继续整个过程有了明确的response_id作为状态锚点不再需要你手动把整个 messages 历史一遍遍重传。这才是 Responses 和 Chat Completions 最本质的区别前者把“会话状态”从开发者手里收回到 API 层后者则是一个无状态的请求-响应模型。这个变化对于简单对话场景没啥感觉但一涉及多轮工具调用、长对话续写、复杂 Agent 编排差别就非常明显了。2. Responses API 核心设计拆解与开源兼容的真相2.1 一个 response 对象解决多个输出先看一个最简单的 Responses 请求长什么样。from openai import OpenAI client OpenAI(api_keyyour-key) resp client.responses.create( modelgpt-5, input用一句话介绍 Responses API, ) print(resp.output_text)注意input可以是字符串也可以直接传消息列表。响应对象里output是个数组里面每一项都有自己明确的type。文本输出的类型是message内容在content下面函数调用输出的类型是function_call参数在arguments里。API 还很贴心地提供了一个output_text便捷属性把你需要从多个输出项里手动提取文本的脏活累活省掉了。对比一下 Chat Completions 时代要兼容多个工具同时返回、又要拿文本得遍历choices[0].message.tool_calls和choices[0].message.content然后自己组装。Responses API 把多输出的处理逻辑统一了写起 Agent 编排来确实清爽很多。2.2 状态锚点与续跑机制Responses API 里最有含金量的设计是previous_response_id。它让你可以把一个多轮工具的上下文串在同一个会话里不需要每次把整个消息历史重新发给服务端。举个例子用户问“帮我查一下今天北京天气然后根据天气推荐穿搭”。Agent 先触发天气查询函数拿到结果后你要让模型继续生成推荐。Chat Completions 的做法是把工具结果追加到 messages 再发一次完整的请求Responses 的做法是传入previous_response_id上一步的response_id再补上工具结果。服务端记住了上下文请求体的体积大幅下降长会话场景下的 token 消耗和延迟都会好一些。这里有个容易忽略的细节如果你使用truncation参数可以控制上下文超长时的截断策略。过去在 Chat Completions 里上下文超长只能自己手动裁剪 messages裁坏了还会导致引用错乱。Responses 至少给了协议层的解决办法虽然实际效果还需要看具体模型的表现但设计上确实是朝“更可控”的方向走了。2.3 开源兼容的真相大家都在兼容哪一层现在回到很多人关心的开源兼容问题。市面上遍地都是“兼容 OpenAI API”的开源项目和推理服务比如 vLLM、llama.cpp、Ollama、LiteLLM、One API、New API 等。但“兼容 OpenAI”这句话含糊得不能再含糊。拆开看至少有三层含义第一是 SDK 兼容层。你使用openai这个 Python/Node 包把base_url改成某个本地服务的地址同样一套调用代码就能跑通。这是大多数本地推理服务提供的兼容方式vLLM 和 llama.cpp 的 OpenAI 兼容端点就是这一类。第二是端点兼容层。服务端实现了/v1/chat/completions这个路径并尽量按照 OpenAI 的请求和响应 schema 返回数据。这部分很多开源项目都做到了但未必实现了/v1/responses。第三是行为兼容层。不仅路径和 schema 对得上流式事件、状态管理、错误格式也要一致。这一层能做到的项目就不多了尤其是 Responses API 刚出来那会儿大多数开源网关只同步了/chat/completions/responses要么没有要么实现得非常粗糙。所以如果你把新版 SDK 的base_url指向一个只做了 Chat Completions 兼容的开源网关然后调用client.responses.create()网关不认识/responses路径就会直接甩一句Unexpected endpoint or method。这不是代码写错了而是你踩到了“兼容版本落后于 SDK 版本”的断层。2.4 为什么开源项目跟不上 OpenAI 的换代速度这里得替开源项目说句公道话。OpenAI 的接口规范迭代很快尤其现在官方已经明确表示 Responses API 是未来方向但 Chat Completions 也还在维护期、没被废弃。两边都要支持意味着网关项目要维护两套 schema 映射、两套流式事件工作量是双份的。更深层的问题是Responses API 的很多能力比如response_id状态管理、工具调用的多物品输出、web_search这类内置工具是跟 OpenAI 的后端服务强绑定的。本地推理引擎靠一己之力在协议层模拟这些行为成本极高。本地模型的推理引擎只需要“把 prompt 跑完把 token 流式吐出来”你要它同时维护会话状态、处理工具调用生命周期、输出结构化的多个 output item这已经不是推理引擎的职责范围了而是把 Agent 框架的工作下沉到了协议层。所以我的结论是在可预见的未来开源生态的主流兼容层仍然会停留在 Chat Completions 上。Responses API 的完整兼容大概率只会出现在商业 API 网关或者专门做 Agent 基础设施的项目里。普通开发者面向开源本地模型时继续使用 Chat Completions 反而是更稳妥的选择。3. 从 Chat Completions 迁移到 Responses 的实操指南3.1 最小改造先看懂请求体的变化如果确有必要迁移建议从最小改动开始。先看一个最简单的对话场景在两种接口下的写法差异。# Chat Completions 方式 resp client.chat.completions.create( modelgpt-5, messages[ {role: system, content: 你是一个写作助手。}, {role: user, content: 帮我写一段产品简介。} ], max_tokens500 ) text resp.choices[0].message.content # Responses 方式 resp client.responses.create( modelgpt-5, input[ {role: system, content: 你是一个写作助手。}, {role: user, content: 帮我写一段产品简介。} ], max_output_tokens500 ) text resp.output_text肉眼可见的主要变化是messages变成了inputmax_tokens推荐换成max_output_tokens返回的文本不再从choices[0].message.content里取而是直接用output_text。如果只是简单对话改到这一步就足够跑通了。但要注意一个容易踩的细节Responses API 对input的校验在某些版本里比 Chat Completions 更严格尤其是工具调用相关的历史消息。你从 Chat 迁移过来时如果 messages 里还残留着历史工具调用记录建议先清掉再测试否则可能遇到意料之外的 400 报错。3.2 流式响应的差异可能是最大的改造点如果你在产品里用了流式输出迁移的工作量会比想象中大。两种接口的流式事件结构完全不一样。Chat Completions 流式解析的典型写法stream client.chat.completions.create( modelgpt-5, messages[{role: user, content: 讲个故事}], streamTrue ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end)Responses API 流式解析的写法with client.responses.stream( modelgpt-5, input讲个故事, ) as stream: for event in stream: if event.type response.output_text.delta: print(event.delta, end)最大的区别是Chat Completions 一切都在chunk.choices[0].delta里而 Responses 流式会产生多种事件类型包括response.output_text.delta、response.function_call_arguments.delta、response.completed等。这意味着你的流式解析逻辑不能再只盯着一个字段而要根据事件类型分发处理。我建议在迁移时先把事件类型打印出来看一遍跑通一次真实的工具调用看response.output_text.delta和response.function_call_arguments.delta是怎么穿插出现的再动手写解析层。这样能少走很多弯路。3.3 参数映射速查表迁移时最实用的是一张参数对照表。我根据自己的实践和官方文档整理了下面这份不敢说覆盖全部参数但日常开发高频用到的都在里面。场景Chat CompletionsResponses API模型名modelmodel同名用户消息/历史messagesinput结构兼容但校验更严最大生成 tokenmax_tokensmax_output_tokens旧参数仍可用但建议换新采样温度temperaturetemperature同名随机种子seedseed同名停止词stopstop同名工具定义toolstools结构基本相同强制工具选择tool_choicetool_choice同名结构化输出response_formattext.format需调整嵌套结构流式开关streamTruestreamTrue事件结构不同用量统计stream_options中的include_usage仍支持但事件时机不同会话续写手动拼接messagesprevious_response_id配合工具结果单次请求多输出不支持一个响应只对应一个角色output数组原生支持多输出项需要特别提醒的是结构化输出。Chat Completions 里你传的是{response_format: {type: json_schema, json_schema: {...}}}Responses API 里变成了{text: {format: {type: json_schema, name: my_schema, schema: {...}}}}这个嵌套层级的变化很容易被忽略一旦写错模型不会报错但会退化成普通文本输出。如果你依赖结构化输出迁移时一定要重点测试这一块。4. 迁移踩坑与排查实录4.1Unexpected endpoint or method的两种常见场景开篇那个报错我再展开说一下。POST /chat/completions返回Unexpected endpoint or method本质上说明你的请求打到了一个不认识这个路径的服务上。我遇到过两种典型场景第一种是你把新版 SDK 的base_url指到了只支持 Chat Completions 的本地推理服务同时代码里调用了client.responses.create()。SDK 拼出的路径是/v1/responses而服务端根本没实现这个路由于是返回Unexpected endpoint or method。这种情况的排查方法是先确认 SDK 的base_url指向的服务到底暴露了哪些路径直接curl /v1/responses看返回。第二种是某些兼容网关自己配置错误把/chat/completions的请求转发到了不支持该方法的后端。这种情况多在网关日志里能看到 405 或类似的原始错误。我的建议是排查时先把 SDK 层剥离掉用 curl 直接打目标端点看是路径不存在、方法不被允许还是鉴权失败。4.2 API Key 与鉴权相关问题的排查顺序迁移到 Responses 后遇到 401 或 403很多人第一反应是 API Key 问题实际上我更建议按这个顺序排查先确认 Authorization 头有没有带对。新版 SDK 通常会用环境变量OPENAI_API_KEY作为默认 key但如果你在代码里同时初始化了多个 client 实例偶尔会出现 key 覆盖的问题。可以在代码里显式打印请求头或者在网关日志里看收到的 Authorization 头是否符合预期。再确认网关是不是把 Authorization 头正常透传了。有的兼容层会自己消费掉这个头转发到上游时反而丢了导致上游报 401。这种问题代码层面看不出任何异常只能在网关日志和上游日志之间做比对。最后才是确认 key 本身有没有过期、余额是否充足、模型权限是否覆盖。我自己踩过的坑是旧项目里用了一个很早生成的 key模型权限默认只在旧的模型列表里换到新模型后返回 403但换回gpt-4o就正常。这类问题在 OpenAI 官方接口里不常见但在第三方兼容服务里出现频率不低。4.3 Codex CLI 安装依赖缺失的排查另外一个近期很多人遇到的问题安装 Codex CLI 时报missing optional dependency openai/codex-win32-x64。这其实是 npm 包机制导致的典型问题。Codex CLI 的二进制依赖是通过optionalDependencies声明的npm 在安装时如果检测到当前平台的二进制包安装失败为了避免整个安装失败会静默跳过。解决方式很简单手动补装对应平台的包# Windows x64 平台 npm install openai/codex-win32-x64 --save-optional # macOS 根据芯片选 arm64 或 x64 版本 npm install openai/codex-darwin-arm64 --save-optional但这个问题的根因往往不只是缺包。有时候是没装 Rust 工具链导致 Codex CLI 某些功能在本地编译时失败有时候是 npm 版本太老对 optionalDependencies 的平台判断有问题。我建议安装前先看 Node 版本最好保持在 LTS 版本然后把 node_modules 整个删掉重新 install。不要把时间浪费在零散的报错排查上。4.4 关于接口迁移的几个现实建议最后分享几个我自己这段时间实操下来的判断不一定对但都是踩过坑换来的。第一不要因为换了 Responses API 就把所有代码一次性迁移。Chat Completions 短期内不会消失官方也明确表示会保留兼容。我更推荐的做法是新项目、新功能优先用 Responses老接口暂时不动让团队有个过渡期。第二面向开源本地模型的项目先别急着迁。本地推理引擎的兼容层大多停留在 Chat Completions你强行在 SDK 层用 Responses最终还是要靠一个翻译层把它映射回 Chat Completions。这种多一层转换的架构出了 bug 特别难排查。第三无论用哪个接口都要尽早把“流式事件类型”的日志打出来。接口换代不可怕可怕的是你的解析层只认一种结构。我在迁移流式逻辑时就发现把事件类型打印出来看比盲改代码快十倍。写到这里从 Completions 到 Responses 的技术脉络基本梳理清楚了。如果你只是做个简单对话产品Chat Completions 还能继续用如果你开始认真做 Agent、做多工具编排Responses API 那套状态管理确实值得早点上手。至于开源兼容这一块我的建议永远是看文档不如看源码看源码不如直接 curl 打一下目标端点眼见为实。