从 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 就行:

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。

它提供本地服务:

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 架构图

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。

这几个边界里,最关键的是 bridge 和 providers 的关系。

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

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

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

组件交互图

GodeX 组件交互图

一次 /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 风格请求里可能出现:

{
  "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 目录形态是:

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 中配置模型别名:

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