workers-ai-provider 3.1.14 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import { o as ProviderPlugin } from "./gateway-delegate-BfaUTwDZ.mjs";
2
+
3
+ //#region src/openai.d.ts
4
+ /**
5
+ * OpenAI-wire provider plugin for the gateway delegate. Pass to
6
+ * `createGatewayDelegate({ providers: [openai] })` to handle every
7
+ * OpenAI-compatible provider in one go — `openai/…`, plus the OpenAI-compatible
8
+ * long tail (`deepseek/…`, `xai/…`, `groq/…`, `mistral/…`, `perplexity/…`,
9
+ * `openrouter/…`, `cohere/…`, …). The registry routes each slug to its gateway
10
+ * provider id; this plugin only supplies the response parser.
11
+ *
12
+ * Requires `@ai-sdk/openai` (an optional peer dependency — install it yourself).
13
+ *
14
+ * Uses `.chat()` (Chat Completions) deliberately: AI SDK v6's bare `openai()`
15
+ * defaults to the Responses API, which the AI Gateway run catalog does not serve.
16
+ */
17
+ declare const openai: ProviderPlugin;
18
+ //#endregion
19
+ export { openai };
20
+ //# sourceMappingURL=openai.d.mts.map
@@ -0,0 +1,27 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ //#region src/openai.ts
3
+ /**
4
+ * OpenAI-wire provider plugin for the gateway delegate. Pass to
5
+ * `createGatewayDelegate({ providers: [openai] })` to handle every
6
+ * OpenAI-compatible provider in one go — `openai/…`, plus the OpenAI-compatible
7
+ * long tail (`deepseek/…`, `xai/…`, `groq/…`, `mistral/…`, `perplexity/…`,
8
+ * `openrouter/…`, `cohere/…`, …). The registry routes each slug to its gateway
9
+ * provider id; this plugin only supplies the response parser.
10
+ *
11
+ * Requires `@ai-sdk/openai` (an optional peer dependency — install it yourself).
12
+ *
13
+ * Uses `.chat()` (Chat Completions) deliberately: AI SDK v6's bare `openai()`
14
+ * defaults to the Responses API, which the AI Gateway run catalog does not serve.
15
+ */
16
+ const openai = {
17
+ wireFormat: "openai",
18
+ create: ({ modelId, fetch, baseURL }) => createOpenAI({
19
+ apiKey: "unused",
20
+ fetch,
21
+ ...baseURL ? { baseURL } : {}
22
+ }).chat(modelId)
23
+ };
24
+ //#endregion
25
+ export { openai };
26
+
27
+ //# sourceMappingURL=openai.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai.mjs","names":[],"sources":["../src/openai.ts"],"sourcesContent":["import { createOpenAI } from \"@ai-sdk/openai\";\nimport type { ProviderPlugin } from \"./gateway-delegate\";\n\n/**\n * OpenAI-wire provider plugin for the gateway delegate. Pass to\n * `createGatewayDelegate({ providers: [openai] })` to handle every\n * OpenAI-compatible provider in one go — `openai/…`, plus the OpenAI-compatible\n * long tail (`deepseek/…`, `xai/…`, `groq/…`, `mistral/…`, `perplexity/…`,\n * `openrouter/…`, `cohere/…`, …). The registry routes each slug to its gateway\n * provider id; this plugin only supplies the response parser.\n *\n * Requires `@ai-sdk/openai` (an optional peer dependency — install it yourself).\n *\n * Uses `.chat()` (Chat Completions) deliberately: AI SDK v6's bare `openai()`\n * defaults to the Responses API, which the AI Gateway run catalog does not serve.\n */\nexport const openai: ProviderPlugin = {\n\twireFormat: \"openai\",\n\tcreate: ({ modelId, fetch, baseURL }) =>\n\t\t// apiKey is a placeholder — the gateway handles auth (unified billing / BYOK)\n\t\t// and the delegate strips the Authorization header on the gateway path.\n\t\t// baseURL (set by the registry for non-OpenAI openai-wire providers) makes\n\t\t// the generated URL host-strip to the right gateway-native endpoint.\n\t\tcreateOpenAI({ apiKey: \"unused\", fetch, ...(baseURL ? { baseURL } : {}) }).chat(modelId),\n};\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,MAAa,SAAyB;CACrC,YAAY;CACZ,SAAS,EAAE,SAAS,OAAO,cAK1B,aAAa;EAAE,QAAQ;EAAU;EAAO,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAAG,CAAC,CAAC,CAAC,KAAK,OAAO;AACzF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workers-ai-provider",
3
- "version": "3.1.14",
3
+ "version": "3.2.0",
4
4
  "description": "Workers AI Provider for the vercel AI SDK",
