多厂商模型网关怎么设计:把 API 差异挡在业务代码之外

2026-08-25

数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。

接第二家模型 API 的时候,几乎所有人都会相信一句话:反正都兼容 OpenAI,改个 base_url 就行。真正把第二家接进生产之后你会发现,兼容的只有”发一条 chat 请求拿一段文本”这条最窄的路径,往外走一步——思考参数怎么开、错误怎么判、流式中途出错怎么感知、限流按什么维度算——每家的形态都不一样。多厂商网关要挡住的就是这些差异,而挡的位置不是”再包一个 chat 函数”,而是六个独立的适配层:通道配置、凭证、思考参数、错误归一化、流式事件、限流与重试。这六层各自独立演化,任何一层偷懒把厂商细节漏进业务代码,换厂商的时候都得全量重改。

第一层:base_url 根本不是一个字段

先看各家官方文档给的 OpenAI 兼容接入点,形态就对不齐。

智谱在《OpenAI API 兼容》里给的 base_urlhttps://open.bigmodel.cn/api/paas/v4/,路径段是 paas/v4 而不是常见的 /v1。Kimi 开放平台的 API 概述写得更细:用 SDK 时 base_url 设为 https://api.moonshot.cn/v1,直接打 HTTP 端点时完整路径是 https://api.moonshot.cn/v1/chat/completions。MiniMax 的官方写法是配环境变量 OPENAI_BASE_URL=https://api.minimaxi.com/v1。阶跃星辰的迁移文档要求把 base_url 设为 https://api.stepfun.com/v1。xAI 文档里的示例统一指向 https://api.x.ai/v1。Gemini 的 OpenAI 兼容层则是 https://generativelanguage.googleapis.com/v1beta/openai/

Anthropic 兼容那一路更乱。智谱要求把 base_url 换成 https://open.bigmodel.cn/api/anthropic,其 cURL 示例打的是 .../api/anthropic/v1/messages;MiniMax 官方给的是 ANTHROPIC_BASE_URL=https://api.minimaxi.com/anthropic;阶跃星辰的 Messages API 文档专门加了一条提示——使用 Anthropic SDK 时 base_url 应设为 https://api.stepfun.com,SDK 会自动拼接 /v1/messages,不需要手动带 /v1。同一份文档还说明,Step Plan 场景的请求地址是 https://api.stepfun.com/v1 之外的另一条通道 https://api.stepfun.com/step_plan/v1/messages,对应 SDK 的 base_urlhttps://api.stepfun.com/step_plan

结论很直白:网关的通道配置里不能只存一个 URL 字符串,至少要拆成三个字段——完整前缀这条前缀是否已经含版本段该版本段是由你拼还是由 SDK 拼。少存后两个,换一次 SDK 版本就可能踩到路径重复或缺失的 404。阶跃星辰的错误码表里 404 的解释正是”请求路径不正确”,而不是模型不存在——模型不存在在它那里归到 400。

同一家厂商还可能有多条通道并存,付费形态不同、路径也不同。这一层的抽象粒度应该是”厂商 × 通道”,不是”厂商”。更细的端点级差异可以对照站内的各家 OpenAI 兼容端点覆盖情况一起看。

第二层:凭证维度比你想的多一层

请求头也没统一。智谱 HTTP 接入文档里的 cURL 用 Authorization: Bearer YOUR_API_KEY;Kimi 的 API 概述明确写所有 API 请求需要在 HTTP 头中携带 API Key,形式同样是 Authorization 头里放 Bearer 加密钥;xAI 文档里的 cURL 也是这个形态。但智谱的 Anthropic 兼容示例用的是 x-api-key,Gemini 的原生 REST 用的是 x-goog-api-key,只有它的 OpenAI 兼容层才回到 Authorization: Bearer。MiniMax 两条兼容线都写明了头部:OpenAI 兼容的对话接口文档写的是用 HTTP Authorization 方案的 Bearer API_key 验证账户信息;Anthropic 兼容那侧的鉴权定义更细,既接受 Authorization: Bearer <API_KEY>,也接受 Anthropic 习惯的 x-api-key: <API_KEY>,并且明确规定两者同时存在时优先使用 Authorization,文档同时推荐用 Authorization: Bearer <API_KEY> 这种写法。这条优先级规则对网关是有实际意义的:如果适配层为了”兼容得更彻底”把两个头都塞进去,真正生效的是哪一个不由你的代码顺序决定,而由服务端这条规则决定——两个头拿的是同一把密钥时看不出问题,一旦是轮换过程中新旧两把密钥并存,鉴权结果就跟你以为的不一样。除了头部写法,MiniMax 的接入示例里还给了环境变量写法(OPENAI_BASE_URL / OPENAI_API_KEYANTHROPIC_BASE_URL / ANTHROPIC_API_KEY),由 SDK 读取后自行组装请求头。

