fix(core): read provider errors from plain AI SDK stream errors (#51194)

This commit is contained in:
Aiden Cline
2026-09-24 13:52:32 -05:00
committed by GitHub
parent 20610e6645
commit e796f2f9a5
3 changed files with 43 additions and 9 deletions
+7 -1
View File
@@ -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.
+18 -8
View File
@@ -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
}
+18
View File
@@ -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"), {