ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从 Responses API 到 Chat Completions:TaoToken 模型网关的协议适配设计复盘

从 Responses API 到 Chat Completions:TaoToken 模型网关的协议适配设计复盘 1. 为什么 Responses API 与 Chat Completions 之间需要一层协议适配如果你最近在折腾 Codex、Cline 或者自研 Agent大概率会遇到一个很别扭的局面客户端只认 OpenAI 的 Responses API而你手头能用的模型——DeepSeek、智谱、MiniMax、Xiaomi Mimo——清一色只提供 Chat Completions 风格的接口。表面上看把/v1/responses的请求体改个 URL 转发到/chat/completions就完事了但真正跑起来你会发现工具调用、流式事件、结构化输出、会话链这些东西根本对不上。我自己第一次做这种转发时就是用一个几十行的反向代理硬怼结果普通聊天没问题一上 Agent 场景就崩工具调用返回的tool_calls结构在 Responses 侧解析不出来流式响应客户端收到一堆无法识别的事件previous_response_id直接丢失导致多轮上下文断裂。后来才意识到这不是转发问题而是协议适配问题。这篇文章聚焦的就是模型网关在 Responses API 与 Chat Completions 两套协议之间的适配层设计。我会把可复制的协议转换配置、请求映射规则以及用 curl 验证两种协议返回一致性的具体步骤都交付出来。适合正在做 Agent 接入、Codex 自定义 provider、或者自研模型网关的开发者。核心检索词就三个Responses API、Chat Completions、模型网关围绕它们把适配层的边界讲清楚。先说清楚两套协议的本质差异。Chat Completions 是消息列表进、消息对象出的扁平模型一次请求对应一次补全工具调用以message.tool_calls数组形式挂在 assistant 消息上。Responses API 则是输入项进、输出项出的事件化模型它把一次交互拆成 response、output item、content part 三层结构工具调用是独立的 output item流式响应有完整的事件生命周期response.created、response.output_item.added、response.output_text.delta等。这个结构差异决定了你不能做字段级映射必须做语义级重建。举个最典型的例子Chat Completions 里工具调用的参数是流式拼接的字符串片段而 Responses 要求工具调用作为一个完整的 output item 出现参数必须是合法 JSON。如果网关只是把 delta 原样转发客户端拿到的就是半截 JSON解析必然失败。所以适配层的核心职责可以归纳成三件事请求归一化把 Responses input 转成 provider-neutral messages、能力规划根据 provider 能力决定支持、降级还是拒绝、响应重建把上游返回重建成 Responses 结构。这三件事如果塞进客户端客户端会越来越重如果每个 provider 各写一套网关会越来越散。正确的做法是收敛到一个共享的 bridge 内核里provider 只声明差异。2. TaoToken 模型网关前置准备Base URL、Key 与模型别名在动手写适配层之前得先把网关的接入底座搭好。这里我用 TaoToken 作为上游模型网关来演示因为它同时提供 Responses API 兼容端点和 Chat Completions 端点正好可以用来验证两种协议的一致性。你需要准备三样东西Base URL、API Key、Model ID这三件套在后面的配置里会反复出现。Base URL 分两个官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 端点统一用https://taotoken.net/api注意 API 地址不加 UTM 参数。API Key 在控制台的 API Keys 页面生成路径是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。生成后立刻复制保存页面刷新后就看不到了。模型别名这块是适配层设计里最容易被低估的部分。Responses API 客户端比如 Codex通常写死了一个模型名比如gpt-5.5但你的上游实际可能是deepseek-v4-pro或者glm-5.1。如果每次换模型都要改客户端配置团队协作会非常痛苦。所以网关必须支持本地别名映射把客户端的模型名解析成provider/model的组合。我建议在网关配置文件里维护一张别名表格式大致是这样server: port: 5678 host: 0.0.0.0 default_provider: deepseek models: aliases: gpt-5.5: deepseek/deepseek-v4-pro gpt-5.4-mini: zhipu/glm-5.1 gpt-5.3-codex: deepseek/deepseek-v4-pro这里要强调一点这些别名是本地路由策略不代表与 OpenAI 原模型能力等价。它解决的是客户端稳定性问题——Codex 侧只知道自己用gpt-5.5网关负责把它路由到真实模型。以后切换 provider 只改网关配置客户端一行不动。如果你用的是 Codex还需要在~/.codex/config.toml里声明自定义 provider。这个文件是 Codex 的认证与 provider 配置入口三件套要写全model gpt-5.5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api/v1 wire_api responses requires_openai_auth false supports_websockets false注意wire_api这里填responses表示 Codex 会用 Responses API 协议发请求。如果你的网关只支持 Chat Completions这里要改成chat但那样就失去了本文讨论的适配层意义。requires_openai_auth false是因为我们用的是自定义 Key不走 OpenAI 官方认证流程。Key 的注入方式有两种环境变量或者配置文件。生产环境建议用环境变量避免 Key 进版本库export TAOTOKEN_API_KEYsk-xxxxxxxxxxxxxxxx然后在网关配置里引用${TAOTOKEN_API_KEY}。如果你在本地快速验证也可以直接写进配置但记得把配置文件加进.gitignore。前置准备做完后你应该能拿到一个可用的 Base URL、一个有效的 Key、以及一张清晰的模型别名表。接下来才是真正的适配层配置。3. 可复制的协议转换配置与请求映射规则这一节是全文的核心我会给出可直接复制的 JSON/TOML 配置片段以及 Responses 到 Chat Completions 的字段映射规则。先说配置文件再说映射逻辑。网关的完整配置建议拆成三块server、providers、bridge。server 管监听和路由providers 管上游端点与认证bridge 管协议转换策略。下面是一份可以直接用的gateway.json{ server: { port: 5678, host: 0.0.0.0, default_provider: taotoken }, providers: { taotoken: { base_url: https://taotoken.net/api/v1, api_key_env: TAOTOKEN_API_KEY, wire_api: chat_completions, capabilities: { tool_choice: [auto, none, required, function], response_format: [text, json_object], reasoning: boolean, cached_tokens: true } } }, bridge: { strict_schema_fallback: json_object, tool_id_recovery: true, stream_state_machine: true, session_store: sqlite, trace: { enabled: true, capture_payload: false } } }这份配置里几个关键字段值得展开。wire_api填chat_completions表示上游是 Chat Completions 协议网关需要做 Responses 到 Chat 的转换。capabilities是 provider 的能力声明直接参与 bridge 的兼容性规划——比如tool_choice只支持auto和none的 provider遇到客户端传required时网关就要决定降级还是拒绝。bridge.strict_schema_fallback控制结构化输出的降级策略。当客户端要求严格 JSON Schema 但 provider 只支持json_object时网关会降级为json_object并在 prompt 前言注入格式指令最终输出阶段做 JSON 语法检查。这不是完整 Schema 校验但至少保证降级行为是显式的。接下来是请求映射规则这是适配层最容易出错的地方。我把它整理成一张对照表Responses API 字段Chat Completions 字段转换规则input(string)messages[].content包装成 user 消息input(array)messages[]按 role 展开为多条消息previous_response_id无直接对应从 session store 恢复历史前置到 messagestools[].functiontools[].function结构基本一致注意strict字段降级tool_choicetool_choice按 provider capability 规划response_format.json_schemaresponse_format.json_object降级并注入 schema 指令reasoning.effortreasoning(boolean)按 provider 能力映射streamstream触发流式状态机反向映射Chat 到 Responses更复杂因为要重建 output item 结构。核心规则是assistant 的文本内容变成一个message类型的 output itemtool_calls数组里每个调用变成一个function_call类型的 output itemfinish_reason映射到 response 的status字段。流式响应的映射是状态机的活。Chat Completions 的 SSE 是data: {choices:[{delta:{content:...}}]}这种扁平 delta而 Responses 要求按事件生命周期输出。状态机至少要处理这几个转换Chat delta.content 首次出现 - response.output_item.added (message) Chat delta.content 持续 - response.output_text.delta Chat delta.tool_calls 出现 - response.output_item.added (function_call) Chat finish_reasonstop - response.output_item.done response.completed Chat finish_reasontool_calls - response.output_item.done (function_call) 上游错误 - response.failed这里有个坑我踩过工具调用的参数在 Chat 流式里是分片拼接的delta.tool_calls[0].function.arguments每次只给几个字符。状态机必须缓存这些片段等finish_reason到达后再拼成完整 JSON然后作为一个完整的function_calloutput item 发出。如果边收边发客户端拿到的就是半截 JSON。会话链的处理也要单独说。previous_response_id不是当前会话 ID而是父指针。每次 response 指向上一个 response形成一条链。网关收到带previous_response_id的请求时必须先根据这个 ID 从 session store 恢复历史转成 provider-neutral messages再和当前 input 拼接。这个恢复动作必须发生在构建 provider request 之前否则上下文就断了。session store 里保存的应该是 API-shaped snapshot而不是某个 provider 的私有 message 格式。这样后续切换 provider 或调整 bridge 策略时历史上下文还能复用。如果存的是 provider-specific 格式换 provider 就得迁移数据非常麻烦。4. 用 curl 验证两种协议返回一致性配置写完后必须验证 Responses 端点和 Chat Completions 端点对同一个请求返回的内容是否语义一致。这一步不能省因为适配层的 bug 往往藏在看起来能跑的表象下。先验证健康检查和模型列表curl -s http://localhost:5678/health | jq curl -s http://localhost:5678/v1/models | jq .data[].id/health应该返回 provider 注册状态/v1/models应该列出你配置的所有模型别名。如果这里就报错先检查 Key 和 Base URL。接着用 Chat Completions 协议发一个基准请求curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: deepseek-v4-pro, messages: [{role: user, content: 用一句话解释什么是协议适配层}], stream: false } | jq .choices[0].message.content记下返回的文本内容。然后用 Responses 协议发同样的语义请求走网关curl -s http://localhost:5678/v1/responses \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-5.5, input: 用一句话解释什么是协议适配层, stream: false } | jq .output[0].content[0].text两次返回的文本应该语义一致措辞可能不同因为模型有随机性。如果 Responses 侧返回空或者结构不对说明 bridge 的响应重建有问题。再验证流式响应。Chat Completions 流式curl -N https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: deepseek-v4-pro, messages: [{role: user, content: 数到三}], stream: true }你会看到一串data: {choices:[{delta:...}]}。然后走网关的 Responses 流式curl -N http://localhost:5678/v1/responses \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-5.5, input: 数到三, stream: true }这里应该看到完整的事件序列response.created、response.output_item.added、多个response.output_text.delta、response.output_item.done、response.completed。如果只看到 delta 没有生命周期事件说明状态机没生效。最后验证工具调用。这是最容易出问题的场景curl -s http://localhost:5678/v1/responses \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-5.5, input: 北京现在天气怎么样, tools: [{ type: function, name: get_weather, description: 查询指定城市天气, parameters: { type: object, properties: {city: {type: string}}, required: [city] } }], tool_choice: required, stream: false } | jq .output[] | select(.typefunction_call)如果返回里有一个type为function_call的 output item且arguments是合法 JSON说明工具调用映射正确。如果arguments是空或者半截回去检查状态机的参数拼接逻辑。验证通过后建议把这几条 curl 命令存成一个verify.sh脚本每次改完 bridge 配置都跑一遍。适配层的回归测试靠肉眼是看不出来的。5. 常见报错排查401、local proxy failed、reading choices、OAuth适配层跑起来后报错基本集中在几类。我把真实遇到过的错误和排查路径整理出来对照着看能省不少时间。401 Unauthorized。这个最常见但原因有好几种。先确认 Key 有没有正确注入echo $TAOTOKEN_API_KEY看环境变量是否为空。如果 Key 没问题检查请求头格式必须是Authorization: Bearer sk-xxx少个空格或者用了Token前缀都会 401。还有一种情况是网关配置里api_key_env写错了变量名导致读取到空值。排查时可以在网关日志里看实际发出的请求头注意不要打印完整 Key。local proxy failed。这个报错通常出现在 Codex 或 Cline 这类客户端里意思是客户端连不上你配置的本地网关。先确认网关进程在跑curl http://localhost:5678/health。如果本地能通但客户端报错检查config.toml里的base_url是不是写成了http://127.0.0.1:5678/v1端口和路径都要对。还有一种情况是客户端走了系统代理把本地请求也代理出去了需要在客户端配置里加no_proxylocalhost,127.0.0.1。reading choices 相关报错。典型信息是Cannot read properties of undefined (reading choices)或者reading 0。这说明网关在解析上游响应时期望的choices字段不存在。原因通常是上游返回了错误响应比如 429 限流或 400 参数错误但网关没做错误分支处理直接去读choices就崩了。修复方法是在 provider client 里加响应校验先判断 HTTP 状态码非 200 时把上游错误体原样抛出不要往下走解析逻辑。另外有些 provider 在流式模式下错误也是以 SSE 形式返回的状态机要能识别data: {error:...}这种事件。OAuth 相关报错。如果你在 Codex 里看到OAuth token expired或者failed to refresh token说明客户端还在走 OpenAI 官方认证流程。这时候要检查config.toml里有没有设requires_openai_auth false。如果设了还报错可能是 Codex 版本较老不支持自定义 provider 的免认证模式升级到最新版即可。还有一种情况是你之前登录过 OpenAI 账号本地缓存了 token需要清掉~/.codex/auth.json再重启。除了这四类还有几个隐蔽的坑。比如工具调用返回的tool_call_id在 Responses 侧恢复时对不上导致下一轮请求里 tool result 无法关联。这个要在 bridge 里维护一个 ID 映射表Chat 侧的tool_call_id和 Responses 侧的call_id要能互相转换。再比如finish_reason映射错误Chat 的length应该映射到 Responses 的incomplete如果映射成completed客户端会以为输出完整实际被截断了。排查这些问题的通用方法是打开 trace。网关的 trace 会记录 provider request 元数据、原始和转换后的 stream event、usage 详情。Payload 捕获默认是摘要模式调试时可以临时开capture_payload: true看完整请求体但要注意里面可能含敏感信息调完记得关掉。6. 长期编码与 Agent 场景的接入建议如果你打算把这套适配层用在长期编码或者 Agent 场景有几个工程决策值得提前想清楚。第一provider 只声明差异公共策略放 bridge。provider 的职责是回答我支持哪些 tool_choice我的 reasoning 参数怎么表达我的 stream delta 长什么样而不是决定这个请求该降级还是拒绝。一旦 provider 开始做公共决策bridge 的边界就被打穿了后面加 provider 会越来越乱。我见过一个项目每个 provider 的 adapter 里都复制了一份降级逻辑结果改一次策略要改五个文件还经常漏改。第二流式响应必须按状态机设计不能靠 if/else 拼。只要涉及工具调用、结构化输出、usage、错误恢复状态机的稳定性远超临时判断。状态机要明确几个关键节点response 何时 created、output item 何时 added、delta 何时写入、何时 done、错误何时转成response.failed。这些节点定义清楚后新增 provider 只需要适配 delta 结构不用重写流程。第三session store 存中性快照不存 provider 私有格式。这样切换 provider 时历史上下文能复用调整 bridge 策略时也不用迁移数据。快照里应该包含 API-shaped 的 input/output 项而不是某个 provider 的 message 对象。第四trace 默认开启但 payload 捕获默认关闭。trace 的价值不只是排障还能验证兼容性规划是否符合预期——某个请求为什么降级、某个工具为什么恢复成这个 ID、某次 stream 为什么以 failed 结束都应该能还原。但完整 payload 可能含用户代码、密钥等敏感信息生产环境要谨慎。如果你在团队里推这套方案建议把模型别名表当成配置资产来管理。不同场景用不同模型复杂编码任务走强代码模型子任务或轻量生成走更快更便宜的模型测试环境走 mock 端点。这些路由策略集中在网关配置里客户端只认别名切换成本极低。最后给一个实操建议把验证脚本纳入 CI。每次改 bridge 配置或新增 provider自动跑一遍 curl 一致性检查包括普通文本、流式、工具调用三个场景。适配层的 bug 往往在特定组合下才暴露靠手动测试覆盖不全。这套脚本跑通后你新增一个 provider 的时间能从半天压缩到半小时。如果你还没开始搭可以从 TaoToken 的模型对话页面先手动验证一下两种协议的返回差异路径是https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。看清楚差异再动手写适配层比边写边猜要快得多。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的端点说明和参数列表。长期跑 Agent 任务的话Coding Plan 页面https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite有配额和模型路由的说明值得先看一眼再决定架构。
返回列表