diff --git a/packages/ai/src/provider-error.ts b/packages/ai/src/provider-error.ts index 7408f609c03..b1f3c0a70f2 100644 --- a/packages/ai/src/provider-error.ts +++ b/packages/ai/src/provider-error.ts @@ -250,6 +250,7 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason ...providerCodes(body), ...providerCodes(input.message), ...(GATEWAY_CODE_LABEL.exec(input.message)?.slice(1) ?? []), + ...awsErrorType(input.http?.headers["x-amzn-errortype"]), ].map((code) => code.toLowerCase()) // Scan the raw payload too so signals missing from the summary message // (e.g. overflow phrases nested in a JSON error body) still classify. @@ -325,6 +326,8 @@ function providerCodes(value: unknown) { const exception = isRecord(decoded.exception) ? decoded.exception : undefined return [ decoded.code, + ...awsErrorType(decoded.__type), + ...awsErrorType(decoded.code), // Stability's `{ id, name, errors }` bodies carry the code in `name`. Array.isArray(decoded.errors) ? decoded.name : undefined, decoded.error_type, @@ -343,3 +346,10 @@ function providerCodes(value: unknown) { exception?.type, ].filter((value): value is string => typeof value === "string") } + +// AWS restJson1 reports the modeled error in `X-Amzn-Errortype`, `__type` or `code`, possibly +// namespaced (`ns#Name`) or suffixed (`Name:uri`). Keep the shape name only. +// https://smithy.io/2.0/aws/protocols/aws-restjson1-protocol.html#operation-error-serialization +function awsErrorType(value: unknown) { + return typeof value === "string" ? [value.replace(/:.*/s, "").replace(/^[^#]*#/, "")] : [] +} diff --git a/packages/ai/test/executor.test.ts b/packages/ai/test/executor.test.ts index 07baf92bbda..ca15902a447 100644 --- a/packages/ai/test/executor.test.ts +++ b/packages/ai/test/executor.test.ts @@ -75,6 +75,29 @@ describe("RequestExecutor", () => { }), ) + it.effect("classifies AWS modeled errors from every restJson1 identity location", () => + Effect.sync(() => { + const cases = [ + { body: { code: "ThrottlingException" } }, + { body: { __type: "ThrottlingException" } }, + { body: { code: "com.amazonaws.bedrock#ThrottlingException:http://internal.amazon.com/coral/" } }, + { body: {}, headers: { "X-Amzn-Errortype": "ThrottlingException:http://internal.amazon.com/coral/" } }, + ] + for (const item of cases) { + const body = JSON.stringify({ ...item.body, message: "Slow down" }) + const error = RequestExecutor.httpFailure({ + message: "Slow down", + url: request.url, + status: 400, + responseHeaders: item.headers, + responseBody: body, + }) + expect(error.reason._tag).toBe("RateLimit") + expect(error.reason.body).toBe(body) + } + }), + ) + it.effect("retains the original body-read failure on an HTTP status error", () => Effect.gen(function* () { const cause = new Error("response body disconnected")