从 Responses API 到 Chat Completions:一个模型网关的设计复盘

从 Responses API 到 Chat Completions:一个模型网关的设计复盘

最近我在做 GodeX 1.0.0,它是一个 OpenAI Responses API 兼容网关。

表面上看,这类项目很容易讲清楚:给 Codex、CLI 工具和开发者 Agent 提供一个本地 Responses API 服务,让这些客户端可以通过同一个协议调用 DeepSeek、Xiaomi、MiniMax、智谱等模型。

但真正做下去之后,会发现这不是"把请求转发到另一个 URL"那么简单。

Responses API 和 Chat Completions API 在概念上相近,但在工程细节上差异很大。尤其是当客户端不只是普通聊天窗口,而是 Codex 这类 Agent 工具时,请求里会出现工具调用、结构化输出、流式响应、会话链、模型别名、usage 统计、错误恢复和可观测性要求。

如果把这些差异都塞进客户端,客户端会越来越重;如果每个 provider 都自己写一套完整 mapper,网关会越来越散。

GodeX 1.0.0 的核心设计,就是把这些差异收敛到一个相对清晰的协议桥接内核里。这个问题本身并不只属于 GodeX,任何试图把多家 Chat Completions provider 接到 Responses API 客户端的网关,都会遇到类似边界。

本文不是单纯的项目公告,而是对这个网关设计过程的复盘:为什么需要它,哪些地方容易踩坑,GodeX 怎么划分边界,以及这些设计对其他 Agent 网关或模型适配层有什么参考价值。

问题从哪里来

很多模型提供商现在都提供 Chat Completions 风格的接口。

从最简单的角度看,似乎只要把客户端的 POST /v1/responses 改写成上游的 POST /chat/completions 就行:

text 复制代码
client responses request
  -> gateway
  -> provider chat completions request
  -> provider response
  -> gateway
  -> client responses response

如果只处理一次普通文本生成,这个想法大体成立。

但开发者 Agent 对协议的依赖要深得多。一个真实请求里可能包含:

  • 多轮上下文;
  • previous_response_id
  • tool definitions;
  • tool_choice
  • response_format
  • reasoning 参数;
  • streaming;
  • usage;
  • cached tokens;
  • provider-specific finish reason;
  • partial output;
  • upstream error;
  • interrupted stream。

这些东西不能靠简单字段映射解决。

比如工具调用。Responses API 里的 output item、tool call、tool result 和 Chat Completions 的 message/tool_calls 并不是天然同构。再比如流式响应,Responses SSE 有自己的事件生命周期,而上游 Chat Completions SSE 往往只是 delta 拼接。再比如结构化输出,有的 provider 支持 json_object,但不支持严格 json_schema;有的 provider 对工具选择只支持 auto,有的支持 required 或指定 function。

如果网关在这些地方只是"能传就传,不能传就丢",Agent 的行为会变得不可预测。失败的时候,用户甚至不知道到底是模型能力问题、provider 协议差异,还是网关转换错误。

这就是 GodeX 要解决的问题。

GodeX 是什么

GodeX 是一个 OpenAI Responses API 兼容网关,面向 Codex、CLI 工具和开发者 Agent。

它提供本地服务:

http 复制代码
POST /v1/responses
GET /v1/models
GET /health

客户端仍然面向 Responses API 编程,GodeX 负责把请求桥接到实际 provider 的 Chat Completions API,再把上游响应重建为 Responses 风格输出。

GodeX 1.0.0 内置支持:

  • DeepSeek
  • Xiaomi Mimo
  • MiniMax
  • Zhipu

也可以通过配置接入自定义 Chat Completions 兼容端点。

从工程角度看,GodeX 做的事情可以概括为:把"模型 provider 差异"从客户端里拿出来,集中放到一个可测试、可观测、可扩展的协议层里。

架构图