5
5
  "keywords": [
6
6
  "ai",
@@ -29,25 +29,66 @@
29
29
  "type": "module",
30
30
  "main": "dist/index.mjs",
31
31
  "types": "dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs"
36
+ },
37
+ "./openai": {
38
+ "types": "./dist/openai.d.mts",
39
+ "import": "./dist/openai.mjs"
40
+ },
41
+ "./anthropic": {
42
+ "types": "./dist/anthropic.d.mts",
43
+ "import": "./dist/anthropic.mjs"
44
+ },
45
+ "./google": {
46
+ "types": "./dist/google.d.mts",
47
+ "import": "./dist/google.mjs"
48
+ },
49
+ "./gateway": {
50
+ "types": "./dist/gateway-provider.d.mts",
51
+ "import": "./dist/gateway-provider.mjs"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
32
55
  "devDependencies": {
33
- "@ai-sdk/provider": "^3.0.8",
34
- "@cloudflare/workers-types": "^4.20260402.1",
35
- "ai": "^6.0.143",
36
- "zod": "^4.3.6"
56
+ "@ai-sdk/anthropic": "^3.0.84",
57
+ "@ai-sdk/google": "^3.0.0",
58
+ "@ai-sdk/openai": "^3.0.71",
59
+ "@ai-sdk/provider": "^3.0.10",
60
+ "@cloudflare/workers-types": "^4.20260613.1",
61
+ "ai": "^6.0.204",
62
+ "zod": "^4.4.3"
37
63
  },
38
64
  "peerDependencies": {
65
+ "@ai-sdk/anthropic": "^3.0.0",
66
+ "@ai-sdk/google": "^3.0.0",
67
+ "@ai-sdk/openai": "^3.0.0",
39
68
  "@ai-sdk/provider": "^3.0.0",
40
69
  "ai": "^6.0.0"
41
70
  },
71
+ "peerDependenciesMeta": {
72
+ "@ai-sdk/anthropic": {
73
+ "optional": true
74
+ },
75
+ "@ai-sdk/google": {
76
+ "optional": true
77
+ },
78
+ "@ai-sdk/openai": {
79
+ "optional": true
80
+ }
81
+ },
42
82
  "authors": "Cloudflare Inc.",
43
83
  "scripts": {
44
- "build": "rm -rf dist && tsdown src/index.ts --dts --sourcemap --format esm --target es2020",
84
+ "build": "rm -rf dist && tsdown src/index.ts src/gateway-provider.ts src/openai.ts src/anthropic.ts src/google.ts --dts --sourcemap --format esm --target es2020",
45
85
  "format": "oxfmt --write .",
46
86
  "test:ci": "vitest --watch=false",
47
87
  "test": "vitest",
48
88
  "test:e2e": "vitest --config vitest.e2e.config.ts --watch=false",
49
89
  "test:e2e:rest": "vitest --config vitest.e2e.config.ts --watch=false test/e2e/workers-ai-rest.e2e.test.ts",
50
90
  "test:e2e:binding": "vitest --config vitest.e2e.config.ts --watch=false test/e2e/workers-ai-binding.e2e.test.ts",
91
+ "test:e2e:gateway": "vitest --config vitest.e2e.config.ts --watch=false test/e2e/workers-ai-gateway.e2e.test.ts",
51
92
  "type-check": "tsc --noEmit"
52
93
  }
53
94
  }