更容易踩的是同一家的密钥不通用。Kimi 的常见问题里写得很清楚:官方提供两个平台,境内建议用 platform.kimi.com,境外建议用 platform.kimi.ai,两个平台的账户和 key 完全独立、不能混用,用错会返回 401 invalid_authentication_error;国内与境外的 base_url 也是两个不同域名。MiniMax 的接口概览里则说明,Token Plan 的订阅 Key 与按量计费的 API Key 相互独立。

所以网关的凭证表主键不能是”厂商名”,得是”厂商 + 通道 + 计费形态”。把这层做对,密钥轮换和权限收敛才有落脚点;反过来,如果凭证只按厂商存一份,同一家的两条通道就会互相覆盖,排查时看到的还是一个笼统的 401。

第三层:思考参数是差异最大的一块

这是我认为最不该让业务代码看见的部分,因为六家给出了六种写法。

智谱在 OpenAI 兼容示例里用 extra_body{"thinking": {"type": "enabled"}},思考内容从流式 chunk 的 delta.reasoning_content 里取。Kimi 的自动断线重连文档说明,K3 使用请求顶层的 reasoning_effort 配置推理强度,支持 low / high / max;同一段还提醒,换成其它模型时只需替换 model 字段,但各模型的参数配置存在差异。阶跃星辰更特别——它的推理文档写明,支持三档推理强度的模型在 Chat Completion API 里用 reasoning_effort,在 Messages API 里用 output_config.effort,同一家的两个兼容面字段名都不一样;思考过程通过 reasoning_content 字段返回。

MiniMax 走的又是另一条路。它的 OpenAI SDK 文档说明,原生 OpenAI 形态下模型返回的 content 字段会包含 <think> 标签内容,需要完整保留;若设置 extra_body={"reasoning_split": True},思考内容会被分离到 reasoning_details 字段。走 Anthropic SDK 时,思考内容是 content 列表里 type == "thinking" 的块。Gemini 的兼容层则做了映射:OpenAI 的 reasoning_effort 映射到 thinking_level(Gemini 3.x)或 thinking_budget(Gemini 2.5),两者功能重叠不能同时使用;官方明文说明 Gemini 2.5 Pro 与 Gemini 3 系列无法关闭推理。

网关该做的是定义一个自己的枚举(比如 off / low / medium / high),再由每个厂商适配器翻译过去,翻译不了的组合直接报配置错误而不是静默丢弃。这里有一个特别容易踩的坑:MiniMax 在 OpenAI 与 Anthropic 两份文档里都专门写了「特别注意」——多轮 Function Call 对话中必须把完整的模型返回(包含 tool_callsthinking / text 等所有块)加回对话历史,以保持思维链的连续性。如果网关为了”统一格式”在回程做了裁剪,只留文本,多轮工具调用就会从这里断掉。

第四层:错误语义对不齐,照抄 HTTP 状态码必错

各家的错误结构完全不是一回事。

智谱的错误码文档开宗明义:响应码由两部分组成,外层是 HTTP 状态码,内层是响应体里的业务错误码,返回形如 {"error":{"code":"1001","message":"..."}},注意 code 是字符串形态的数字。它的表里有一条特别值得记住——业务码 1113「您的账户已欠费,请充值后重试」对应的 HTTP 状态码是 429。网关若按状态码机械退避重试,这种请求重试到天荒地老也不会成功。同表里 1302(速率限制)、1305(模型访问量过大)、1308/1310(周期用量上限,附带 next_flush_time)也都是 429,但处置完全不同。

