From e796f2f9a55ba24a87aead44dd6da42bcc0666d9 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:52:32 -0500 Subject: [PATCH] fix(core): read provider errors from plain AI SDK stream errors (#51194) --- packages/ai/src/provider-error.ts | 8 +++++++- packages/core/src/aisdk.ts | 26 ++++++++++++++++++-------- packages/core/test/aisdk.test.ts | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts index a91b876c9cf..746bef2e238 100644 --- a/packages/ai/src/provider-error.ts +++ b/packages/ai/src/provider-error.ts @@ -80,7 +80,13 @@ const SERVER_CODES = new Set([ "slow_down", "serviceunavailableexception", ]) -const INVALID_REQUEST_CODES = new Set(["invalid_prompt", "invalid_request_error", "validationexception"]) +// `invalid_request` is the Vercel AI Gateway's code for an upstream request rejection. +const INVALID_REQUEST_CODES = new Set([ + "invalid_prompt", + "invalid_request", + "invalid_request_error", + "validationexception", +]) // Azure OpenAI reports `content_filter` with `innererror.code` ResponsibleAIPolicyViolation. // OpenRouter tags provider failures with a typed `error_type`; its Responses skin also // emits `image_content_policy_violation` as the native code. diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 95f081f0023..175781b9c66 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -24,7 +24,6 @@ import { ProviderMetadata, TransportError, ToolResultValue, - UnknownProviderError, type ContentPart, type LLMRequest, type Media, @@ -936,15 +935,18 @@ function llmError(error: unknown, operation: "request" | "read") { code: network.code, }), }) - return new AIError({ - reason: new UnknownProviderError({ - message: unknownErrorMessage(error), - body: errorBody(error), - cause: error, - }), + return RequestExecutor.httpFailure({ + message: unknownErrorMessage(error), + data: errorValue(error) ?? error, + responseBody: errorBody(error), + cause: error, }) } +// AI SDK stream errors can arrive as plain objects. A gateway's type validation error keeps the provider's error +// response in `value`, which carries the message and codes. +const errorValue = (error: unknown) => (ProviderShared.isRecord(error) ? error.value : undefined) + // Runtime-generated network failure shapes. The codes mirror the AI SDK's own // Bun network error list in handleFetchError; the messages are undici's fetch // TypeError and stream termination strings plus our SSE chunk timeout error. @@ -1032,7 +1034,15 @@ const decodeProviderError = Schema.decodeUnknownOption( ) function unknownErrorMessage(error: unknown) { - const message = error instanceof Error ? error.message : String(error) + const message = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : ([error, errorValue(error)] + .map((value) => Option.getOrUndefined(decodeProviderError(value))) + .flatMap((decoded) => [decoded?.error?.message, decoded?.message]) + .find((value) => value?.trim()) ?? "") return message.trim() === "" ? "Provider request failed" : message } diff --git a/packages/core/test/aisdk.test.ts b/packages/core/test/aisdk.test.ts index e199359f910..354f8698ba4 100644 --- a/packages/core/test/aisdk.test.ts +++ b/packages/core/test/aisdk.test.ts @@ -13,6 +13,7 @@ import { CompactionPart, ProviderID, HttpContext, + InvalidRequestError, LLMEvent, Message, RateLimitError, @@ -883,6 +884,23 @@ Object.values({ ) }) +// Shapes the Vercel AI Gateway streams when the upstream rejects a request. +Object.entries({ + "type validation": { + name: "AI_TypeValidationError", + value: { error: { type: "invalid_request_error", message: "Bad max_tokens" } }, + }, + invalid_request: { code: "invalid_request", message: "Bad max_tokens" }, +}).forEach(([shape, failure]) => + it.effect(`reads gateway ${shape} stream errors as invalid requests`, () => + Effect.gen(function* () { + const error = yield* streamFailure(failure, true) + expect(error.message).toBe("Bad max_tokens") + expect(error.reason).toBeInstanceOf(InvalidRequestError) + }), + ), +) + it.effect("does not copy Error request internals into the provider body", () => Effect.gen(function* () { const cause = Object.assign(new Error("Connection failed"), {