GodeX 不是一层薄代理。它把一次请求拆成几个边界:

  • server:Bun 路由,负责 /health/v1/models/v1/responses
  • context:创建请求级 ResponsesContext,保存 request id、response id、provider、model、session、diagnostics。
  • resolver:把客户端模型名解析为 provider/model,支持别名。
  • session:处理 previous_response_id 会话链,支持 memory 和 SQLite。
  • bridge:共享 Responses-to-Chat 策略,包括兼容性规划、请求归一化、工具规划、结构化输出、响应重建和流式状态机。
  • providers:provider-specific 能力声明、endpoint、auth、hooks 和协议 DTO。
  • responses:同步和流式 pipeline 编排。
  • trace:记录请求、usage、event 和 error。

这几个边界里,最关键的是 bridgeproviders 的关系。

GodeX 不希望每个 provider 都复制一套完整的 adapter。共享协议策略应该在 bridge kernel 中集中处理;provider 只声明自己的能力和差异。例如:

  • 支持哪些 tool choice;
  • 支持哪些 response format;
  • reasoning 怎么表达;
  • usage 和 cached tokens 从哪里读;
  • stream delta 的结构有什么特殊点;
  • finish reason 怎么映射。

这样新增 provider 时,不需要再造一个 mapper 森林;修改公共兼容策略时,也不需要每个 provider 各改一遍。

组件交互图

一次 /v1/responses 请求的大致流程如下:

  1. 客户端提交 Responses 请求。
  2. Bun server 解析并校验请求体。
  3. ResponsesContext 创建请求上下文。
  4. ModelResolver 将模型名解析为 provider 和上游模型。
  5. 如果存在 previous_response_id,session store 恢复会话链。
  6. Registrar 找到对应 provider 的 ProviderEdge
  7. ResponsesBridgeRuntime 进入同步或流式 pipeline。
  8. ProviderExchange 构建 provider request,记录 trace,并调用 provider。
  9. provider 返回 JSON response 或 SSE chunks。
  10. 同步路径重建 ResponseObject;流式路径通过状态机重建 Responses SSE events。
  11. pipeline 校验输出契约、记录 usage、持久化 session,并返回客户端。

这条链路里有几个地方特别容易被低估。

第一,session 恢复必须发生在构建 provider request 之前。因为 provider 收到的是 Chat Completions messages,而客户端传入的是 Responses input 加一个 previous_response_id。网关必须先恢复历史,再转换为 provider-neutral messages。

第二,compatibility plan 必须在 provider request 构建阶段产生,而不是等失败后再猜。比如某 provider 不支持严格 json_schema,GodeX 要提前决定是降级为 json_object,还是拒绝请求。

第三,streaming 不是输出字符串拼接。流式状态机需要知道什么时候创建 response,什么时候创建 output item,什么时候写 delta,什么时候结束,什么时候把错误转换为 response.failed

为什么不能只做字段转发

以几个典型差异为例。

Tool Choice

OpenAI 风格请求里可能出现:

json 复制代码
{
  "tool_choice": "required"
}

也可能指定某个 function。

但不是所有 provider 都支持这些语义。有的只支持 auto,有的支持 none,有的支持 function 指定,有的字段结构还不同。

这里有三种处理方式:

  1. 直接报错;
  2. 静默降级;
  3. 根据 provider capability 规划,并把降级或拒绝写入 diagnostics。

GodeX 选择第三种。

原因很简单:Agent 请求里,工具调用不是装饰品,而是执行路径的一部分。静默降级可能会让模型从"必须调用工具"变成"随便聊聊",这类问题排查起来非常痛苦。

Structured Output

结构化输出也类似。

有些 provider 支持 json_object,但不支持严格 json_schema。如果客户端要求 strict schema,网关需要做一个明确决策:

  • provider 原生支持,就直接传;
  • provider 只支持 JSON object,就降级并注入 schema 指令;
  • provider 连 JSON object 都不支持,就拒绝或诊断。

GodeX 当前对 strict 降级 schema 的处理是:上游使用 json_object,同时在 provider prompt 前言中加入格式指令,最终输出阶段检查 JSON 语法。

这不是完整 JSON Schema 校验,但至少能保证"降级行为是显式的,并且最终输出不会完全失控"。

Streaming

Chat Completions SSE 通常是 provider delta。

Responses SSE 则是一个更完整的事件模型。客户端可能期待:

  • response created;
  • output item added;
  • content part added;
  • output text delta;
  • content part done;
  • output item done;
  • response completed;
  • response failed。

