Codex 接入多模型时,网关层需要处理哪些协议差异?
Codex 接入多模型时,网关层需要处理哪些协议差异?
最近有个小工具 GodeX——一个本地 OpenAI Responses API 网关,专门用来让 Codex、CLI Agent 这类客户端接入 DeepSeek、Xiaomi、MiniMax、Zhipu 等 Chat Completions provider。
一开始,这个问题看起来挺简单:
但真做下去就会发现,事情远没有这么轻。
普通聊天请求可以转,但 Agent 请求不一样。它可能带工具调用、结构化输出、流式响应、previous_response_id、usage、cached tokens、reasoning、provider-specific finish reason。只做字段改名,很快就会陷入一堆边界问题。
这篇文章不谈“项目发布”,重点聊一聊在做 GodeX 时遇到的几个协议桥接点。
1. Responses API 和 Chat Completions 并不同构
最直觉的结构是:
Responses request-> gateway-> Chat Completions request-> provider response-> gateway-> Responses response
如果只有 input -> output_text,问题不大。
但 Agent 请求里会出现:
- tool definitions;
tool_choice;- structured output;
- streaming;
previous_response_id;- usage;
- reasoning;
- incomplete / failed;
- upstream error;
- provider-specific delta。
这些能力在不同 provider 上支持程度不一致。
比如 tool_choice: required,有的 provider 支持,有的不支持。有的只支持 auto。如果网关静默降级,Agent 行为可能直接变了:本来必须调用工具,结果模型开始自由回答。
所以 GodeX 做了一层 compatibility plan:请求进来以后,根据 provider capability 判断哪些能力原生支持、哪些需要降级、哪些必须拒绝,并把结果写入 diagnostics。
2. Provider 不应该自己决定公共策略
一个容易走偏的设计是:每个 provider 里写一套 mapper。
deepseek mapper
zhipu mapper
minimax mapper
xiaomi mapper
短期看很快,长期会有两个问题。
第一,公共逻辑重复。工具调用恢复、结构化输出降级、Responses output item 重建,这些逻辑会散落到每个 provider。
第二,策略不一致。今天 DeepSeek 对 strict schema 降级,明天 Zhipu 直接报错,后天 MiniMax 静默丢字段。客户端看到的行为会越来越不可预测。
GodeX 的做法是把边界拆开:
bridge管公共 Responses-to-Chat 策略;providers只描述 provider 差异;responses管同步/流式 pipeline;trace管可观测性。
Provider 只回答:
- 支持哪些 tool choice;
- 支持哪些 response format;
- usage 从哪里读;
- stream delta 长什么样;
- finish reason 怎么映射;
- 是否需要 request patch。
至于“降级还是拒绝”,由 bridge kernel 统一决定。
3. Streaming 不能靠字符串拼接
流式响应是最容易被低估的部分。
Chat Completions SSE 通常是 provider delta;Responses SSE 则有完整事件生命周期。
一个 Responses stream 可能包含:
- response created;
- output item added;
- content part added;
- output text delta;
- content part done;
- output item done;
- response completed;
- response failed。
如果网关只是把上游 delta 原样转发,Codex 这类客户端就无法把它当标准 Responses stream 使用。
所以 GodeX 在 bridge/stream 中做了状态机,把上游 chunks 转换成 Responses events。状态机要处理:
- 什么时候创建 response;
- 什么时候创建 output item;
- delta 归属哪个 content part;
- tool call delta 如何聚合;
- finish reason 如何映射;
- usage 在哪里出现;
- 输出非法时如何 failed;
- 上游中断时如何 incomplete。
这里的经验是:只要涉及 Agent streaming,就不要用一堆临时 if/else 拼事件。状态机会更啰嗦,但更容易证明行为。
4. Structured Output 要显式降级
另一个典型问题是结构化输出。
有的 provider 支持 json_object,但不支持严格 json_schema。客户端如果要求 strict schema,网关有几种选择:
- 原生传递;
- 降级为
json_object; - 拒绝请求。
GodeX 当前的做法是:如果 provider 只支持 json_object,就降级并注入 schema 格式指令,最终输出阶段检查 JSON 语法。
它不是完整 JSON Schema 校验,但至少避免了“请求看起来成功,结果输出完全不是 JSON”的情况。
重点不是“永远降级”,而是“降级必须可诊断”。
5. Session 不要存 provider 私有格式
Responses API 的 previous_response_id 很容易被误解成会话 ID。
GodeX 里把它当父指针处理。每个 response 指向上一个 response,下一轮请求进来时,session store 恢复历史,再构建 provider-neutral history。
这里有个关键取舍:session store 不保存 provider 私有 message 格式,而是保存 API-shaped snapshot。
这样做的好处是:
- 后续可以切换 provider;
- bridge 策略可以调整;
- session store 不被某个 provider 绑定;
- 缺失父节点、成环、深度溢出都能统一检测。
6. Trace 要从第一版开始做
Agent 请求失败时,问题可能出在很多地方:
- model alias 解析;
- provider capability;
- request downgrade;
- 上游 HTTP;
- stream 中断;
- tool call 恢复;
- output contract;
- session chain。
没有 trace,只看最终输出很难定位。
GodeX 默认写 SQLite trace,记录 provider request、response、stream event、usage 和 error。尤其是流式转换,能同时看到原始 provider event 和转换后的 Responses event,会省很多排查时间。
架构图

请求流程

小结
如果只是调用一次模型 API,不一定需要网关。
但如果你在做 Codex 接入、多 provider 路由、内部 Agent 平台,网关层很快就会承担这些职责:
- 协议转换;
- provider capability 规划;
- structured output 降级;
- stream event 重建;
- session chain;
- trace;
- diagnostics。
GodeX 只是这个问题的一种实现。回看整个过程,最大的收获是:模型网关真正复杂的地方不在 HTTP 转发,而在协议语义的边界。