diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts index 35b9cff7425..cfe108afa1d 100644 --- a/packages/ai/src/provider-error.ts +++ b/packages/ai/src/provider-error.ts @@ -147,6 +147,8 @@ const CONTENT_POLICY_CODES = new Set([ // OpenCode Zen replaces upstream codes outside its allow-list but keeps the original // as a `[code]` label at the start of the rewritten message. const GATEWAY_CODE_LABEL = /^[^:\n]+: \[([A-Za-z0-9_.-]+)\]/ +// xAI reports an invalid API key as HTTP 400 with the generic `invalid-argument` code. +const AUTH_TEXT = /incorrect api key provided/i const RATE_LIMIT_TEXT = /rate increased too quickly|rate[-_\s]?limit|too[_\s]?many[_\s]?requests/i // Only consulted on 429, where throttles and account caps share a status. const QUOTA_TEXT = /insufficient[-_\s]?quota|quota[-_\s]?exceeded|budget exceeded|usage limit/i @@ -246,7 +248,12 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason (input.status === 429 && QUOTA_TEXT.test(text)) ) return new QuotaExceededError(details) - if (input.status === 401 || input.status === 403 || codes.some((code) => AUTH_CODES.has(code))) + if ( + input.status === 401 || + input.status === 403 || + codes.some((code) => AUTH_CODES.has(code)) || + (input.status === 400 && AUTH_TEXT.test(text)) + ) return new AuthenticationError(details) if ( input.status === 429 || diff --git a/packages/ai/test/provider-error.test.ts b/packages/ai/test/provider-error.test.ts index 98a94c1b143..c342ea60a55 100644 --- a/packages/ai/test/provider-error.test.ts +++ b/packages/ai/test/provider-error.test.ts @@ -383,7 +383,7 @@ describe("provider error rawBody classification", () => { expect(reason._tag === "InvalidRequest" ? reason.classification : reason._tag).toBe("context-overflow") }) - test("classifies Google invalid API keys as authentication failures", () => { + test("classifies invalid API keys reported as HTTP 400 as authentication failures", () => { const rawBody = JSON.stringify({ error: { code: 400, @@ -407,6 +407,14 @@ describe("provider error rawBody classification", () => { classifyProviderFailure({ message: "API key not valid. Please pass a valid API key.", status: 400, rawBody }) ._tag, ).toBe("Authentication") + // xAI + expect( + classifyProviderFailure({ + message: "Incorrect API key provided. You can obtain an API key from https://console.x.ai.", + status: 400, + rawBody: '{"code":"invalid-argument","error":"Incorrect API key provided."}', + })._tag, + ).toBe("Authentication") }) test("classifies overflow signals buried in the raw payload when the summary is vague", () => {