Kimi 的错误对象是 {"error": {"type": ..., "message": ...}},机器可读的字段名叫 type 不叫 code。它把 429 拆成三种 type:engine_overloaded_error(服务节点负载高,文档明说这个错误由服务端容量导致,充值或提升 Tier 不能直接消除)、exceeded_current_quota_error(欠费或额度不足)、rate_limit_reached_error(组织级并发 / RPM / TPM / TPD 限制)。Gemini 那边字段名又回到 code,取值是 snake_case,并且把两种 429 分得很干净:rate_limit_exceeded 是分钟级限流、退避重试有效,quota_exceeded 是每日配额耗尽、退避多少次都没用。

阶跃星辰的错误码表里,余额不足是 402 而不是 429;内容审核不通过是 451;组织额度相关的错误里,insufficient_credit 走 402,而 project_credit_limit_exceededmember_project_credit_limit_exceeded 走 429,文档专门提示这两种情况的错误码与速率限制相同,请以错误标识区分。MiniMax 干脆用一套自有数字业务码(如请求频率超限、余额不足、无效 API Key、超出 Token Plan 资源限制各占一个码),并要求反馈问题时提供 Header 中的 trace_id

网关这一层的正确做法是把厂商错误归一成三类语义:可重试(服务端过载、瞬时限流)、不可重试(鉴权失败、参数非法、内容审核)、换通道或人工介入(欠费、配额耗尽、套餐到期)。第三类最容易被误归到第一类,站内的 429 通用处理策略讲的是通用骨架,落到具体厂商时必须按上面这些 type / code 再细分一层。

第五层:流式的错误不在 HTTP 状态码里

这一层容易被漏掉,因为本地开发很难复现——本机短请求几乎不会在流中途出错,问题往往等到线上长响应才暴露。

Gemini 的错误参考文档写明,标准(非流式)请求通过 HTTP 状态码加 JSON body 里的 error 对象返回错误;而流式请求(SSE)是通过 SSE 流发送 event_type: "error" 事件,error 字段结构相同。直接后果就是:只判 HTTP 状态码的客户端会漏掉流式过程中的错误——首包已经 200 了,错误在后面才来。

智谱的错误码文档在末尾也补了同一件事:使用流式(SSE)调用时,如果 API 在推理过程中异常终止,不会返回错误码,而是在响应体的 finish_reason 参数中返回异常原因。阶跃星辰的异常处理建议干脆把异常分成两类——HTTP 层面的异常和模型层面的异常,后者要求逐个 chunk 读 finish_reason 判断,并列出了 stop(正常结束)、length(受 max_tokens 限制没写完)、content_filter(未通过安全审核)、tool_calls(模型要调工具)四种处置分支。Kimi 的错误列表里还有一条更刁钻的:非流式长请求可能在网关侧超时,此时返回的是 HTML 超时页而不是 JSON,官方给的建议是改用流式输出。

所以网关转发 SSE 时不能只做字节透传。至少要做三件事:解析每个 chunk 并识别错误事件、把 finish_reason 提升为网关自己的终止原因字段、在下游断开时记录(Kimi 把客户端提前断连单列为 499 client_closed_request,常见于流式响应被中间代理切断)。流式中断本身的处置可以接站内的流式中断与续传

第六层:限流维度对不齐,一个令牌桶管不了所有厂商

很多网关会写一个统一的 RPM 令牌桶,然后发现在某些厂商身上完全不起作用。原因是维度就不一样。

智谱的速率限制页面说明,其限制主要体现在并发请求数限制、不同模型设有独立的并发限制、不同用户权益等级与套餐对应不同的并发限制,以及高峰期的动态限流;文档还特意定义了并发数指”同一时刻正在处理中的请求数量”。也就是说,在智谱这边控 RPM 是控不住的,要控的是在途请求数,而且要按模型分桶。MiniMax 的限流文档写的是两种维度 RPM 与 TPM,但部分接口的限制类型是 RPM 加 CONN(最大并行运行任务数);同页还说明速率限制施加在账号(包括主账号 + 子账号)整体上,主子账号共同享有同一份额度——靠多开子账号来放大吞吐这条路,官方文档已经堵死了。Kimi 那边的组织级限制按 error type 分成并发、RPM、TPM、TPD 四种。Gemini 则是多维度并行,超出任何一个维度即触发限流错误,不是综合评估。

