云聚 AI Token Plan 满 199 减 35 元
port:80 AI Junkie
AI 重度玩家的工程笔记本
DigitalOcean 开发者云
AI_ProviderSpecificError 报错定位与修复:AI SDK 排错 封面

AI_ProviderSpecificError 报错定位与修复:AI SDK 排错

28 min read 阅读() #AI Agent 框架
#AI Agent 框架
目录

很多人在 Vercel AI SDK 或多模型调用链里看到 AI_ProviderSpecificError(也写成 ProviderSpecificErrorAI_ProviderSpecific...)时,第一反应是去官方文档搜这个类——结果一无所获。

先说结论,省得你浪费时间:Vercel AI SDK 官方的错误类列表里没有一个叫 AI_ProviderSpecificError 的导出类。我把官方错误参考页的完整清单核对过一遍,里面是 AI_APICallErrorAI_NoSuchModelErrorAI_NoSuchProviderError 这一串,没有 ProviderSpecificError

阿里云 OPC 一人公司创业装备库

那你屏幕上这行报错是哪来的?两种可能:要么是某个第三方 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:实际请求的接口地址,能看出你打的是不是预期的 endpoint
  • statusCode: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”,根因几乎总能在 responseBodycause 里挖出来。

官方推荐的判型方式是静态方法 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);
  }
}

如果你的 errorAPICallError.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 对号入座

拿到 statusCoderesponseBody 之后,绝大多数 ProviderSpecificError 能直接归类。下面是高频根因和对应的判断点。

401 / 403 — 认证问题。 Key 没传、传错环境、或者用了别的 provider 的 key。responseBody 里通常有 invalid_api_keyForbidden 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 — 限流。 这是 isRetryabletrue 的典型。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)和 RetryErrorAI_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_ProviderSpecificError 不是官方类,真身在 responseBodycause 里;先按 statusCode 对号入座,400/401 改代码、429/5xx 才重试;流式调用的错走的是 error part,不一定进外层 catch。

踩坑提醒:JSON.stringify(error) 直接打印会得到空对象,掏 Error 信息要用 Object.getOwnPropertyNames(error) 当第二参数。

toy

未经允许不得转载:80aj » AI_ProviderSpecificError 报错定位与修复:AI SDK 排错
阿里云函数计算 一键部署 AI 大模型

Claude Code 合租 · KYC 封号全托管

官方又涨价又 KYC,封号还得自己重新折腾?ReClaude 拼车了解一下——200 / 400 / 800 / 1600 四档随便挑,账号、风控、切换全平台托管,触发风控自动换号不计次。

上车 4 人车 400/月查看四档套餐