如果只是把上游 delta 原样转发给客户端,客户端无法把它当成标准 Responses stream 使用。

所以 GodeX 在 bridge/stream 中维护状态机,把 provider chunks 转成 Responses events。这里还要处理中断、finish reason、usage、工具调用和最终输出校验。

ProviderSpec:让 provider 只描述差异

GodeX 的 provider 目录形态是:

text 复制代码
src/providers/<name>/
  spec.ts       ProviderSpec declaration
  client.ts     ProviderEdge construction with ChatProviderClient
  hooks.ts      Provider-specific patching, accessors, usage, stream deltas
  protocol/     Provider DTOs when needed
  index.ts      Public exports

这里的设计目标是让 provider package 尽量小。

一个 provider 主要回答这些问题:

  • endpoint 在哪里;
  • auth 怎么做;
  • 默认模型是什么;
  • 支持哪些 tool choice;
  • 支持哪些 response format;
  • reasoning 参数怎么映射;
  • usage 怎么读取;
  • finish reason 怎么读取;
  • stream delta 怎么识别;
  • provider 是否需要 request patch。

共享的策略,例如"当 provider 不支持 strict schema 时如何降级""工具 ID 如何恢复""Responses output item 如何重建",不放在 provider 里。

这能避免两个问题。

第一,provider 之间复制逻辑。复制一开始省事,后来会让兼容策略失控。

第二,provider hooks 里出现公共决策。provider 一旦开始决定"哪些请求应该降级、哪些请求应该拒绝",bridge kernel 的边界就被打穿了。

GodeX 1.0.0 的一个重要约束就是:provider hooks 暴露协议差异,bridge 决定支持、降级、拒绝和诊断。

模型别名:把路由策略留在本地

GodeX 支持在 godex.yaml 中配置模型别名:

yaml 复制代码
server:
  port: 5678
  host: 0.0.0.0

default_provider: deepseek

models:
  aliases:
    gpt-5.5: "deepseek/deepseek-v4-pro"
    gpt-5.4: "deepseek/deepseek-v4-pro"
    gpt-5.4-mini: "zhipu/glm-5.1"
    gpt-5.3-codex: "deepseek/deepseek-v4-pro"
    gpt-5.3-codex-spark: "zhipu/glm-5.1"

这里需要强调一点:这些 alias 是本地 routing policy,不代表与 OpenAI 原模型能力等价。

它解决的是客户端稳定性问题。

例如 Codex 侧只知道自己使用 gpt-5.5,但 GodeX 可以把这个别名路由到 deepseek/deepseek-v4-pro。以后如果你想切换 provider 或模型,只需要改 GodeX 配置,而不是改每个客户端。

对团队内部工具来说,不同场景可以用不同模型:

  • 复杂编码任务走强代码模型;
  • 子任务或轻量生成走更快更便宜的模型;
  • 特定业务场景走私有化 provider;
  • 测试环境走 mock 或自定义兼容端点。

会话链:previous_response_id 不是 mutable cursor

Responses API 的 previous_response_id 很容易被误解成"当前会话 ID"。

在 GodeX 里,它更像父指针。

每次 response 都可以指向上一个 response,形成一条链。下一轮请求进来时,GodeX 根据 previous_response_id 解析历史,恢复 provider-neutral 的上下文,然后再构建 Chat Completions messages。

这个设计带来几个工程要求:

  • 父节点缺失要报错;
  • 链条成环要检测;
  • 深度要有限制;
  • incomplete response 不能被当作正常上下文;
  • store: false 要跳过当前轮持久化;
  • session store 里保存的是 API-shaped snapshot,而不是某个 provider 的私有 message。

这些细节看起来不像亮点,但它们决定了多轮 Agent 是否可靠。

如果 session store 里保存的是 provider-specific 格式,后续切换 provider 或调整 bridge 策略都会变得困难。GodeX 选择把 provider-specific 转换留在 bridge 阶段,session store 只保存更中性的 API 快照。

Trace:为什么网关需要可观测性

Agent 请求的调试难点在于链路长。