@@ -0,0 +1,17 @@
1
+ import { createAnthropic } from "@ai-sdk/anthropic";
2
+ import type { ProviderPlugin } from "./gateway-delegate";
3
+
4
+ /**
5
+ * Anthropic-wire provider plugin for the gateway delegate. Pass to
6
+ * `createGatewayDelegate({ providers: [anthropic] })` to handle
7
+ * `"anthropic/<model>"` slugs.
8
+ *
9
+ * Requires `@ai-sdk/anthropic` (an optional peer dependency — install it yourself).
10
+ */
11
+ export const anthropic: ProviderPlugin = {
12
+ wireFormat: "anthropic",
13
+ create: ({ modelId, fetch, baseURL }) =>
14
+ // apiKey is a placeholder — the gateway handles auth (unified billing / BYOK)
15
+ // and the delegate strips the x-api-key header on the gateway path.
16
+ createAnthropic({ apiKey: "unused", fetch, ...(baseURL ? { baseURL } : {}) })(modelId),
17
+ };
@@ -0,0 +1,70 @@
1
+ import type {
2
+ LanguageModelV3,
3
+ LanguageModelV3CallOptions,
4
+ LanguageModelV3GenerateResult,
5
+ LanguageModelV3StreamResult,
6
+ } from "@ai-sdk/provider";
7
+ import { type FallbackAttempt, WorkersAIFallbackError, WorkersAIGatewayError } from "./errors";
8
+ import type { Transport } from "./gateway-delegate";
9
+
10
+ /** One model in a client-side fallback chain. */
11
+ export interface FallbackLeg {
12
+ /** The model slug this leg dispatches. */
13
+ slug: string;
14
+ /** The built AI SDK model. */
15
+ model: LanguageModelV3;
16
+ /** Transport the leg uses. */
17
+ transport: Transport;
18
+ }
19
+
20
+ /**
21
+ * Wrap a chain of models so a failed *pre-stream* dispatch falls through to the
22
+ * next model, preserving resume on each leg's own transport. If every leg fails,
23
+ * throws a {@link WorkersAIFallbackError} carrying the full attempt tree.
24
+ *
25
+ * Fallback triggers on `doGenerate`/`doStream` rejection (the dispatch never
26
+ * produced a stream). Errors that surface *mid-stream* — after content has
27
+ * already been emitted — are not recoverable here and propagate as-is.
28
+ */
29
+ export function createClientFallbackModel(legs: FallbackLeg[]): LanguageModelV3 {
30
+ if (legs.length === 0) {
31
+ throw new Error("createClientFallbackModel requires at least one model leg.");
32
+ }
33
+ const primary = legs[0].model;
34
+
35
+ async function attempt<T>(run: (model: LanguageModelV3) => PromiseLike<T>): Promise<T> {
36
+ const attempts: FallbackAttempt[] = [];
37
+ for (const leg of legs) {
38
+ try {
39
+ const result = await run(leg.model);
40
+ attempts.push({ model: leg.slug, transport: leg.transport, ok: true });
41
+ return result;
42
+ } catch (e) {
43
+ const err = WorkersAIGatewayError.fromUnknown(e);
44
+ attempts.push({
45
+ model: leg.slug,
46
+ transport: leg.transport,
47
+ ok: false,
48
+ status: err.status,
49
+ error: err,
50
+ });
51
+ }
52
+ }
53
+ throw new WorkersAIFallbackError(attempts);
54
+ }
55
+
56
+ return {
57
+ specificationVersion: "v3",
58
+ provider: primary.provider,
59
+ modelId: primary.modelId,
60
+ supportedUrls: primary.supportedUrls,
61
+ doGenerate(
62
+ options: LanguageModelV3CallOptions,
63
+ ): PromiseLike<LanguageModelV3GenerateResult> {
64
+ return attempt((m) => m.doGenerate(options));
65
+ },
66
+ doStream(options: LanguageModelV3CallOptions): PromiseLike<LanguageModelV3StreamResult> {
67
+ return attempt((m) => m.doStream(options));
68
+ },
69
+ };
70
+ }
@@ -1,4 +1,5 @@
1
1
  import type { LanguageModelV3DataContent, LanguageModelV3Prompt } from "@ai-sdk/provider";
2
+ import { UnsupportedFunctionalityError } from "@ai-sdk/provider";
2
3
  import { toWorkersAIToolCallId } from "./utils";
3
4
  import type { WorkersAIContentPart, WorkersAIChatPrompt } from "./workersai-chat-prompt";
4
5
 
@@ -41,6 +42,30 @@ function toUint8Array(data: LanguageModelV3DataContent): Uint8Array | null {
41
42
  return null;
42
43
  }
43
44
 
