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

    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。

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

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

    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。

    安装:

    npm install -g @ahoo-wang/godex
    godex init
    godex serve --config ./godex.yaml
    

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

    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 安装

    npm install -g @ahoo-wang/godex
    godex --help
    

    源码运行

    git clone https://github.com/Ahoo-Wang/GodeX.git
    cd GodeX
    bun install
    bun run dev
    

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

    初始化配置

    godex init
    godex serve --config ./godex.yaml
    

    健康检查与模型列表

    curl http://localhost:5678/health
    curl http://localhost:5678/v1/models
    

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

    Docker 部署

    GodeX 也提供 Docker 镜像。

    Docker Hub:

    docker pull ahoowang/godex:latest
    

    使用配置文件运行:

    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


    __EOF__

  • 本文作者: Ahoo-Wang
  • 本文链接: https://www.cnblogs.com/ahoo-wang/p/20241948/godex
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    Session是什么?它与Cookie有什么区别?
    产品经理岗位进阶的唯一法则
    基于图像识别的跌倒检测算法 计算机竞赛
    【Python 千题 —— 基础篇】判断布尔值
    在 SQL Server 中,可以使用内置函数 NEWID() 来生成一个随机的 GUID(全局唯一标识符)。可以将此 GUID 作为字符串拼接到查询结果中。
    发必收藏的好用API接口,可领取免费次数
    6.1 使用scikit-learn构建模型
    maven仓库的地址
    5款推荐给手残党的AI代码提示,带你健步如飞
    代码随想录第28天|回溯算法
  • 原文地址:https://www.cnblogs.com/Ahoo-Wang/p/20241948/godex