☰
OpenAI接口演进:从Completions到Responses的兼容性与迁移实践
2026/10/7 12:57:55 网站建设 项目流程

如果你最近在项目的日志里看到过这么一行英文:[error] unexpected endpoint or method. (post /chat/completions). returning 2,第一反应多半是"我的 base_url 是不是填错了?"我先说结论:这一行报错背后藏着的,其实是 OpenAI 接口规范过去三年里最重要的一次演进——从第一批 Completions、到 Chat Completions、再到 2024 年底开始主推的 Responses,三个端点看着只是名字不同,实际上从入参、出参到底层能力都已经换了代。更巧的是,几乎所有本地开源模型网关都选择了只兼容chat/completions这一代。所以这个错误不仅仅是一个配置问题,它是一张"OpenAI 官方演进路线"和"开源生态兼容现状"在地图上的错位点。

这篇文章我从一次真实的排错经历说起,整理了三代接口的核心差异、Completions 正式退役后的兼容性余波,以及开源项目在适配 Responses 时真正该注意的事情。适合正在做 Agent、工具调用(Function Calling)、或者维护本地模型后端兼容层的读者参考。看完你至少能分清:你的请求到底走的是哪个端点,出错时该去看哪一层。

1. 一次让项目半夜报警的报错:unexpected endpoint or method 的前因后果

1.1 现场还原:请求链路里到底哪一节断了

那是晚上十一点多,同事在群里发了这段报错,说前端一直返回 502。我让他先把服务器日志翻出来,看到完整的一行是:

[error] unexpected endpoint or method. (post /chat/completions). returning 2

注意,这不是 OpenAI 云端返回的 JSON 错误。OpenAI 如果收到不存在的路径,正常情况下会返回 404,并且响应体里有"error": {"message": "..."}这样的结构。这句话更像是某个 SDK 或网关在本地拦截到请求后,发现自己的路由表里没有POST /chat/completions这个组合,于是直接打印了 "unexpected endpoint or method",并且用一个内部错误码 2 通知上层调用方。

所以第一件事不是去改 API Key,也不是去换模型名,而是先确认:这个请求到底被哪一个组件拦截了。在我这次的情况里,有一段老的 Python 服务,从 2023 年就在用openaiSDK 直连云端https://api.openai.com/v1,后来有人为了统一出口,把这个服务的 base_url 改成了一个本地网关的地址。结果就是:本地网关实现了POST /v1/chat/completions,而老代码里还残留着POST /v1/completions的调用路径。网关一看路径不匹配,就相当于"你要的接口我这里没有",于是返回了这个让人摸不着头脑的错误。

另外补充一个容易误导人的小细节:returning 2这个数字不是HTTP 状态码,它更像是一个内部错误标识。在一些网关实现里,错误码 2 表示"端点不存在或方法不允许",和请求被拒绝、鉴权失败是分开标记的。如果你看到它,就别再浪费时间检查授权那一层了。

1.2 排查链路:先分清楚三层问题

现在遇到类似报错,我建议按这个顺序排查:

  1. 确认请求实际发出的 URL 和 HTTP 方法。在 SDK 里打开请求日志,看它请求的是https://api.openai.com/v1/chat/completions、https://api.openai.com/v1/completions,还是https://local-gateway/v1/xxx。这一步能立刻确定问题出在"路径前缀"还是"基础地址"。
  2. 确认是云端还是本地网关。如果 URL 指向 OpenAI 官方,这种"unexpected endpoint"一般不会出现,因为官方对不存在的端点会返回明确的 JSON 错误结构;如果 URL 指向本地网关、内网中间层,那十有八九是网关没有实现对应的路由。
  3. 确认 SDK 版本和代码调用方式是否匹配。同一个 SDK 的不同版本,对base_url的拼接逻辑不同,有些会自动加/v1,有些不会;老版本还可能默认调用/v1/completions,新版又可能默认走/v1/responses。别小看这个细节,我见过不止一个项目因为升级了 SDK、model字段和入参格式没改,结果所有请求全挂。

我那次排错,最后就是改了一行路由转发规则,把/v1/completions老路径也映射到网关内部的chat/completions处理器上,服务立刻就恢复了。整个过程看起来很简单,但真正要命的是中间那段"为什么两边接不上"的思考。