一次失败可能来自:

  • 客户端请求不合法;
  • 模型别名解析错误;
  • provider 不支持某个能力;
  • request downgrade 后模型没有遵守格式;
  • 上游 HTTP 错误;
  • 上游 stream 中断;
  • tool call 没有正确恢复;
  • output contract 校验失败;
  • session chain 缺失或成环。

没有 trace 时,这些问题很容易混在一起。

GodeX 默认启用 SQLite trace,记录:

  • provider request 元数据;
  • provider request / response 摘要 payload;
  • 原始和转换后的 stream event;
  • usage 详情,包括 cached tokens;
  • route error 和 provider error。

Payload 捕获默认是摘要模式。也可以开启 trace.capture_payload: true 保存更完整 payload,但这会涉及敏感信息,需要谨慎。

从实践上看,trace 的价值不只是"出了问题能看日志"。它还能帮助检查兼容性规划是否符合预期:某个请求为什么降级、某个工具为什么恢复成这个 ID、某次 stream 为什么以 failed 结束,都应该能被还原出来。

首发支持的 Provider

GodeX 1.0.0 内置 provider 情况如下:

Provider Default Model Reasoning Tool Choice Response Format Cached Tokens
DeepSeek deepseek-v4-pro native auto, none, required, function text, json_object 支持
Xiaomi mimo-v2.5-pro boolean auto text, json_object 支持
MiniMax MiniMax-M2.7 none auto, none, required, function text, json_object 支持
Zhipu glm-5.1 boolean auto, none text, json_object 支持

这些 provider 的能力声明不是为了做一张漂亮表格,而是直接参与 bridge 的 compatibility plan。

同一个客户端请求,在不同 provider 上可能得到不同规划:

  • DeepSeek 可以使用更完整的 tool choice;
  • Xiaomi 的 tool choice 能力更窄;
  • MiniMax 没有 reasoning;
  • Zhipu 支持布尔 reasoning 但 tool choice 范围不同。

网关的职责是让这些差异可见、可控,而不是假装它们不存在。

和 Codex 集成

GodeX 可以作为 Codex 的本地模型 provider。

安装:

bash 复制代码
npm install -g @ahoo-wang/godex
godex init
godex serve --config ./godex.yaml

~/.codex/config.toml 中添加自定义 provider:

toml 复制代码
model = "gpt-5.5"
model_provider = "godex"

[model_providers.godex]
name = "GodeX"
base_url = "http://127.0.0.1:5678/v1"
wire_api = "responses"
requires_openai_auth = false
supports_websockets = false

之后 Codex 请求 gpt-5.5,GodeX 会根据配置路由到真实 provider/model。

这样做的好处不是"多包了一层",而是把 Codex 和 provider 之间的耦合降下来:

  • Codex 只认 Responses API;
  • GodeX 负责 provider 适配;
  • 模型路由策略在 godex.yaml
  • trace 和 session 在本地;
  • provider 能力差异集中诊断。

安装与快速开始

npm 安装

bash 复制代码
npm install -g @ahoo-wang/godex
godex --help

源码运行

bash 复制代码
git clone https://github.com/Ahoo-Wang/GodeX.git
cd GodeX
bun install
bun run dev

bun run dev 默认使用 13145 端口;运行时配置默认端口是 5678

初始化配置

bash 复制代码
godex init
godex serve --config ./godex.yaml

健康检查与模型列表

bash 复制代码
curl http://localhost:5678/health
curl http://localhost:5678/v1/models

/health 可以看到 provider 注册状态;/v1/models 会暴露已配置模型别名。

Docker 部署

GodeX 也提供 Docker 镜像。

Docker Hub:

bash 复制代码
docker pull ahoowang/godex:latest

使用配置文件运行:

bash 复制代码
docker run -d \
  --name godex \
  -p 5678:5678 \
  -e ZHIPU_API_KEY=your-key \
  -e DEEPSEEK_API_KEY=your-key \
  -e MINIMAX_API_KEY=your-key \
  -e MIMO_API_KEY=your-key \
  -v ./godex.yaml:/etc/godex/godex.yaml:ro \
  -v godex-data:/data \
  ahoowang/godex:latest

