UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

141 lines (135 loc) 5.71 kB
/** * Shared error-narrowing helper for activities that convert thrown values * into structured `RUN_ERROR` events. * * Accepts Error instances, objects with string-ish `message`/`code`, or bare * strings; always returns a shape safe to serialize. Never leaks the full * error object (which may carry request/response state from an SDK). * * Abort-shaped errors (DOM `AbortError`, OpenAI `APIUserAbortError`, * OpenRouter `RequestAbortedError`) are normalized to a stable * `{ message: 'Request aborted', code: 'aborted' }` shape so callers can * discriminate user-initiated cancellation from other failures without * matching on provider-specific message strings. */ const ABORT_ERROR_NAMES = new Set([ 'AbortError', 'APIUserAbortError', 'RequestAbortedError', ]) /** * True when a thrown value is an abort-shaped error (DOM `AbortError`, OpenAI * `APIUserAbortError`, OpenRouter `RequestAbortedError`) — i.e. user-initiated * cancellation rather than a genuine failure. Matches on the error `name` so * callers can discriminate aborts without depending on a signal's state or on * provider-specific message strings. */ export function isAbortShapedError(error: unknown): boolean { if (error && typeof error === 'object') { const name = (error as { name?: unknown }).name return typeof name === 'string' && ABORT_ERROR_NAMES.has(name) } return false } // HTTP status codes carried as numbers (e.g. `error.status = 429`) are a // common variant on SDK error classes; coerce so the resulting `code` field // is stable as a string for downstream consumers. function normalizeCode(codeField: unknown): string | undefined { if (typeof codeField === 'string') return codeField if (typeof codeField === 'number' && Number.isFinite(codeField)) { return String(codeField) } return undefined } // SDK error classes disagree on where they carry the HTTP status. Most expose a // `code` (OpenAI/Anthropic error bodies), but some report it only as a numeric // `status` — Google's `@google/genai` `ApiError` sets `status: number` and no // `code` at all. Without this fallback such errors reach downstream consumers // with `code: undefined`, so a 401/403/404/429 is indistinguishable from an // unknown failure and cannot be classified. // // Only a *numeric* `status` is used: a string `status` is commonly an HTTP // reason phrase ("Forbidden") or a symbolic status ("PERMISSION_DENIED"), not // the numeric code consumers key on, so forwarding it would be misleading. function extractCode(source: { code?: unknown status?: unknown }): string | undefined { const fromCode = normalizeCode(source.code) if (fromCode !== undefined) return fromCode if (typeof source.status === 'number' && Number.isFinite(source.status)) { return String(source.status) } return undefined } export function toRunErrorPayload( error: unknown, fallbackMessage = 'Unknown error occurred', ): { message: string; code: string | undefined } { if (isAbortShapedError(error)) { return { message: 'Request aborted', code: 'aborted' } } if (error instanceof Error) { return { message: error.message || fallbackMessage, code: extractCode(error as Error & { code?: unknown; status?: unknown }), } } if (typeof error === 'object' && error !== null) { const messageField = (error as { message?: unknown }).message return { message: typeof messageField === 'string' && messageField.length > 0 ? messageField : fallbackMessage, code: extractCode(error as { code?: unknown; status?: unknown }), } } if (typeof error === 'string' && error.length > 0) { return { message: error, code: undefined } } return { message: fallbackMessage, code: undefined } } /** * Extract the provider's *structured error body* from a thrown value, to attach * as the AG-UI `rawEvent` on a RUN_ERROR event. This is the recoverable upstream * detail (provider name, the upstream model's error JSON, rate-limit/overload * codes, etc.) that `toRunErrorPayload`'s `{ message, code }` deliberately drops. * * Security boundary: only known provider-response-body fields are forwarded — * never the raw SDK exception object, which can carry request metadata such as * auth headers or request ids. The recognized sources, in priority order: * * - `error.rawEvent` — a provider body an adapter attached explicitly (e.g. the * OpenRouter mid-stream `chunk.error`). * - `error.error` (object) — the parsed provider response body exposed by SDK * `APIError` instances (OpenAI/Anthropic `{ type, message, code, param }`, * OpenRouter typed errors whose `.error` carries `.metadata`). This is * provider-shaped data, distinct from `.headers` / `.request_id`. * - `error.metadata` — OpenRouter's `provider_name` + raw upstream body, when * surfaced directly on the thrown error. * * Returns `undefined` when no structured provider body is present, so callers * omit the field entirely rather than setting it to `null`: * * const rawEvent = toRunErrorRawEvent(error) * yield { type: EventType.RUN_ERROR, ..., ...(rawEvent !== undefined && { rawEvent }) } */ export function toRunErrorRawEvent(error: unknown): unknown { if (!error || typeof error !== 'object') return undefined const e = error as { rawEvent?: unknown error?: unknown metadata?: unknown } if (e.rawEvent !== undefined && e.rawEvent !== null) return e.rawEvent if ( e.error !== undefined && e.error !== null && typeof e.error === 'object' ) { return e.error } if (e.metadata !== undefined && e.metadata !== null) return e.metadata return undefined }