模型 API 各家错误码风格不一样:怎么做一层统一封装

2026-08-25

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

如果你的代码里只有一句 if status == 429: sleep(); retry(),那么换到任何一家国产平台上都会出问题。各家把错误信息放的层次不一样:智谱 GLM 是外层 HTTP 状态码套一层响应体里的业务错误码,Kimi 用 error.type 这种字符串类型名做判别键,MiniMax 分接口而异(一套数字错误码表挂在响应体的 base_resp.status_code 上,Anthropic 兼容端点与视频生成 v2 那边则是 HTTP 状态码加 type),阶跃星辰主要用 HTTP 状态码、只在组织额度这一格额外给了「错误标识」字符串,Grok 的官方调试页干脆整篇按 HTTP 状态码组织,Gemini 则用 snake_case 的 code 字段。更麻烦的是同一个状态码在不同家甚至同一家内部含义都不同——429 底下既可能是「等一会儿就好」,也可能是「等到天亮也没用」。所以统一封装的第一步不是写重试逻辑,而是先决定用什么当判别键,再把各家的原始码映射到你自己的一套语义分类上,并且原始码一个字都不能丢

第一件事:搞清楚每家把错误放在哪一层

这一步决定了你的解析函数长什么样,比后面所有事都重要。

智谱 GLM 的官方错误码页写得很直白:调用时收到的响应码由两部分组成,外层是 HTTP 状态码,内层是响应体正文中定义的业务错误码,后者提供更具体的错误描述。官方给的响应示例里,HTTP 是 401,响应体是一个 error 对象,里面 code 为字符串形式的业务码、message 为中文描述。也就是说在 GLM 上,只看 HTTP 状态码等于把信息量最大的那一层扔了。

Kimi 的错误响应同样是一个 error 对象,但里面的判别字段叫 type,取值是 content_filterinvalid_request_errorinvalid_authentication_errorincorrect_api_key_errorpermission_denied_errorresource_not_found_errorengine_overloaded_errorexceeded_current_quota_errorrate_limit_reached_errorclient_closed_requestserver_errorunexpected_outputserver_unavailable 这一类语义化的字符串。官方的排障建议里明确说了收到 429 要「先根据 error.type 区分原因」——这句话本身就说明 HTTP 码在 Kimi 这里不足以定位问题。

MiniMax 是这几家里分层最多的一个,值得多花两段说清楚,因为它的判别键取决于你打的是哪一套接口。

它确实有一张独立的数字错误码表:1000 未知错误/系统默认错误、1001 请求超时、1002 请求频率超限、1004 未授权或 Token 不匹配、1008 余额不足、1024 内部错误、1026 输入内容涉敏、1027 输出内容涉敏、1033 系统错误或下游服务错误、1039 Token 限制、2013 参数错误等。这张表只给码值、含义和解决方法,不带 HTTP 状态码列,也不带字段名——所以只翻这一页是找不到「这个码在响应体的哪里」的。答案在接口文档里:走 OpenAI 兼容的文本对话接口时,数字码放在响应体的 base_resp.status_code,同一个对象里还有一个 status_msg 存错误详情,官方在该字段说明里直接列了 1000、1001、1002、1004、1008、1013、1027、1039、2013 这几个取值。封装层要从 MiniMax 取原始码,找的就是这个字段。

而数字码与 HTTP 状态码之间并不是没有通道。走 Anthropic 兼容端点时,官方对错误响应的说明是统一使用 HTTP 状态码加 JSON body,error.type 的取值枚举为 invalid_request_errorauthentication_errorpermission_errornot_found_errorrequest_too_largerate_limit_errorapi_erroroverloaded_error,外层还带一个 request_id;官方逐个给了 400、401、403、404、413、429、500、529 的响应示例,其中 413 对应请求体或多模态文件超出大小限制,这个码在本文其他几家的错误码页里都没有出现过,跨厂商映射表里很容易漏掉。视频生成 v2 那类接口给的又是另一套:官方称其为 OpenAI 风格错误响应,出错时 HTTP 状态码就是真实错误码,响应体里同时有 typeauthorized_error 对 401、bad_request_error 对 400、rate_limit_error 对 429、insufficient_balance_error 对 402、unprocessable_entity_error 对 422、overloaded_error 对 529、server_error 对 500)和 http_code 字段,而内部数字码写在 message 结尾的括号里——官方鉴权失败示例的 message 以 (1004) 结尾,余额不足示例以 (1008) 结尾。这就是数字码与 HTTP 码之间的换算通道。