1.3 为什么这个报错最近变得特别常见

这个报错能登上热搜榜,本质原因是"旧教程 + 新版底层"的冲突。网上大量的教程、开源项目 README、甚至企业内部公共组件,还停留在 Chat Completions 这一代,把messages数组、chat/completions当成了唯一正确的调用方式。但另一方面,OpenAI 官方从 2024 年开始高调推行 Responses 接口,新模型也越来越多地围绕 Responses 提供额外能力。两套文档同时存在,新引入的人很容易读一篇旧文章、用一个新 SDK,配置上两边对不上,于是这类奇奇怪怪的 endpoint 报错就开始集中冒出来。

提示:当你看到 "unexpected endpoint or method" 时,先别急着改密钥或换模型。优先打印请求的完整 URL 路径,确认它到底在调哪个端点。大多数情况下,问题不在"凭据",而在"路线"。

2. 三代接口:Completions、Chat Completions、Responses 到底差在哪

2.1 Completions 时代的思维模型:模型就是一个文本续写器

先回到第一代 Completions。它做的事情非常简单:给你一段prompt,模型继续输出后面的文本。没有 role,没有 system,没有工具调用。一个典型的请求长这样:

import openai # 第一代 Completions 的典型写法(现在已经跑不通了) resp = openai.Completion.create( model="text-davinci-003", prompt="Q: 什么是 OpenAPI?\nA:", max_tokens=100 ) print(resp["choices"][0]["text"])

这段代码如果现在拿去跑,大概率会直接报错,因为text-davinci-003本身已经被冻结,/v1/completions端点也在 2025 年正式退役。但它帮助我们理解了为什么后来一定要引入 Chat Completions:纯文本续写很难规范地表达"系统人设"和"多用户对话",所有上下文都得开发者手工拼进 prompt,token 上限很容易被撑爆,更别说做结构化的工具调用了。

2.2 Chat Completions 时代:消息数组的大统一

Chat Completions 的核心理念是把对话建模成一个消息数组。每条消息有role(system/user/assistant),有content,调用方只需要把历史消息和当前提问一起传上去,模型自己理解对话结构。

这看上去只是一个小改动,但它把许多原本要开发者手工处理的逻辑变成了内置能力:

  • system 消息让"模型行为设定"有了标准位置,不用再靠 prompt 前缀拼凑;
  • assistant 历史消息让多轮对话不用再手工拼接长文本;
  • function calling的出现,让模型可以在回复中输出结构化的函数调用请求,应用层再去执行真正的函数。
POST /v1/chat/completions { "model": "gpt-4o-mini", "messages": [ {"role": "system", "content": "你是一个助手。"}, {"role": "user", "content": "帮我查一下北京的天气"} ], "tools": [...] }

正因为接口简单、语义清晰,它迅速成了事实标准。后来 Ollama、vLLM、llama.cpp、LM Studio 在做"OpenAI 兼容接口"时,优先实现的基本都是它,而不是更早的 Completions。可以说,Chat Completions 用一个非常克制的抽象,定义了"对话式 LLM API 该长什么样"。

2.3 Responses API:面向 Agent 的重构

Chat Completions 做普通聊天完全够用,但做 Agent 时,开发者必须在循环里手动保存历史消息、手工解析 function call、执行完再拼接回去。这套流程重复几遍之后,你会发现大部分代码都不是在调模型,而是在"伺候"对话状态。

Responses API 就是为了把这个过程变得更顺。它的核心变化可以这样概括:

  • input替代messages,既可以是消息数组,也可以直接给字符串;
  • instructions从 message 列表里单独拆出来,语义上更接近"系统级操作手册";
  • tools不再是单纯的function,还包含 OpenAI 内置的web_search、code_interpreter、file_search;
  • 返回结构更结构化,output是一个数组,里面明确区分message、function_call、reasoning等类型;
  • 支持previous_response_id,可以在部分场景下省去重复传历史消息的麻烦。

这一代接口的定位很清楚:给 Agent 场景用的。它不是简单地把chat/completions改名,而是把"对话"升级成了"任务"。

2.4 一张表说清楚三者差异

用表格直接对比会更直观:

维度CompletionsChat CompletionsResponses
端点路径/v1/completions(已下线)/v1/chat/completions/v1/responses
顶层入参promptmessagesinput/instructions
角色概念无system/user/assistant有,且 output item 类型更细
工具调用无tools+ 手动解析内置工具类型,返回更结构化
上下文管理开发者自己拼接开发者维护 messagesprevious_response_id等方式
典型应用文本补全对话、Function CallingAgent、多步工具调用、内置查询

这张表是我个人整理的经验版,不完全等于官方文档口径,但用来做迁移判断足够了:如果只是做一个聊天机器人,Chat Completions 完全够用;如果你正在写 Agent 框架,Responses 的价值才会体现出来。

3. 2025年大限已至:Completions 的正式退役与兼容性余波

3.1 官方时间线:退役是怎么一步步来的

回到"从 Completions 到 Responses"这条时间线。官方其实很早就开始铺垫了:2023 年 3 月推出 ChatGPT API,也就是chat/completions端点;此后官方明确建议新项目一律走 Chat Completions,并陆陆续续冻结了一批第一代文本模型,包括text-davinci-003、text-curie-001等。

到了 2024 年,官方在发布公告中正式提出要弃用早期的 Completions 端点,给了开发者一段过渡期。最终在 2025 年年中之前,/v1/completions这个老端点被正式关闭。如果你现在再去请求它,基本会得到 404 或明确的模型不可用错误。

这次下线主要影响的其实是三类人:

  1. 还在用旧模型名(如text-davinci-003)的项目,连模型通道都不存在了;
  2. 还在请求/v1/completions的老 SDK 调用,比如第一代openai.Completion.create;
  3. 企业私有化部署的一些老网关,内部路由还硬编码了这个路径。

如果你一直在使用chat/completions和gpt-4o-mini这类模型,这次下线对你几乎没有任何影响。

3.2 关停之后,谁还在踩雷

实际踩雷的人比想象中多。最常见的情况是翻出 2023 年的项目模板,里面恰好用了Completion.create而不是ChatCompletion.create。当时两种写法共存,后来 SDK 升级,旧的Completion类被移除,项目一跑就报错。

另一些情况发生在"本地网关的兼容区"里。有些网关为了照顾老项目,会在路由层保留一个/v1/completions到内部chat/completions处理器的转换,但一旦网关没做这个映射,就会暴露类似的 "unexpected endpoint or method" 错误。更麻烦的是,这类网关往往自己对 OpenAI 新端点的适配也是滞后状态,所以新老代码混在一起时,排查的复杂度会成倍增加。

3.3 Responses 会不会淘汰 Chat Completions

这是很多人关心的问题,我的判断是:短期不会。官方目前明确让两者并存,Chat Completions 依然是一个被广泛支持的基础端点。真正需要注意的地方是,一些新模型能力(比如内置的 web search、更完整的推理字段)可能只出现在 Responses 里,而 Chat Completions 不会第一时间获得。

对开源界和中间层服务来说,这就意味着一个跷跷板:如果只做 Chat Completions,可能错过新能力;如果只做 Responses,又和现有生态脱节。两端都要维护,又确实有成本。

提示:如果你在维护开源兼容层,请把"Chat Completions 仍会长期存在"作为默认假设,但把 Responses 当作一个并行端点持续跟踪,而不是等到它完全替代了再行动。

4. 开源兼容的真相:别把"长得很像"当成"就是"

4.1 为什么本地推理引擎全都选了 Chat Completions

本地推理引擎需要一套通用、好学的 HTTP 协议来暴露能力。Chat Completions 的messages结构足够表达绝大部分任务,返回结构也简单:choices[0].message.content直接拿字符串。相比之下,Completions 太老,Responses 又太复杂——后者不仅有instructions、input、tools的多种组合,响应里的output还包含多种 item 类型,对引擎作者来说光是测试矩阵就很头疼。

所以你会发现一个现象:几乎每一个本地引擎都宣布自己"兼容 OpenAI API",但仔细看文档,底下的示例基本都是POST /v1/chat/completions。这就是开源生态集体投票的结果。

4.2 兼容也有三个层次

"兼容"这个词其实可以拆成三层:

  • 路径层:只实现了POST /v1/chat/completions、GET /v1/models;
  • 参数层:能传temperature、top_p、max_tokens等采样参数;
  • 语义层:system 提示、角色切换、工具调用、流式事件顺序都和云端一致。