还有一个乘法陷阱。Kimi 的常见问题里指出,OpenAI SDK 自带重试机制,遇到错误时会自动重试,一个请求会被放大成多次请求,而这些重试同样占用 RPM 额度(具体重试次数以官方文档当前版本为准)。如果网关自己再加一层重试,实际打到厂商的请求数是两层相乘。要么关掉 SDK 的自动重试把控制权收到网关,要么反过来,但不能两层都开着还按一层估算配额。

用量口径:厂商字段只做映射,流水以自己的为准

网关必须自己落一张调用流水表,厂商返回的字段只做映射,不做主键。因为可观测字段各家给的东西不一样:xAI 的文档说明 usage 对象里带 cost_in_usd_ticks,流式场景下用 OpenAI SDK 或 REST 时需要设置 stream_options: { include_usage: true },且费用只出现在最后一个 chunk(choices 为空的那个),中间 chunk 不含用量数据;智谱的对话补全请求体支持由调用方传入 request_id(未提供时平台自动生成)和 user_id(用于标识终端用户);MiniMax 排查问题要 Header 里的 trace_id;Gemini 的计费依据按官方 FAQ 是四项——输入 token、输出 token、缓存的 token,以及缓存 token 的存储时长。

这四种东西压根不在一个抽象层级上,指望它们自动对齐是不现实的。可行的做法是:网关在入口生成自己的调用 ID,能透传给厂商的(如智谱的 request_id)就透传,不能透传的就把厂商侧标识(如 trace_id)作为附属字段记下来,对账时以自家流水为准、厂商账单为校验。

区域与合规:路由前置判断,别让业务代码 try/catch

如果网关里挂了境外通道,可用性判断要放在路由前,不能等请求失败再兜。

Gemini 的官方区域页说明,Gemini API 与 Google AI Studio 在官方列出的国家/地区推出,该列表中不包含中国大陆;官方对不在支持区域者给出的路径是改用 Gemini Enterprise Agent Platform 中的 Gemini API。除区域外还有两条准入条件:最低年龄要求,以及需在 Google 账号中完成年龄验证。Kimi 那边前面提过,境内境外是两个独立平台、密钥不能混用;它的错误列表里还有一条 403 permission_denied_error,message 为调用 IP 不在组织白名单内,文档标注为国际站常见。

网关该做的是把”这个租户 / 这个业务线能不能路由到这条通道”写成一条显式策略,命中不了就在网关层返回明确的配置错误,而不是让业务代码去 try/catch 一个跨境网络错误——后者的表现是超时或连接失败,和厂商侧的服务异常长得一模一样,排查时根本分不开。国内业务应当走各家官方给出的境内通道或官方指定的企业采购路径。至于任何绕过区域判定的手段,不在讨论范围内。

最后:这件事上最容易栽的两个坑

第一个坑是抽象层级选错。很多人把网关抽象成”一个 chat 函数 + 一个 model 字符串”,结果思考参数、工具调用回传、流式终止原因这些东西无处安放,只能以 if provider == "xxx" 的形式散进业务代码。正确的切法是按上面六层各自建适配器,业务代码只认网关自己的请求/响应模型。

第二个坑是把兼容层当等价物。Gemini 官方在推荐 OpenAI 兼容层的同时明确建议:如果尚未使用 OpenAI 库,推荐直接调用 Gemini 原生 API;智谱在 OpenAI 兼容与 Claude 兼容两份文档里都挂了同一条警告——某些场景下与原接口仍存在差异;阶跃星辰的迁移文档只列出了明确兼容的那几个接口(Chat Completion、文件的上传/列表/信息/内容/删除、模型列表与单模型查询、生成图片),没列进去的就不在兼容范围内。兼容层是迁移成本的降低,不是能力的等价。网关的价值恰恰在于把这些”差一点”的地方显式记下来,而不是假装它们不存在。

打算把现有单厂商代码改造成网关的,可以先照着换厂商迁移检查清单把差异盘一遍,再决定六层里哪几层先做。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。