所以 MiniMax 这一格的正确做法不是写一个解析函数,而是按接口族分别写:OpenAI 兼容文本对话取 base_resp.status_code,Anthropic 兼容端点取 HTTP 码加 error.type,视频生成 v2 那类取 http_codetype、再从 message 尾部括号里抠出数字码。另外官方特别提示:反馈问题时请提供 Header 中的 trace_id

阶跃星辰的常见错误码表直接就是 HTTP 状态码表(400、401、402、404、429、451、500、503)。但它还有一组「组织额度相关错误码」,官方在那张表前面写了一句很关键的话:其中两种情况的错误码与速率限制相同,请以错误标识区分。对应的错误标识是 insufficient_creditproject_credit_limit_exceededmember_project_credit_limit_exceeded

Grok 的官方调试页按状态码分组列出 400、401、403、404、405、415、422、429,并且额外列了一个 2XX 的特例:/v1/chat/deferred-completion/{request_id} 端点会返回 202 Accepted,表示延迟补全请求已排队但结果还没出来。这个 202 不是错误,但如果你的封装层把「非 200 即异常」写死,它就会被误判成失败。

Gemini 的错误参考里,返回的 error 对象含 code(机器可读的 snake_case)与 message(人类可读)两个字段,官方还补了一句:未列出的错误码,API 以标准 HTTP 状态文本的 snake_case 形式返回。这一句对封装层特别有用——它意味着你可以放心地把未知码原样落库,格式是可预期的。

429 是最大的陷阱:同一个码底下藏着完全不同的处置

站内那篇 429 的通用处理思路讲的是跨厂商的共性做法,而这一格恰恰是各家差异最大的地方。

Gemini 把 429 明确拆成了两个码:rate_limit_exceeded 是超出每分钟或每秒的请求或 token 限制,官方推荐处置是等待加指数退避重试;quota_exceeded 是超出每日配额,官方推荐处置是等待配额重置或申请增加配额。这两者语义完全不同——对后者做指数退避,退多少次都不会成功。

Kimi 的 429 底下有三种 typeengine_overloaded_error 是服务节点负载较高,官方说明按 Retry-After 提示等待、降低并发并使用指数退避重试,并且明确写了一句:该错误由服务端容量导致,充值或提升 Tier 不能直接消除。exceeded_current_quota_error 对应账户欠费停用或 token 额度不足,要充值。rate_limit_reached_error 则细分为组织级并发限制、组织级 RPM、组织级 TPM、组织级 TPD 四种触发原因,官方给的动作是降低并发、按响应提示等待,或者升级 tier。限流维度本身的差异可以对照站内的 RPM 与 TPM 限流维度那篇看。

智谱 GLM 的 429 是这批里最热闹的:账户欠费、账户已达速率限制、模型当前访问量过大、达到使用上限且限额将在下次重置时间恢复、编程套餐已到期需续订、达到周期使用上限、当前订阅套餐未开放某个模型权限、使用模式不符合公平使用策略而被限制、企业套餐失效需联系管理员、以及某个 API Key 仅限企业编程套餐场景使用需要换成对应产品类型的 Key——这些全部挂在 429 下面,各自有独立的业务错误码。其中最反直觉的是最后一条:密钥类型用错了,返回的却是 429 而不是 401 或 403。

阶跃星辰的 429 除了速率超限,还承载了项目达到当期 Credit 上限、成员在该项目内达到当期 Credit 上限两种情况,官方给的解法是由主账号调高上限或等待下月重置,与「稍后重试」完全是两回事。