很多网关只做到第二层。我遇到过最典型的情况是:本地引擎跑普通对话完全正常,一旦传入tools字段就报 400。原因是引擎虽然开放了/v1/chat/completions,但并没有真正实现 function calling 的语义。这时你其实不是在调 OpenAI,你只是"碰巧"用了一个长得像 OpenAI 的接口。

所以,在选型前一定先看两件事:它对tools的支持是否完整?它对流式tool_calls增量事件的处理是否符合官方行为?只看"支持 Chat Completions"这句话远远不够。

4.3 unexpected endpoint 背后其实是两套标准的错位

回到标题想表达的核心矛盾。从 Completions 到 Responses 的演进过程中,不同项目的代码习惯可能停在任何一个版本:

代码习惯网关实现结果
老的/v1/completions只实现/v1/chat/completionsunexpected endpoint
SDK 默认走/v1/responses只实现/v1/chat/completions404 / unexpected endpoint
SDK 走/v1/chat/completions网关完整实现正常

这个表格解释了为什么最近半年unexpected endpoint和chat completions会同时成为热搜:大家照着新教程改代码、用各类中间层转发,结果反复卡在"路径对不上"这一层。本质上不是某一个人的配置水平问题,而是整个生态在迁移期的必然摩擦。

4.4 开源项目适配 Responses 的难度被低估了

我不认为开源项目适配 Responses 只是多写一个路由那么简单。Responses 的响应里包含了reasoning、function_call、内置工具执行结果等不同类型的 output item,如果要完整支持,还需要把previous_response_id这类状态管理映射到底层模型。对于只做单次补全的引擎来说,它需要把 Responses 请求翻译成内部格式,再把响应重新拼装成官方结构,这个过程中不可避免地会丢东西。

比如官方内置的web_search工具,背后是一套完整的检索服务;开源网关无法凭空模拟"搜索出结果并组织成引用"的过程。所以我的判断是:短期内开源生态的主流仍是 Chat Completions。除非 Agent 框架们大面积转向 Responses,否则开源引擎不会优先投入资源补齐这块适配。

这就是标题里"开源兼容真相"最需要被理解的部分:开源兼容是成本驱动的,哪个标准简单、用户多,生态就聚在哪边。

4.5 但我仍然建议你关注 Responses

不是让你立刻把线上代码全部切过去,而是建议在写新项目时,把 Responses 和 Chat Completions 都纳入选型视野。特别是当你明显需要某类新能力——比如内置搜索、更完整的工具输出时,先查一下这个能力是不是只在 Responses 里提供。如果是,就直接以 Responses 为基准写代码,再做一个很薄的转换层把消息结构翻译一下。这样做的好处是:主逻辑不受端点切换影响,未来迁移的成本被压缩在一个函数里。

5. 从 Chat Completions 迁到 Responses:动手改一次就知道的区别

5.1 入参结构的差异:messages 变成 input + instructions

最直接的差异在请求体。Chat Completions 里你需要把所有历史消息放进messages,system 设定也混在其中;Responses 里,instructions被单独拆出来,input可以是字符串,也可以是消息数组。以最简单的场景为例:

from openai import OpenAI client = OpenAI() # Chat Completions 风格 resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是客服助手。"}, {"role": "user", "content": "我的订单为什么还没发货?"} ], ) print(resp.choices[0].message.content) # Responses 风格 resp = client.responses.create( model="gpt-4o-mini", instructions="你是客服助手。", input="我的订单为什么还没发货?", ) print(resp.output_text)

从可读性来说,Responses 其实更简洁,尤其适合"一次提问、一次回答"的场景。如果业务需要维护多轮消息,input也可以传消息数组,迁移成本并不高。关键是把原来塞在messages里的 system 内容拆到instructions,这个思维转换要早点完成。

5.2 工具调用:省掉最恶心的一段解析代码

这是我最喜欢 Responses 的地方。在 Chat Completions 里,Function Calling 的返回长这样:

{ "choices": [{ "message": { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_abc", "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } }] } }] }

注意arguments是一段 JSON 字符串,你不仅要二次解析,还要小心它被截断或转义出问题。Responses 的返回结构则清楚得多:

