fix(ai): surface provider HTTP error body instead of opaque SDK message
Add a shared normalizeProviderError helper in packages/ai/src/utils/error-body.ts and route the 8 body-blind / status-only providers through it (amazon-bedrock, azure-openai-responses, google, google-vertex, images/openrouter, openai-codex-responses, openai-completions, openai-responses). Non-schema 4xx/5xx responses from proxies / gateways now show the real reason carried in the response body alongside the HTTP status, instead of "403 status code (no body)" or "Unknown: UnknownError". The helper probes status (statusCode, status, $metadata.httpStatusCode, $response.statusCode) and body (body, parsed error object, $response.body) across the Mistral, OpenAI, Google, and Bedrock SDK shapes, truncates the body at a 4000 char cap, and preserves error.message when the SDK already folded the body in (Anthropic / Google happy path). mistral.ts and anthropic.ts are left untouched. Provider prefixes and the OpenRouter metadata.raw append are preserved. closes #5763
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { headersToRecord } from "../utils/headers.ts";
|
||||
import { getProviderEnvValue } from "../utils/provider-env.ts";
|
||||
@@ -44,19 +45,7 @@ function resolveDeploymentName(model: Model<"azure-openai-responses">, options?:
|
||||
}
|
||||
|
||||
function formatAzureOpenAIError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const status = (error as Error & { status?: unknown }).status;
|
||||
const statusCode = typeof status === "number" ? status : undefined;
|
||||
if (statusCode !== undefined) {
|
||||
return `Azure OpenAI API error (${statusCode}): ${error.message}`;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
return formatProviderError(normalizeProviderError(error), "Azure OpenAI API error");
|
||||
}
|
||||
|
||||
// Azure OpenAI Responses-specific options
|
||||
|
||||
@@ -47,6 +47,7 @@ import type {
|
||||
ToolCall,
|
||||
ToolResultMessage,
|
||||
} from "../types.ts";
|
||||
import { normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { providerHeadersToRecord } from "../utils/headers.ts";
|
||||
import { parseStreamingJson } from "../utils/json-parse.ts";
|
||||
@@ -322,15 +323,22 @@ const BEDROCK_DATA_RETENTION_DOCS_URL = "https://docs.aws.amazon.com/bedrock/lat
|
||||
* detection) can distinguish error categories via simple string matching.
|
||||
*/
|
||||
function formatBedrockError(error: unknown): string {
|
||||
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
const dataRetentionHint = /data retention mode/i.test(message)
|
||||
const norm = normalizeProviderError(error);
|
||||
// Surface the raw HTTP body (with status) when the SDK did not fold it into
|
||||
// the message; otherwise fall back to the message. This is what stops a
|
||||
// gateway 403 from collapsing to `Unknown: UnknownError`.
|
||||
const core =
|
||||
!norm.messageCarriesBody && norm.status !== undefined && norm.body !== undefined
|
||||
? `${norm.status}: ${norm.body}`
|
||||
: norm.message;
|
||||
const dataRetentionHint = /data retention mode/i.test(core)
|
||||
? ` See ${BEDROCK_DATA_RETENTION_DOCS_URL} for supported data retention modes.`
|
||||
: "";
|
||||
if (error instanceof BedrockRuntimeServiceException) {
|
||||
const prefix = BEDROCK_ERROR_PREFIXES[error.name] ?? error.name;
|
||||
return `${prefix}: ${message}${dataRetentionHint}`;
|
||||
return `${prefix}: ${core}${dataRetentionHint}`;
|
||||
}
|
||||
return `${message}${dataRetentionHint}`;
|
||||
return `${core}${dataRetentionHint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
ThinkingLevel,
|
||||
ToolCall,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { providerHeadersToRecord } from "../utils/headers.ts";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
|
||||
@@ -271,7 +272,7 @@ export const stream: StreamFunction<"google-generative-ai", GoogleOptions> = (
|
||||
}
|
||||
}
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
ThinkingContent,
|
||||
ToolCall,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { providerHeadersToRecord } from "../utils/headers.ts";
|
||||
import { getProviderEnvValue } from "../utils/provider-env.ts";
|
||||
@@ -288,7 +289,7 @@ export const stream: StreamFunction<"google-vertex", GoogleVertexOptions> = (
|
||||
}
|
||||
}
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
createAssistantMessageDiagnostic,
|
||||
formatThrownValue,
|
||||
} from "../utils/diagnostics.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { headersToRecord } from "../utils/headers.ts";
|
||||
import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts";
|
||||
@@ -411,7 +412,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
||||
delete (block as { partialJson?: string }).partialJson;
|
||||
}
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : String(error);
|
||||
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import type {
|
||||
ToolCall,
|
||||
ToolResultMessage,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { headersToRecord } from "../utils/headers.ts";
|
||||
import { parseStreamingJson } from "../utils/json-parse.ts";
|
||||
@@ -463,10 +464,15 @@ export const stream: StreamFunction<"openai-completions", OpenAICompletionsOptio
|
||||
delete (block as { streamIndex?: number }).streamIndex;
|
||||
}
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
||||
// Some providers via OpenRouter give additional information in this field.
|
||||
// normalizeProviderError already stringifies the parsed body (error.error)
|
||||
// into errorMessage, so only append the raw metadata when it is not already
|
||||
// present to avoid double-printing it.
|
||||
const rawMetadata = (error as any)?.error?.metadata?.raw;
|
||||
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
||||
if (rawMetadata && !output.errorMessage.includes(String(rawMetadata))) {
|
||||
output.errorMessage += `\n${rawMetadata}`;
|
||||
}
|
||||
stream.push({ type: "error", reason: output.stopReason, error: output });
|
||||
stream.end();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
StreamOptions,
|
||||
Usage,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { headersToRecord } from "../utils/headers.ts";
|
||||
import { getProviderEnvValue } from "../utils/provider-env.ts";
|
||||
@@ -70,19 +71,7 @@ function getPromptCacheRetention(
|
||||
}
|
||||
|
||||
function formatOpenAIResponsesError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const status = (error as Error & { status?: unknown }).status;
|
||||
const statusCode = typeof status === "number" ? status : undefined;
|
||||
if (statusCode !== undefined) {
|
||||
return `OpenAI API error (${statusCode}): ${error.message}`;
|
||||
}
|
||||
return error.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch {
|
||||
return String(error);
|
||||
}
|
||||
return formatProviderError(normalizeProviderError(error), "OpenAI API error");
|
||||
}
|
||||
|
||||
// OpenAI Responses-specific options
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
ProviderHeaders,
|
||||
TextContent,
|
||||
} from "../types.ts";
|
||||
import { formatProviderError, normalizeProviderError } from "../utils/error-body.ts";
|
||||
import { headersToRecord, providerHeadersToRecord } from "../utils/headers.ts";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
|
||||
|
||||
@@ -99,7 +100,7 @@ export const generateImages: ImagesFunction<"openrouter-images", ImagesOptions>
|
||||
return output;
|
||||
} catch (error) {
|
||||
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
||||
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
||||
output.errorMessage = formatProviderError(normalizeProviderError(error));
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user