Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/ai/src/models.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12298,13 +12298,13 @@ export const MODELS = {
reasoning: true,
input: ["text", "image"],
cost: {
input: 0.39,
output: 2.34,
input: 0.385,
output: 2.45,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 65536,
contextWindow: 256000,
maxTokens: 4096,
} satisfies Model<"openai-completions">,
"qwen/qwen3.5-9b": {
id: "qwen/qwen3.5-9b",
Expand Down Expand Up @@ -12587,13 +12587,13 @@ export const MODELS = {
reasoning: true,
input: ["text"],
cost: {
input: 0.063,
output: 0.21,
cacheRead: 0.021,
input: 0.066,
output: 0.26,
cacheRead: 0.029,
cacheWrite: 0,
},
contextWindow: 262144,
maxTokens: 4096,
maxTokens: 262144,
} satisfies Model<"openai-completions">,
"thedrummer/rocinante-12b": {
id: "thedrummer/rocinante-12b",
Expand Down
12 changes: 8 additions & 4 deletions packages/ai/src/providers/amazon-bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { createHttpProxyAgentsForTarget } from "../utils/node-http-proxy.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { formatProviderError } from "./error-utils.ts";
import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";

Expand Down Expand Up @@ -305,15 +306,18 @@ 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 formatted = formatProviderError(error, "Bedrock");
const dataRetentionHint = /data retention mode/i.test(formatted)
? ` 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}`;
const detail = formatted.startsWith("Bedrock API error")
? formatted.slice("Bedrock API error".length).trim()
: formatted;
return `${prefix}: ${detail}${dataRetentionHint}`;
}
return `${message}${dataRetentionHint}`;
return `${formatted}${dataRetentionHint}`;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/ai/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.ts"
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";

import { resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { formatProviderError } from "./error-utils.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";
Expand Down Expand Up @@ -706,7 +707,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti
delete (block as { partialJson?: string }).partialJson;
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
output.errorMessage = formatProviderError(error, "Anthropic");
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
Expand Down
15 changes: 2 additions & 13 deletions packages/ai/src/providers/azure-openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { formatProviderError } from "./error-utils.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
Expand Down Expand Up @@ -41,19 +42,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(error, "Azure OpenAI");
}

// Azure OpenAI Responses-specific options
Expand Down
109 changes: 109 additions & 0 deletions packages/ai/src/providers/error-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
export function formatProviderError(error: unknown, providerName: string): string {
if (error instanceof Error) {
const sdkError = error as Error & {
statusCode?: unknown;
status?: unknown;
body?: unknown;
error?: unknown;
$response?: { statusCode?: unknown; body?: unknown };
$metadata?: { httpStatusCode?: unknown };
};

// 1. Extract status code
const status =
typeof sdkError.statusCode === "number"
? sdkError.statusCode
: typeof sdkError.status === "number"
? sdkError.status
: typeof sdkError.$response?.statusCode === "number"
? sdkError.$response.statusCode
: typeof sdkError.$metadata?.httpStatusCode === "number"
? sdkError.$metadata.httpStatusCode
: undefined;

// 2. Extract raw body text
let bodyText: string | undefined;
const rawBody = sdkError.body ?? sdkError.$response?.body;
if (typeof rawBody === "string") {
bodyText = rawBody.trim();
} else if (rawBody && typeof rawBody === "object") {
if (rawBody instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(rawBody))) {
bodyText = new TextDecoder().decode(rawBody as Uint8Array).trim();
} else {
try {
bodyText = JSON.stringify(rawBody).trim();
} catch {
// Ignore stringify error
}
}
}

// Also check sdkError.error if bodyText is not found (OpenAI APIError uses .error for parsed JSON)
if (!bodyText && sdkError.error) {
if (typeof sdkError.error === "string") {
bodyText = sdkError.error.trim();
} else if (typeof sdkError.error === "object") {
const errObj = sdkError.error as Record<string, unknown>;
if (typeof errObj.message === "string") {
bodyText = errObj.message.trim();
} else {
try {
bodyText = JSON.stringify(sdkError.error).trim();
} catch {
// Ignore stringify error
}
}
}
}

// 3. Try to parse error message if it's a JSON string (e.g. from Google SDK)
let parsedMsg: string | undefined;
if (error.message.startsWith("{") && error.message.endsWith("}")) {
try {
const parsed = JSON.parse(error.message) as Record<string, unknown>;
// Google Gen AI error shape: { error: { message, code, status } }
if (parsed && typeof parsed === "object") {
const innerError = (parsed.error || parsed) as Record<string, unknown>;
if (innerError && typeof innerError === "object") {
const msgVal = innerError.message || innerError.statusText || innerError.status;
if (typeof msgVal === "string") {
parsedMsg = msgVal;
} else {
parsedMsg = JSON.stringify(innerError);
}
}
}
} catch {
// Ignore parse error
}
}

const detailMessage = parsedMsg || error.message;

// 4. Combine status code and body/message
if (status !== undefined) {
if (bodyText) {
return `${providerName} API error (${status}): ${truncateErrorText(bodyText, 4000)}`;
}
return `${providerName} API error (${status}): ${detailMessage}`;
}

return detailMessage;
}

return safeJsonStringify(error);
}

function truncateErrorText(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;
}

function safeJsonStringify(value: unknown): string {
try {
const serialized = JSON.stringify(value);
return serialized === undefined ? String(value) : serialized;
} catch {
return String(value);
}
}
3 changes: 2 additions & 1 deletion packages/ai/src/providers/google-vertex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { formatProviderError } from "./error-utils.ts";
import type { GoogleThinkingLevel } from "./google-shared.ts";
import {
convertMessages,
Expand Down Expand Up @@ -283,7 +284,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex", GoogleVertexOpt
}
}
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
output.errorMessage = formatProviderError(error, "Vertex");
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
Expand Down
3 changes: 2 additions & 1 deletion packages/ai/src/providers/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
} from "../types.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { formatProviderError } from "./error-utils.ts";
import type { GoogleThinkingLevel } from "./google-shared.ts";
import {
convertMessages,
Expand Down Expand Up @@ -268,7 +269,7 @@ export const streamGoogle: 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(error, "Google");
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
Expand Down
28 changes: 2 additions & 26 deletions packages/ai/src/providers/mistral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { shortHash } from "../utils/hash.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { formatProviderError } from "./error-utils.ts";
import { buildBaseOptions } from "./simple-options.ts";
import { transformMessages } from "./transform-messages.ts";

const MISTRAL_TOOL_CALL_ID_LENGTH = 9;
const MAX_MISTRAL_ERROR_BODY_CHARS = 4000;

/**
* Provider-specific options for the Mistral API.
Expand Down Expand Up @@ -183,31 +183,7 @@ function deriveMistralToolCallId(id: string, attempt: number): string {
}

function formatMistralError(error: unknown): string {
if (error instanceof Error) {
const sdkError = error as Error & { statusCode?: unknown; body?: unknown };
const statusCode = typeof sdkError.statusCode === "number" ? sdkError.statusCode : undefined;
const bodyText = typeof sdkError.body === "string" ? sdkError.body.trim() : undefined;
if (statusCode !== undefined && bodyText) {
return `Mistral API error (${statusCode}): ${truncateErrorText(bodyText, MAX_MISTRAL_ERROR_BODY_CHARS)}`;
}
if (statusCode !== undefined) return `Mistral API error (${statusCode}): ${error.message}`;
return error.message;
}
return safeJsonStringify(error);
}

function truncateErrorText(text: string, maxChars: number): string {
if (text.length <= maxChars) return text;
return `${text.slice(0, maxChars)}... [truncated ${text.length - maxChars} chars]`;
}

function safeJsonStringify(value: unknown): string {
try {
const serialized = JSON.stringify(value);
return serialized === undefined ? String(value) : serialized;
} catch {
return String(value);
}
return formatProviderError(error, "Mistral");
}

function buildRequestOptions(model: Model<"mistral-conversations">, options?: MistralOptions) {
Expand Down
3 changes: 2 additions & 1 deletion packages/ai/src/providers/openai-codex-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
} from "../utils/diagnostics.ts";
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { formatProviderError } from "./error-utils.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
import { buildBaseOptions } from "./simple-options.ts";
Expand Down Expand Up @@ -397,7 +398,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
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(error, "OpenAI Codex");
stream.push({ type: "error", reason: output.stopReason, error: output });
stream.end();
}
Expand Down
3 changes: 2 additions & 1 deletion packages/ai/src/providers/openai-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { headersToRecord } from "../utils/headers.ts";
import { parseStreamingJson } from "../utils/json-parse.ts";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.ts";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { formatProviderError } from "./error-utils.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { buildBaseOptions } from "./simple-options.ts";
Expand Down Expand Up @@ -413,7 +414,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
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(error, "OpenAI");
// Some providers via OpenRouter give additional information in this field.
const rawMetadata = (error as any)?.error?.metadata?.raw;
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
Expand Down
15 changes: 2 additions & 13 deletions packages/ai/src/providers/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.ts";
import { headersToRecord } from "../utils/headers.ts";
import { isCloudflareProvider, resolveCloudflareBaseUrl } from "./cloudflare.ts";
import { formatProviderError } from "./error-utils.ts";
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.ts";
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.ts";
Expand Down Expand Up @@ -53,19 +54,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(error, "OpenAI");
}

// OpenAI Responses-specific options
Expand Down
Loading
Loading