所以封装层至少要把 429 拆成三类:可退避重试的限流、服务端容量问题、以及需要人工介入的额度耗尽或套餐问题。第三类打死也不该进重试队列。

401 与 403 也不只有「密钥填错了」一种

GLM 的 401 底下区分了身份验证失败、Header 中未收到认证参数、认证 Token 已过期需重新生成、以及已开启二次认证保护需要二次认证登录四种情况。最后一种是账号安全设置引起的,跟密钥字符串本身对不对没关系。它的 403 则对应「无权访问某个 API」。

Kimi 的 401 里有一条值得单独拎出来:官方写明中国站与国际站两个平台的账户、余额和 API Key 完全独立,混用会返回 401,请确认调用端点与 Key 所属平台一致。这类错误查半天日志都查不出来,因为密钥字符串本身没有任何问题。Kimi 的 403 则包括该 API 未对当前账号开放、不允许访问其他用户信息、以及调用 IP 不在组织白名单内(官方注明国际站常见)三种。

Gemini 这一格是 authentication 对应 401、permission_denied 对应 403,官方处置分别是验证密钥、检查密钥权限与项目访问权限。Grok 的 401 官方说明是没有提供授权头或提供了无效的授权 token,解法是补上 Bearer 形式的授权头。

这一整类的共同点是不可重试,封装层应该直接归成硬失败并告警,而不是丢进退避队列空转。密钥本身的分发与轮换问题可以看 API Key 的安全管理

「没钱了」这一格,各家的 HTTP 码根本对不上

这是我认为最能说明「不能按 HTTP 码建映射表」的一格。同样是余额或配额耗尽:阶跃星辰的常见错误码表里 402 就是余额不足,组织场景下另有一条 402 带 insufficient_credit 标识;GLM 的账户欠费挂在 429 底下的一个业务码上;Kimi 的欠费与额度不足是 429 加 exceeded_current_quota_error;Gemini 的每日配额耗尽是 429 加 quota_exceeded;MiniMax 的数字码表里是 1008 余额不足,落到 OpenAI 风格错误响应上对应 insufficient_balance_error 与 HTTP 402。

同一个语义,在 HTTP 层被摊成了 402 和 429 两格,而这两格各自还兼着别的差事:429 同时装着可以退避的限流,402 则在 GLM 与 Kimi 的官方错误码页里根本没有出现过。按 HTTP 码做分支的后果很直接——在 GLM、Kimi、Gemini 上,「没钱了」会被识别成限流丢进退避队列,退到天亮也不会好。更麻烦的是同一家内部也不齐:阶跃星辰的组织额度错误里,Credit 账户余额不足是 402,项目达到当期 Credit 上限、成员在该项目内达到当期 Credit 上限却都是 429,官方为此专门在表前写了一句「其中两种情况的错误码与速率限制相同,请以错误标识区分」。判别键必须是「厂商 + 该厂商自己的码」这个二元组,映射到你自己定义的语义分类上。

内容安全被拦是另一条支线,而且不都在错误里

Kimi 的内容审查触发返回 400 加 content_filter,官方给的 message 是请求因被判定为高风险而被拒绝。阶跃星辰用了一个比较少见的状态码 451,含义是请求内容或者响应内容未审核通过。MiniMax 把输入涉敏(1026)与输出涉敏(1027)拆成了两个不同的数字码,这个拆分很有用——前者要改用户的输入,后者是模型自己生成了不该生成的东西,产品上的应对完全不同;在视频生成 v2 的 OpenAI 风格响应里,输入涉敏落到 HTTP 422 加 unprocessable_entity_error。GLM 的对应码在 400 下,描述是系统检测到输入或生成内容可能包含不安全或敏感内容。