{ "output": [ { "type": "function_call", "name": "get_weather", "arguments": {"city": "北京"}, "call_id": "call_abc" } ] }

差别显而易见:前者需要手写一段"从字符串里抠 JSON"的胶水代码,后者从一开始就是结构化对象。而且output是数组,多个并行工具调用可以直接平铺在同一个数组里,不用再自己拼接。

5.3 流式输出:事件体系完全重做

流式输出是迁移中工作量最大的地方。Chat Completions 的流式事件以choices[0].delta.content为主,你需要在客户端做增量拼接。Responses 的事件体系更细,常见的如response.output_text.delta一类事件,每一段的语义更明确。

如果你还在手写 SSE 解析,迁移到 Responses 时建议直接用官方 SDK 的stream模式,让 SDK 替你处理事件。不要试图只改 URL 就保留原来的解析逻辑,那样会踩很多隐蔽的坑。

5.4 最小迁移清单

给你一份可以照着执行的清单:

  1. 替换 SDK 调用方法:chat.completions.create→responses.create;
  2. 拆分 messages:system 内容 →instructions,其余内容 →input;
  3. 内容读取:choices[0].message.content→output_text或output数组;
  4. 工具调用:不再手动解析tool_calls,改为读取output中type=function_call的条目;
  5. 流式处理:重写基于delta的解析 → 基于事件类型的解析;
  6. 回归测试:重点覆盖工具调用、多轮对话、流式三种场景。

6. 迁移适配中的常见坑与个人建议

6.1 升级 SDK 不等于迁移完成

很多团队以为把openai包升到最新版就是迁移了。实际上新版 SDK 同时提供chat.completions和responses两个属性,你调用方法的名字没换,就不会真的走到 Responses 逻辑。真正的迁移是代码层面的调用方式切换,而不只是依赖版本号变了。

6.2 中间层的选择决定你会不会踩 unexpected endpoint

无论你用的是云厂商托管网关、开源网关、还是自己写的一层转发,都要先确认它到底实现了多少个端点。如果网关只认/v1/chat/completions,而新版 SDK 里你用了responses.create,网关很可能不知道/v1/responses是什么,于是返回 404,或者就是你开头看到的 unexpected endpoint。

如果不确定,最土但最有效的办法是打一次原始 curl,直接看到响应内容。不要依赖"文档上说支持"这种话。

6.3 开源框架的适配进度:现在还不用焦虑

LangChain、LlamaIndex 这类框架对 Responses 的集成正在逐步完善,但很多底层封装默认仍然把ChatOpenAI映射到chat.completions。如果你要跑的是需要新能力的场景,直接在代码里调官方 SDK 会更可控,别让框架的抽象层再遮一层纱。

这也意味着,如果你维护的是内部组件,最好自己做一层薄薄的 Adapter,把messages -> input、instructions之类的转换写成纯函数。这样上层业务不关心底层走的是哪个端点,未来的迁移成本就被限制在一个文件里。

6.4 我的实际建议:两条腿走路

最后说说我在项目里的处理方式:

  • 线上稳定的对话和工具调用,继续留在 Chat Completions,不为了追新而冒险;
  • 新模型的新能力(内置搜索、富工具调用)单独封装一个服务,走 Responses 端点;
  • 在团队内部维护一份"端点能力矩阵"文档,谁用的哪个端点、支持哪些参数,一目了然;
  • 不要把 API Key 硬编码进代码仓库,也不要用别人的 Key 做实验。安全习惯比任何接口迁移都重要。

另外在动手迁移前,先在本地写一个对比脚本,把同样的对话分别用chat.completions.create和responses.create调一次,观察返回结构的差异。这个方法虽然老,但对团队里没接触过 Responses 的同事来说,比看十页文档都管用。

我自己在迁移过程中最大的体会是:接口演进从来不只是 URL 变一变,背后是一整套"对话管理心智"的替换。Completions 时代靠拼 prompt,Chat Completions 时代靠 messages 数组,Responses 时代靠输入输出对象。每走一步,开发者手写的胶水代码都在减少,但前提是你得理解每个端点设计的初衷。

一句话收束:兼容是有时效的,架构上多点冗余,总比被热搜报错追着跑要好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询