45
+ function assertImageMediaType(mediaType: string | undefined): string {
46
+ if (!mediaType) {
47
+ throw new UnsupportedFunctionalityError({
48
+ functionality: "file-part-without-media-type",
49
+ message:
50
+ "Workers AI chat only supports image file parts with an image/* mediaType. " +
51
+ "Received a file part without a mediaType.",
52
+ });
53
+ }
54
+
55
+ // Media types are case-insensitive (RFC 2045), so compare against a
56
+ // lower-cased copy while preserving the caller's original casing on output.
57
+ if (!mediaType.toLowerCase().startsWith("image/")) {
58
+ throw new UnsupportedFunctionalityError({
59
+ functionality: "non-image-file-part",
60
+ message:
61
+ "Workers AI chat only supports image file parts with an image/* mediaType. " +
62
+ `Received mediaType "${mediaType}".`,
63
+ });
64
+ }
65
+
66
+ return mediaType;
67
+ }
68
+
44
69
  function uint8ArrayToBase64(bytes: Uint8Array): string {
45
70
  let binary = "";
46
71
  const chunkSize = 8192;
@@ -65,7 +90,7 @@ export function convertToWorkersAIChatMessages(prompt: LanguageModelV3Prompt): {
65
90
 
66
91
  case "user": {
67
92
  const textParts: string[] = [];
68
- const imageParts: { image: Uint8Array; mediaType: string | undefined }[] = [];
93
+ const imageParts: { image: Uint8Array; mediaType: string }[] = [];
69
94
 
70
95
  for (const part of content) {
71
96
  switch (part.type) {
@@ -74,11 +99,12 @@ export function convertToWorkersAIChatMessages(prompt: LanguageModelV3Prompt): {
74
99
  break;
75
100
  }
76
101
  case "file": {
102
+ const mediaType = assertImageMediaType(part.mediaType);
77
103
  const imageBytes = toUint8Array(part.data);
78
104
  if (imageBytes) {
79
105
  imageParts.push({
80
106
  image: imageBytes,
81
- mediaType: part.mediaType,
107
+ mediaType,
82
108
  });
83
109
  }
84
110
  break;
@@ -93,10 +119,9 @@ export function convertToWorkersAIChatMessages(prompt: LanguageModelV3Prompt): {
93
119
  }
94
120
  for (const img of imageParts) {
95
121
  const base64 = uint8ArrayToBase64(img.image);
96
- const mediaType = img.mediaType || "image/png";
97
122
  contentArray.push({
98
123
  type: "image_url",
99
- image_url: { url: `data:${mediaType};base64,${base64}` },
124
+ image_url: { url: `data:${img.mediaType};base64,${base64}` },
100
125
  });
101
126
  }
102
127
  messages.push({ content: contentArray, role: "user" });
@@ -126,7 +151,7 @@ export function convertToWorkersAIChatMessages(prompt: LanguageModelV3Prompt): {
126
151
  case "reasoning": {
127
152
  // Reasoning is accumulated separately and sent as the `reasoning`
128
153
  // field on the message object. This is the field name vLLM expects
129
- // on input for reasoning models (kimi-k2.5, glm-4.7-flash).
154
+ // on input for reasoning models (kimi-k2.7-code, glm-4.7-flash).
130
155
  // Concatenating it into `content` corrupts the conversation history
131
156
  // and causes models to produce empty or garbled responses on the
132
157
  // next turn.
package/src/errors.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Typed errors for the gateway delegate.
3
+ *
4
+ * - {@link WorkersAIGatewayError} — a single dispatch failed. Carries a coarse
5
+ * {@link GatewayErrorCode}, a `recoverable` hint (whether a retry/fallback is
6
+ * worth attempting), and the parsed gateway/provider envelope.
7
+ * - {@link WorkersAIFallbackError} — every model in a client-side fallback chain
8
+ * failed. Carries the per-attempt tree so callers can see exactly what was
9
+ * tried and why each leg failed.
10
+ */
11
+
12
+ /** Coarse classification of a gateway/provider failure. */
13
+ export type GatewayErrorCode =
14
+ | "auth" // 401 / 403 — bad or missing key (BYOK), or unified billing not enabled
15
+ | "rate-limit" // 429 — throttled
16
+ | "not-found" // 404 — unknown model/endpoint (or expired resume buffer)
17
+ | "bad-request" // 400 / 422 — malformed request
18
+ | "provider-error" // 5xx — upstream provider failure
19
+ | "gateway-error" // gateway/transport failure with no usable status
20
+ | "resume-expired" // resume buffer TTL elapsed (404 from resume endpoint)
21
+ | "unknown";
22
+
23
+ /** Context attached to a {@link WorkersAIGatewayError}. */
24
+ export interface GatewayErrorContext {
25
+ /** Gateway provider id (e.g. `"openai"`, `"google-ai-studio"`). */
26
+ provider?: string;
27
+ /** Provider-native model id. */
28
+ modelId?: string;
29
+ /** Transport the failed dispatch used. */
30
+ transport?: "run" | "gateway";
31
+ /** HTTP status, if any. */
32
+ status?: number | null;
33
+ /** `cf-aig-log-id` for cross-referencing in the dashboard. */
34
+ logId?: string | null;
35
+ /** `cf-aig-run-id`, if the run path issued one. */
36
+ runId?: string | null;
37
+ }
38
+
39
+ /** Map an HTTP status to a {@link GatewayErrorCode} + recoverability hint. */
40
+ export function classifyStatus(status: number): {
41
+ code: GatewayErrorCode;
42
+ recoverable: boolean;
43
+ } {
44
+ if (status === 401 || status === 403) return { code: "auth", recoverable: false };
45
+ if (status === 429) return { code: "rate-limit", recoverable: true };
46
+ if (status === 404) return { code: "not-found", recoverable: false };
47
+ if (status === 400 || status === 422) return { code: "bad-request", recoverable: false };
48
+ if (status >= 500) return { code: "provider-error", recoverable: true };
49
+ return { code: "unknown", recoverable: false };
50
+ }
51
+
52
+ /** Best-effort extraction of a human message from a CF/provider error envelope. */
53
+ export function extractErrorMessage(raw: unknown): string | undefined {
54
+ if (typeof raw === "string") {
55
+ const trimmed = raw.trim();
56
+ if (!trimmed) return undefined;
57
+ try {
58
+ return extractErrorMessage(JSON.parse(trimmed));
59
+ } catch {
60
+ return trimmed.slice(0, 500);
61
+ }
62
+ }
63
+ if (!raw || typeof raw !== "object") return undefined;
64
+ const obj = raw as Record<string, unknown>;
65
+ // CF gateway envelope: { errors: [{ code, message }] }
66
+ if (Array.isArray(obj.errors) && obj.errors.length > 0) {
67
+ const first = obj.errors[0] as Record<string, unknown>;
68
+ if (typeof first?.message === "string") return first.message;
69
+ }
70
+ // Provider envelopes: { error: { message } } or { error: "..." } or { message }
71
+ if (obj.error && typeof obj.error === "object") {
72
+ const err = obj.error as Record<string, unknown>;
73
+ if (typeof err.message === "string") return err.message;
74
+ }
75
+ if (typeof obj.error === "string") return obj.error;
76
+ if (typeof obj.message === "string") return obj.message;
77
+ return undefined;
78
+ }
79
+
80
+ /** A single dispatch failure through AI Gateway (run or gateway path). */
81
+ export class WorkersAIGatewayError extends Error {
82
+ readonly code: GatewayErrorCode;
83
+ /** Whether a retry or fallback to another model is worth attempting. */
84
+ readonly recoverable: boolean;
85
+ readonly status: number | null;
86
+ readonly context: GatewayErrorContext;
87
+ /** Parsed gateway/provider error envelope (or raw text). */
88
+ readonly raw?: unknown;
89
+ override readonly cause?: unknown;
90
+
91
+ constructor(
92
+ code: GatewayErrorCode,
93
+ message: string,
94
+ opts: {
95
+ recoverable?: boolean;
96
+ status?: number | null;
97
+ context?: GatewayErrorContext;
98
+ raw?: unknown;
99
+ cause?: unknown;
100
+ } = {},
101
+ ) {
102
+ super(message);
103
+ this.name = "WorkersAIGatewayError";
104
+ this.code = code;
105
+ this.recoverable = opts.recoverable ?? false;
106
+ this.status = opts.status ?? null;
107
+ this.context = opts.context ?? {};
108
+ this.raw = opts.raw;
109
+ this.cause = opts.cause;
110
+ }
111
+
112
+ /**
113
+ * Classify an arbitrary thrown value. Understands AI SDK `APICallError`
114
+ * (reads `statusCode` / `responseBody` / `isRetryable`); falls back to a
115
+ * recoverable `gateway-error` for transport/connection failures so a fallback
116
+ * chain keeps trying.
117
+ */
118
+ static fromUnknown(e: unknown): WorkersAIGatewayError {
119
+ if (e instanceof WorkersAIGatewayError) return e;
120
+ const obj = e && typeof e === "object" ? (e as Record<string, unknown>) : {};
121
+ const status = typeof obj.statusCode === "number" ? obj.statusCode : null;
122
+ const responseBody = typeof obj.responseBody === "string" ? obj.responseBody : undefined;
123
+
124
+ if (status !== null) {
125
+ const classified = classifyStatus(status);
126
+ // AI SDK already decides retryability per status; prefer it when present.
127
+ const recoverable =
128
+ typeof obj.isRetryable === "boolean" ? obj.isRetryable : classified.recoverable;
129
+ const message =
130
+ extractErrorMessage(responseBody) ??
131
+ (e instanceof Error ? e.message : `Gateway dispatch failed (HTTP ${status}).`);
132
+ let raw: unknown = responseBody;
133
+ try {
134
+ raw = responseBody ? JSON.parse(responseBody) : responseBody;
135
+ } catch {
136
+ // keep raw text
137
+ }
138
+ return new WorkersAIGatewayError(classified.code, message, {
139
+ recoverable,
140
+ status,
141
+ raw,
142
+ cause: e,
143
+ });
144
+ }
145
+
146
+ return new WorkersAIGatewayError(
147
+ "gateway-error",
148
+ e instanceof Error ? e.message : String(e),
149
+ { recoverable: true, cause: e },
150
+ );
151
+ }
152
+
153
+ /** Build from an HTTP `Response` (reads the body for the envelope). */
154
+ static async fromResponse(
155
+ resp: Response,
156
+ context: GatewayErrorContext = {},
157
+ ): Promise<WorkersAIGatewayError> {
158
+ const text = await resp.text().catch(() => "");
159
+ const { code, recoverable } = classifyStatus(resp.status);
160
+ const message =
161
+ extractErrorMessage(text) ?? `Gateway dispatch failed (HTTP ${resp.status}).`;
162
+ let raw: unknown = text;
163
+ try {
164
+ raw = text ? JSON.parse(text) : text;
165
+ } catch {
166
+ // keep raw text
167
+ }
168
+ return new WorkersAIGatewayError(code, message, {
169
+ recoverable,
170
+ status: resp.status,
171
+ raw,
172
+ context: {
173
+ ...context,
174
+ status: resp.status,
175
+ logId: resp.headers.get("cf-aig-log-id"),
176
+ runId: resp.headers.get("cf-aig-run-id"),
177
+ },
178
+ });
179
+ }
180
+ }
181
+
182
+ /** One leg of a client-side fallback chain. */
183
+ export interface FallbackAttempt {
184
+ /** The model slug attempted. */
185
+ model: string;
186
+ /** Transport used for this attempt. */
187
+ transport: "run" | "gateway";
188
+ /** Whether this attempt succeeded. */
189
+ ok: boolean;
190
+ /** HTTP status, if any. */
191
+ status?: number | null;
192
+ /** The classified error, when the attempt failed. */
193
+ error?: WorkersAIGatewayError;
194
+ }
195
+
196
+ /** Every model in a client-side fallback chain failed. */
197
+ export class WorkersAIFallbackError extends Error {
198
+ /** The ordered attempt tree (primary first, then each fallback). */
199
+ readonly attempts: FallbackAttempt[];
200
+
201
+ constructor(attempts: FallbackAttempt[], message?: string) {
202
+ const tried = attempts.map((a) => a.model).join(" → ");
203
+ super(message ?? `All fallback models failed: ${tried}.`);
204
+ this.name = "WorkersAIFallbackError";
205
+ this.attempts = attempts;
206
+ }
207
+
208
+ /** The last (most recent) attempt's error, if any. */
209
+ get lastError(): WorkersAIGatewayError | undefined {
210
+ for (let i = this.attempts.length - 1; i >= 0; i--) {
211
+ const e = this.attempts[i].error;
212
+ if (e) return e;
213
+ }
214
+ return undefined;
215
+ }
216
+ }