Gemini 这边把「生成被阻止」单独做成了一组码,官方列出的取值包括 safetyrecitationlanguageprohibited_contentspiiblocklistimage_safetyimage_prohibited_contentimage_recitationimage_othercontent_blocked,统一的官方处置是修改输入后重试。这一组比其他家细得多,尤其 recitation(版权或引述限制)和 spii(敏感个人身份信息)这两个码,在做内容类应用时值得单独埋点统计。

★ 注意这里有个结构上的分岔:输出被安全策略截断,不一定表现为 HTTP 错误。阶跃星辰的异常处理文档里把它归到「模型层面的异常」,靠 finish_reason 取值为 content_filter 来判断,HTTP 层面是正常的。封装层如果只包了 HTTP 错误分支,这一类会静悄悄地变成一条内容残缺的正常响应流进业务逻辑。

流式请求的错误不一定走 HTTP 状态码

这一节的信息密度最高,也是封装层通常整节没覆盖的地方。

GLM 的错误码页末尾有一条注:使用流式(SSE)调用时,如果 API 在推理过程中异常终止,不会返回上述错误码,而是在响应体的 finish_reason 参数中返回异常原因。

Gemini 的做法又不一样:官方说明标准(非流式)请求是设置 HTTP 状态码加 JSON body 里的 error 对象,而流式请求是通过 SSE 流发送 event_typeerror 的事件,error 字段结构相同。直接后果就是只判 HTTP 状态码的客户端会完全漏掉流式过程中发生的错误——连接建立时是 200,错误在流中间才到。

阶跃星辰给的是模型层异常的处理范式:finish_reasonstop 表示按预定计划结束,length 表示受限于 max_token 未能完整输出,content_filter 表示未通过安全审核,tool_calls 表示模型希望调用函数需要把结果传入下一次请求。官方还给了一段 Python 示例,说明流式下要对每一个 chunk 返回的 finish_reason 分别处理,而不是只看最后一个。

MiniMax 的 Anthropic 兼容端点也走 SSE 下发错误,而且官方把客户端该怎么办一并写了:流式过程中出现错误时会以 event: error 事件下发,body 结构与非流式的错误响应一致,客户端应在收到 error 后停止读取并清理本次会话状态。最后半句值得单独记一笔:它意味着流内错误不是抛个异常就完事,已经攒了一半的增量拼接缓冲区也要一并丢掉,否则复用同一个会话对象时会拼出前后不搭的内容。

Kimi 则补上了连接层的两个码:499 client_closed_request 是客户端在服务端返回前断开连接,官方指出常见于流式响应被中间代理切断或用户主动取消,建议检查 KeepAlive 与超时设置;504 是网关超时,官方建议非流式长请求改用流式输出。

结论很清楚:封装层必须有一条「流内错误」通道,把 SSE 事件里的错误、异常的 finish_reason、以及连接被切断这三种情况,转换成和 HTTP 错误同构的对象抛给上层。否则「为什么线上偶尔返回半截内容」这个问题永远查不清。

参数与上下文超限:措辞五花八门,动作只有两个

GLM 这一格的业务码分得很细:参数有误、模型不存在需检查模型代码、当前模型不支持某种调用方式、未正常接收到某个参数、某参数非法、两个参数不能同时设置、指定 API 已下线、指定 API 不存在、Prompt 超长。

★ 这里藏着一个封装层的实用细节:GLM 的这些 message 里带花括号占位符(比如以 field、API_name、next_flush_time 命名的变量),说明文案是模板渲染出来的。所以绝对不要拿 message 字符串做匹配键——同一个业务码在不同参数下渲染出的文案完全不同,字符串匹配必然漏。判别只能认 code。

Kimi 把上下文超限拆成了两条:输入 token 长度超过模型最大上下文限制,以及 prompt tokens 加 max_tokens 超过模型规格,后者的官方处置是减小 max_tokens 或换模型——注意这两条的修复动作是不同的。MiniMax 的 token 限制码官方也直接给了「请调整 max_tokens」的解法。阶跃星辰的 400 则把图片无法下载、图片数量超过限制、该模型不支持视频输入、模型不存在或无权限、参数值不合法全部归到一起。Grok 额外区分了 415 不支持的媒体类型与 422 请求体某字段格式无效。Gemini 这边是 invalid_requestparameter_unknown,后者的官方处置是移除无法识别的参数后重试——这条在做跨厂商透传时特别常见。

