很多人在 Vercel AI SDK 或多模型调用链里看到 AI_ProviderSpecificError(也写成 ProviderSpecificError、AI_ProviderSpecific...)时,第一反应是去官方文档搜这个类——结果一无所获。
先说结论,省得你浪费时间:Vercel AI SDK 官方的错误类列表里没有一个叫 AI_ProviderSpecificError 的导出类。我把官方错误参考页的完整清单核对过一遍,里面是 AI_APICallError、AI_NoSuchModelError、AI_NoSuchProviderError 这一串,没有 ProviderSpecificError。
那你屏幕上这行报错是哪来的?两种可能:要么是某个第三方 provider 包(OpenRouter、AI Gateway、社区 provider)自己包了一层错误,把上游返回的非标准错误重新命名成了带 “provider specific” 字样的东西;要么是你自己的代码 / 框架(LangChain 适配层、内部封装)在 catch 之后重新抛了个自定义错误。
这事儿有个背景。AI SDK 想把所有 provider 抹平成一套统一接口,但每家 provider 的错误格式根本不一样:OpenAI 返回 {"error":{"message":...}},有的网关返回 {"error":[{"code":2005,...}]},还有的 provider 干脆用 HTTP 200 包一个错误 payload 回来。SDK 没法把这些五花八门的格式都归进自己那二十几个标准错误类,剩下的兜底就变成了”provider 特有的、我也不认识的错误”——这正是 “provider specific” 这个名字的由来。换句话说,它不是一个具体故障,是一个”我没法归类”的标签。
不管哪种,定位思路是一样的:这是一个”上游模型 provider 返回了非标准错误,SDK 没法归到已知类型”的信号。它本身不告诉你哪儿坏了,但它指了一条路——真相在原始响应体里,不在错误名字里。下面是怎么把它拆开、查出真实根因、写出能复制的修复。
先认清 AI SDK 真正的错误类层级
排这类错前,你得知道 AI SDK 把错误分成几层。最底层、最常碰到的是 APICallError(在错误参考里写作 AI_APICallError)。只要 provider 的 HTTP 接口返回了非 2xx,或者返回体没法解析,抛的就是它。
APICallError 身上挂着这些字段,全是排错的关键证据:
url:实际请求的接口地址,能看出你打的是不是预期的 endpointstatusCode:HTTP 状态码,401/403/429/400/404/500 直接告诉你是哪类问题responseBody:上游返回的原始响应体(字符串),provider 的真实报错文案在这里responseHeaders:响应头,rate limit 的retry-after在这里requestBodyValues:你发出去的请求体,对照它能查出参数问题isRetryable:SDK 判断这个错能不能重试(基于状态码)data:附加数据,部分 provider 会把解析后的错误对象放这里cause:底层错误(比如网络层的ECONNRESET、JSON 解析失败)
记住一个分工:responseBody / data 装的是 provider 说了什么,cause 装的是 传输/解析这一层出了什么。一个 AI_ProviderSpecificError 之所以”specific”,根因几乎总能在 responseBody 或 cause 里挖出来。
官方推荐的判型方式是静态方法 isInstance,不是裸 instanceof(跨包、跨打包产物时 instanceof 会因为多份类定义而失效):
import { APICallError } from 'ai';
if (APICallError.isInstance(error)) {
// 是一次 provider HTTP 调用失败
}
第一步:把错误对象整个打印出来
排 ProviderSpecificError 最大的坑,是大家只 console.log(error.message)。message 经常被上层包成一句没信息量的话(”Provider specific error occurred”),真正的根因全在子字段里。
第一件事永远是把对象的关键字段全掏出来:
import { APICallError } from 'ai';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: '写一段排错日志分析',
});
console.log(text);
} catch (error) {
if (APICallError.isInstance(error)) {
console.error('name :', error.name);
console.error('statusCode :', error.statusCode);
console.error('url :', error.url);
console.error('isRetryable :', error.isRetryable);
console.error('responseBody:', error.responseBody);
console.error('cause :', error.cause);
} else {
// 不是 APICallError,可能是 provider 包自定义的错误
console.error('non-APICallError:', error);
}
}
如果你的 error 连 APICallError.isInstance 都不为真,但名字里带 “ProviderSpecific”,那它来自某个 provider 包的自定义错误类。这种情况下直接 JSON.stringify 兜底,把整个对象(含原型链上够不到的 enumerable 字段)拍平看一眼:
} catch (error) {
console.error(JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
}
Object.getOwnPropertyNames 这步很关键——Error 对象的 message / stack 默认是 non-enumerable,直接 JSON.stringify(error) 会得到一个空 {},很多人就是卡在这一步以为”没有信息”。
第二步:按 statusCode 对号入座
拿到 statusCode 和 responseBody 之后,绝大多数 ProviderSpecificError 能直接归类。下面是高频根因和对应的判断点。
401 / 403 — 认证问题。 Key 没传、传错环境、或者用了别的 provider 的 key。responseBody 里通常有 invalid_api_key 或 Forbidden by auth provider 字样。如果你走的是 AI Gateway / OpenRouter 这类网关,还要注意:网关自己的 token 和上游 provider 的 key 是两个 header,别塞混了。
404 — 模型不存在或路由错。 model id 拼错、provider 不支持这个模型、或者 endpoint base URL 配错。注意 AI SDK 还有个独立的 AI_NoSuchModelError(字段 modelId / modelType),那是 SDK 在本地 registry 里就没找到模型时抛的;而 404 是请求打到上游、上游说”没这模型”。两者根因不同:
import { NoSuchModelError } from 'ai';
if (NoSuchModelError.isInstance(error)) {
// 本地 provider registry 里就没注册这个 model id —— 拼写/注册问题
console.error('未注册的模型:', error.modelId, error.modelType);
}
429 — 限流。 这是 isRetryable 为 true 的典型。responseHeaders['retry-after'] 会告诉你等多久。别盲目立刻重试,按 header 退避。
400 — 参数被这个 provider 拒了。 最容易伪装成 “ProviderSpecificError” 的就是它:你用了一个 A provider 支持、B provider 不支持的参数(比如某些 provider 不吃 responseFormat: json_schema、不支持 tools、不支持 temperature 之外的采样参数)。responseBody 里会指名道姓说哪个字段不合法。这种错重试一万遍也没用,必须改请求。
5xx — 上游自己挂了。 provider 后端故障,跟你的代码没关系。可重试,但要有上限和降级,别陷进无限重试把自己也拖垮。
这里要特别提一个 LangChain / 多模型框架里常见的迷惑场景:你用 LangChain 的 provider 适配层调 AI SDK,或者反过来,错误经常被套两三层。最外面那层是框架的自定义错误(名字里可能就带 “ProviderSpecific”),中间是 AI SDK 的 APICallError,最里面才是上游真实的 responseBody。这种时候别只看最外层,顺着 error.cause 一层层往里剥,直到剥出带 statusCode 的那一层。cause 链就是你的排错地图。
第三步:按错误类型分流处理(可复制模板)
把上面几步固化成一个能直接抄进项目的处理函数。它做三件事:判型、提取真实根因、给出”能不能重试”的明确信号。
import {
APICallError,
NoSuchModelError,
InvalidArgumentError,
RetryError,
} from 'ai';
type Verdict =
| { kind: 'auth'; retry: false; detail: string }
| { kind: 'rate_limit'; retry: true; retryAfter?: number; detail: string }
| { kind: 'bad_request'; retry: false; detail: string }
| { kind: 'not_found'; retry: false; detail: string }
| { kind: 'upstream'; retry: true; detail: string }
| { kind: 'unknown'; retry: false; detail: string };
export function classifyError(error: unknown): Verdict {
// 本地 registry 没这个模型,和 404 不是一回事
if (NoSuchModelError.isInstance(error)) {
return { kind: 'not_found', retry: false, detail: `未注册模型 ${error.modelId}` };
}
// 参数本身非法,改参数才有用
if (InvalidArgumentError.isInstance(error)) {
return { kind: 'bad_request', retry: false, detail: error.message };
}
if (APICallError.isInstance(error)) {
const status = error.statusCode;
// 真实根因优先从 responseBody 取
const detail = error.responseBody ?? error.message;
if (status === 401 || status === 403) {
return { kind: 'auth', retry: false, detail };
}
if (status === 429) {
const ra = error.responseHeaders?.['retry-after'];
return {
kind: 'rate_limit',
retry: true,
retryAfter: ra ? Number(ra) : undefined,
detail,
};
}
if (status === 404) {
return { kind: 'not_found', retry: false, detail };
}
if (status && status >= 400 && status < 500) {
return { kind: 'bad_request', retry: false, detail };
}
if (status && status >= 500) {
return { kind: 'upstream', retry: true, detail };
}
// 没有状态码,多半是网络层,看 cause
return { kind: 'upstream', retry: !!error.isRetryable, detail: String(error.cause ?? detail) };
}
return { kind: 'unknown', retry: false, detail: String(error) };
}
注意 InvalidArgumentError(参考里写作 AI_InvalidArgumentError)和 RetryError(AI_RetryError)都是官方真实存在的类,前者是参数非法,后者是 SDK 重试若干次后仍失败、把每次的错误打包抛出。
调用侧拿到 Verdict 后逻辑就很干净了:
const verdict = classifyError(error);
if (verdict.kind === 'auth') {
// 别重试,直接告警:key 配错了
throw new Error(`Auth 失败,检查 API key:${verdict.detail}`);
}
if (verdict.kind === 'rate_limit') {
const wait = (verdict.retryAfter ?? 2) * 1000;
await new Promise((r) => setTimeout(r, wait));
// 退避后再试
}
if (verdict.kind === 'bad_request') {
// 改请求,不要重试。这里大概率就是你看到 ProviderSpecificError 的真身
throw new Error(`参数被 provider 拒绝:${verdict.detail}`);
}
第四步:多 provider 降级,把”specific”变成”通用”
如果你在做多模型调用(一个 provider 挂了切下一个),ProviderSpecificError 的真正价值是当成”换 provider”的触发信号。关键是别把”参数错”也拿去重试别的 provider——那只会让同样的参数在每个 provider 上挨个报一遍。
只对 retry: true 的类型做 fallback:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
const chain = [openai('gpt-4o'), anthropic('claude-sonnet-4-5')];
async function generateWithFallback(prompt: string) {
let lastDetail = '';
for (const model of chain) {
try {
return await generateText({ model, prompt });
} catch (error) {
const v = classifyError(error);
lastDetail = v.detail;
// 只有"可重试"的错才换下一个 provider
if (!v.retry) throw error; // 参数错/认证错,换谁都一样,直接抛
// 否则继续 for 循环,尝试下一个 provider
}
}
throw new Error(`所有 provider 都失败,最后原因:${lastDetail}`);
}
这套逻辑能把”看起来玄学的 AI_ProviderSpecificError“压成两类确定行为:能换就换,不能换就停。漫无目的的整体 retry 是这类报错被放大的主因——我见过有人把整个调用裹一个 retry 3 次,结果一个 400 参数错被原样重放三遍,再 fallback 到另外两个 provider 又各放三遍,日志里九条一模一样的报错,看着像系统崩了,其实就一个字段写错了。
判断”能不能换 provider”的标准很简单:这个错跟我发的内容有没有关系。认证错、参数错、模型不存在,都是”我这边的问题”,换谁都一样,立刻停下来改代码。限流、5xx、网络抖动,是”对方这边的问题”,换一家或者等一会儿就好。classifyError 返回的 retry 字段就是在表达这个判断,别让它形同虚设。
流式调用里的坑
如果你用 streamText 而不是 generateText,错误不一定从外层 try/catch 出来。简单流(直接遍历 textStream)会把错误抛出来,能 catch;但 full stream 的错误是作为流里的一个 error part 出现的,你得在循环里接:
const { fullStream } = streamText({ model: openai('gpt-4o'), prompt });
for await (const part of fullStream) {
if (part.type === 'error') {
const v = classifyError(part.error);
console.error('stream 内部错误:', v.kind, v.detail);
break;
}
if (part.type === 'text-delta') {
process.stdout.write(part.text);
}
}
很多人排 ProviderSpecificError 排到一半发现 catch 块根本没进,就是因为用了流式但只在外层包了 try/catch。先确认你的错误是从哪条路径冒出来的,再决定在哪儿接。
还有一种更隐蔽的情况:流已经开始吐内容了,吐到一半 provider 那边断了或限流了。这时前半段文本你已经拿到、甚至已经渲染给用户看了,错误才在 error part 里冒出来。处理逻辑要考虑”已经吐出去的部分怎么办”——是回滚、是标记不完整、还是接着用 fallback provider 续写,取决于你的业务。但前提仍然是:你得在循环里接住那个 error part,而不是指望外层 catch。
验证修复有没有真的生效
改完别凭感觉。最快的验证是手动构造每一类错误,看你的 classifyError 是不是都归对了类。
故意触发认证错(用一个假 key):
OPENAI_API_KEY=sk-invalid-key-for-test node ./test-error.mjs
故意触发模型不存在(把 model id 写成不存在的):
await generateText({ model: openai('gpt-this-does-not-exist'), prompt: 'hi' });
跑完看日志:401 应该归到 auth、不可重试;不存在的模型归到 not_found。两条都对,说明你的分流逻辑站得住,之后线上再冒 ProviderSpecificError 就不会再两眼一抹黑。
关联阅读
排这类多模型调用错,本质是 agent 工程的一部分。如果你在搭基于工具调用的 agent,可以顺带看下站内的相关整理:
- AI Agent 专题:agent 编排、工具调用、错误恢复这条线的文章合集
- Codex 专题:OpenAI 系工具链的实战排错
- Claude Code 专题:Anthropic 模型在 agent 场景里的用法
- 更多排错与工程实践见首页
收个尾
记住三句话就够用:AI_ProviderSpecificError 不是官方类,真身在 responseBody 和 cause 里;先按 statusCode 对号入座,400/401 改代码、429/5xx 才重试;流式调用的错走的是 error part,不一定进外层 catch。
踩坑提醒:JSON.stringify(error) 直接打印会得到空对象,掏 Error 信息要用 Object.getOwnPropertyNames(error) 当第二参数。
toy