容器默认使用 5678 端口,配置文件路径为 /etc/godex/godex.yaml,数据目录为 /data,适合把 session 和 trace 持久化下来。

对类似项目的几点经验

如果你也在做模型网关、Agent runtime 或 provider 适配层,我觉得 GodeX 这次有几条经验值得单独拿出来。

1. 不要让 provider 决定公共策略

Provider 可以告诉你"我支持什么""我的字段怎么读""我的 delta 长什么样"。

但 provider 不应该自己决定公共协议策略。否则每个 provider 都会变成一个小网关,最后共享行为会失控。

更好的方式是:

  • provider 暴露 capability 和 hooks;
  • bridge kernel 统一规划支持、降级、拒绝和 diagnostics;
  • pipeline 统一处理 trace、session、logging 和 output validation。

2. 流式响应要按状态机设计

流式协议不要靠"看到什么就输出什么"拼出来。

只要涉及工具调用、结构化输出、usage、错误恢复,状态机会比临时 if/else 稳定得多。

至少要明确:

  • response 何时 created;
  • output item 何时 added;
  • content part 何时 added/done;
  • delta 如何归属;
  • finish reason 如何映射;
  • incomplete 和 failed 如何表达;
  • 最终 usage 在哪里出现。

3. 降级必须可诊断

兼容层经常需要降级。

降级本身不是问题,静默降级才是问题。

当 strict schema 变成 json_object,当 required tool choice 无法原生支持,当 reasoning 参数被 provider 忽略时,网关应该能告诉调用方发生了什么。

这对 Agent 系统尤其重要,因为 Agent 的失败往往不是一个明确异常,而是行为偏离预期。

4. Session store 不要保存 provider 私有格式

如果 session store 保存的是某个 provider 的 message 格式,后续会很难切换 provider,也很难调整 bridge 行为。

更稳妥的方式是保存 API-shaped snapshot,让 provider-specific 转换发生在请求构建阶段。

5. Trace 要从第一版开始做

网关类项目越早有 trace,后面越省时间。

尤其是流式场景,只看最终字符串几乎没法定位问题。记录原始 provider event、转换后 Responses event、usage 和 error,对排查协议桥接问题非常有帮助。

什么时候需要类似网关

如果只是偶尔调用一次模型 API,单独引入网关未必有必要。

但如果系统里出现下面这些需求,就值得考虑在客户端和 provider 之间放一个协议层:

  • Codex 或内部 Agent 需要接入多家模型提供商;
  • 模型路由策略需要独立于客户端配置;
  • 多个客户端都需要统一 Responses API;
  • 工具调用、结构化输出、流式事件和 usage 统计需要稳定语义;
  • Agent 请求链路需要 trace;
  • Chat Completions provider 需要以统一方式扩展;
  • 团队内部需要维护一个轻量模型网关。

这类网关的定位不是替代模型厂商,而是把 provider 差异隔离在一个更容易测试和诊断的地方。

写在最后

GodeX 1.0.0 是一个阶段版本,也是一组设计取舍的阶段性收敛。

它最重要的不是"支持了几家 provider",而是把 Responses API 到 Chat Completions API 的桥接问题拆出了几个清楚边界:

  • bridge kernel 管公共协议策略;
  • provider hooks 管 provider 差异;
  • session store 管 API-shaped 会话快照;
  • pipeline 管同步/流式编排;
  • trace 管请求可观测性。

这套边界不一定适合所有项目,但它解决了一个很现实的问题:当 Agent 客户端、OpenAI 风格协议和多模型 provider 同时出现时,适配逻辑必须有地方安放。

对 GodeX 来说,这个地方就是 bridge kernel。

后续我会继续围绕 provider 扩展、Responses API 兼容性和 Agent 网关设计补充更多实现细节。

相关资料

英文文档:

https://godex.ahoo.me/

中文文档:

https://godex.ahoo.me/zh/

GitHub:

Ahoo-Wang/GodeX

Gitee:

https://gitee.com/AhooWang/GodeX

npm:

https://www.npmjs.com/package/@ahoo-wang/godex

Docker Hub:

https://hub.docker.com/r/ahoowang/godex