顺带说一个跨家的坑:「模型名写错了」这件事,GLM 归在 400 的业务码里,Kimi 归在 404 resource_not_found_error,Gemini 有专门的 model_not_found 对应 404,阶跃星辰把「模型不存在或无权限」写在 400 的可能原因清单里。同一件事在 HTTP 层就分裂成了 400 与 404 两格,业务码与 type 更是各家一套,映射表里务必给它单开一格——多厂商切换时模型名跟着一起换,这一格一旦漏了,报出来的会是「参数不合法」这种指向完全错误的提示。

一层统一封装该长什么样

把上面的差异收拢,我建议封装成这样一个归一化对象,字段固定:

字段作用为什么必须有
vendor哪一家判别键是「厂商 + 原始码」的二元组,缺一不可
http_status原始 HTTP 状态码网络层与代理层的问题只能靠它定位
vendor_code厂商原始码,原样保留开工单时厂商只认这个
category你自己的语义分类业务代码只依赖它,不依赖任何一家的码
retryable是否允许自动重试由 category 推导,不由 HTTP 码推导
trace厂商的追踪标识各家名字不同,见下
raw原始响应体未知码兜底,事后复盘全靠它

category 这一层建议的枚举是:认证失败、权限不足、额度耗尽、限流、服务端容量、请求非法、模型不存在、内容被拦、输出被截断、上游错误、客户端取消。其中允许自动退避重试的只有限流、服务端容量、上游错误三类;额度耗尽和认证失败必须直接失败并告警。

trace 这一格各家叫法不同,落库时要各自适配:Kimi 的排障建议是持续出现 500 时附带 request_id 联系支持;MiniMax 要求提供 Header 中的 trace_id,而它的 Anthropic 兼容与视频生成 v2 响应体里另有一个 request_id 字段,两个都值得落库;阶跃星辰的故障排查指南要求提供响应头中的 X-Trace-Id 字段以及生成结果中的 id;Grok 则是邮件附上 API 请求、响应与相关日志,另外它给了状态页与 RSS 订阅地址,可以接进你的告警系统做「是不是人家挂了」的快速判断。

最后一定要有未知码兜底分支。Gemini 官方那句「未列出的错误码以标准 HTTP 状态文本的 snake_case 形式返回」正说明各家的码表都在变,映射表命中不了的一律归成上游错误、标记不可自动重试、原样落库告警,比硬猜一个分类安全得多。

最容易栽的三个坑

第一,把 429 一律当限流退避。上面已经看到,GLM、Kimi、阶跃星辰、Gemini 四家的 429 底下都混着额度耗尽或套餐、Credit 上限一类的问题,这些退避多少次都不会好,只会白白烧掉重试预算并且让真实故障晚几分钟才暴露。

第二,只判 HTTP 状态码。流式场景下 GLM 走 finish_reason、Gemini 走 SSE 的错误事件、阶跃星辰要逐 chunk 判断,任何一条漏掉都会变成「偶发的内容不完整」,这类问题在监控上几乎看不见。

第三,把厂商原始码丢掉只留自己的枚举。归一化是给业务代码用的,出问题开工单时厂商只认原始码和追踪 ID。归一化对象里必须同时留着原始响应体。

下一步建议做的事:先只挑你实际在用的那两三家,把上面 category 枚举里的十一类语义各写一条真实的错误样本进单元测试,让映射函数在文档更新时能被测试直接照出来;准备切换或新增厂商时,把错误码映射这一项写进 换厂商的迁移检查清单里,它比模型能力对齐更容易被漏掉,也更容易在上线后炸。以上所有错误码与字段名均以各平台官方文档当前版本为准,各家都在持续更新,映射表要当成会过期的东西来维护。

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