workers-ai-provider 3.2.1 → 3.3.1

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.
package/dist/index.mjs CHANGED
@@ -1,6 +1,52 @@
1
- import { a as findProviderBySlug, c as WorkersAIGatewayError, i as detectProviderByUrl, l as _defineProperty, n as createGatewayProvider, o as wireableProviders, r as GATEWAY_PROVIDERS, s as WorkersAIFallbackError, t as createGatewayFetch } from "./gateway-provider-1USFWm7c.mjs";
2
- import { TooManyEmbeddingValuesForCallError, UnsupportedFunctionalityError } from "@ai-sdk/provider";
1
+ import { C as GatewayDelegateError, S as headersToObject, _ as detectProviderByUrl, a as WORKERS_AI_ERROR_CODE_TO_STATUS, b as asText, c as parseWorkersAIErrorCode, d as isForcedToolChoice, f as normalizeMessagesForBinding$1, g as GATEWAY_PROVIDERS, h as createResumableStream, i as WorkersAIGatewayError, l as SSEDecoder, m as processText, n as createGatewayProvider, o as isAbortError, p as parseLeakedToolCalls$1, r as WorkersAIFallbackError, s as messageOf, t as createGatewayFetch, u as getToolNames, v as findProviderBySlug, w as _defineProperty, x as buildGatewayEntry, y as wireableProviders } from "./gateway-provider-CQU-v2IO.mjs";
2
+ import { APICallError, TooManyEmbeddingValuesForCallError, UnsupportedFunctionalityError } from "@ai-sdk/provider";
3
3
  import { generateId } from "ai";
4
+ //#region src/workersai-error.ts
5
+ /**
6
+ * Normalize an error thrown by the Workers AI **binding** (`env.AI.run`) into an
7
+ * `APICallError` so the AI SDK can classify and retry it.
8
+ *
9
+ * Cancellations (`AbortError` / `TimeoutError` / `ResponseAborted`, including
10
+ * `DOMException` aborts) and errors that are already an `APICallError` pass
11
+ * through unchanged. Everything else becomes an
12
+ * `APICallError`; when the internal code maps to a known HTTP status, that
13
+ * `statusCode` is attached and `APICallError` derives `isRetryable` from it.
14
+ * Unrecognized errors get no `statusCode`, so they stay non-retryable (the
15
+ * prior behavior).
16
+ */
17
+ function normalizeBindingError(error, context) {
18
+ if (APICallError.isInstance(error) || isAbortError(error)) return error;
19
+ const code = parseWorkersAIErrorCode(error);
20
+ const statusCode = code != null ? WORKERS_AI_ERROR_CODE_TO_STATUS[code] : void 0;
21
+ const message = messageOf(error);
22
+ return new APICallError({
23
+ message,
24
+ url: `workers-ai:binding/run/${context.model}`,
25
+ requestBodyValues: context.requestBodyValues,
26
+ statusCode,
27
+ responseBody: message,
28
+ cause: error,
29
+ ...code != null ? { data: { workersAIErrorCode: code } } : {}
30
+ });
31
+ }
32
+ /**
33
+ * Build an `APICallError` from a non-OK Workers AI **REST** response. The HTTP
34
+ * status is authoritative here, so `APICallError` derives `isRetryable` from it
35
+ * directly (429 / 5xx → retryable). Response headers are preserved so the AI
36
+ * SDK can honor `Retry-After`. The message keeps the historical
37
+ * `"Workers AI API error (<status> <statusText>): <body>"` shape.
38
+ */
39
+ function apiCallErrorFromResponse(response, errorBody, context) {
40
+ return new APICallError({
41
+ message: `Workers AI API error (${response.status} ${response.statusText}): ${errorBody}`,
42
+ url: context.url,
43
+ requestBodyValues: context.requestBodyValues,
44
+ statusCode: response.status,
45
+ responseHeaders: headersToObject(response.headers),
46
+ responseBody: errorBody
47
+ });
48
+ }
49
+ //#endregion
4
50
  //#region src/utils.ts
5
51
  /**
6
52
  * Normalize messages before passing to the Workers AI binding.
@@ -9,11 +55,7 @@ import { generateId } from "ai";
9
55
  * - `content` must not be null
10
56
  */
11
57
  function normalizeMessagesForBinding(messages) {
12
- return messages.map((msg) => {
13
- const normalized = { ...msg };
14
- if (normalized.content === null || normalized.content === void 0) normalized.content = "";
15
- return normalized;
16
- });
58
+ return normalizeMessagesForBinding$1(messages);
17
59
  }
18
60
  /**
19
61
  * Creates a run method that emulates the Cloudflare Workers AI binding,
@@ -62,7 +104,10 @@ function createRun(config) {
62
104
  } catch {
63
105
  errorBody = "<unable to read response body>";
64
106
  }
65
- throw new Error(`Workers AI API error (${response.status} ${response.statusText}): ${errorBody}`);
107
+ throw apiCallErrorFromResponse(response, errorBody, {
108
+ url,
109
+ requestBodyValues: inputs
110
+ });
66
111
  }
67
112
  if (returnRawResponse) return response;
68
113
  if (inputs.stream === true) {
@@ -85,7 +130,10 @@ function createRun(config) {
85
130
  } catch {
86
131
  errorBody = "<unable to read response body>";
87
132
  }
88
- throw new Error(`Workers AI API error (${retryResponse.status} ${retryResponse.statusText}): ${errorBody}`);
133
+ throw apiCallErrorFromResponse(retryResponse, errorBody, {
134
+ url,
135
+ requestBodyValues: inputs
136
+ });
89
137
  }
90
138
  return (await retryResponse.json()).result;
91
139
  }
@@ -123,7 +171,13 @@ async function createRunBinary(config, model, audioBytes, contentType, signal) {
123
171
  } catch {
124
172
  errorBody = "<unable to read response body>";
125
173
  }
126
- throw new Error(`Workers AI API error (${response.status} ${response.statusText}): ${errorBody}`);
174
+ throw apiCallErrorFromResponse(response, errorBody, {
175
+ url,
176
+ requestBodyValues: {
177
+ contentType,
178
+ byteLength: audioBytes.byteLength
179
+ }
180
+ });
127
181
  }
128
182
  const data = await response.json();
129
183
  return data.result ?? data;
@@ -235,56 +289,20 @@ function processToolCalls(output) {
235
289
  return [];
236
290
  }
237
291
  /**
238
- * Was a specific tool forced for this request?
239
- *
240
- * True for both `tool_choice: "required"` and the named-function form
241
- * `{ type: "function", function: { name } }`.
242
- */
243
- function isForcedToolChoice(toolChoice) {
244
- if (toolChoice === "required") return true;
245
- return typeof toolChoice === "object" && toolChoice !== null && toolChoice.type === "function";
246
- }
247
- /**
248
292
  * Parse tool calls that a model leaked as JSON text instead of structured
249
- * `tool_calls`. Shared by the non-streaming salvage and the streaming buffer.
293
+ * `tool_calls`, assigning AI-SDK tool-call ids.
250
294
  *
251
- * Only JSON objects whose `name` is one of `knownToolNames` are recovered;
252
- * everything else (prose, harmony channel/role leaks like `{"name":"analysis"}`,
253
- * hallucinated names) is ignored to avoid fabricating bogus calls.
295
+ * The recovery logic (which JSON shapes count as a leaked call) lives in
296
+ * `@cloudflare/gateway-core`; this wrapper only layers the framework id on each
297
+ * neutral result so the existing `LanguageModelV3ToolCall` shape is preserved.
254
298
  */
255
299
  function parseLeakedToolCalls(text, knownToolNames) {
256
- let parsed;
257
- try {
258
- parsed = JSON.parse(text.trim());
259
- } catch {
260
- return [];
261
- }
262
- const candidates = Array.isArray(parsed) ? parsed : [parsed];
263
- const salvaged = [];
264
- for (const candidate of candidates) {
265
- if (typeof candidate !== "object" || candidate === null) continue;
266
- const obj = candidate;
267
- const name = obj.name;
268
- if (typeof name !== "string" || !knownToolNames.has(name)) continue;
269
- let args;
270
- if ("arguments" in obj) args = obj.arguments;
271
- else if ("parameters" in obj) args = obj.parameters;
272
- else {
273
- const { name: _name, ...rest } = obj;
274
- args = rest;
275
- }
276
- salvaged.push({
277
- input: typeof args === "string" ? args : JSON.stringify(args ?? {}),
278
- toolCallId: createAISDKToolCallId(void 0),
279
- type: "tool-call",
280
- toolName: name
281
- });
282
- }
283
- return salvaged;
284
- }
285
- /** Collect the requested tool names from mapped tools. */
286
- function getToolNames(tools) {
287
- return new Set((tools ?? []).map((tool) => tool.function?.name).filter((name) => typeof name === "string"));
300
+ return parseLeakedToolCalls$1(text, knownToolNames).map((call) => ({
301
+ input: call.input,
302
+ toolCallId: createAISDKToolCallId(void 0),
303
+ type: "tool-call",
304
+ toolName: call.toolName
305
+ }));
288
306
  }
289
307
  /**
290
308
  * Salvage a tool call that a model leaked into text content instead of the
@@ -320,24 +338,6 @@ function salvageToolCallsFromText(output, context) {
320
338
  const salvaged = parseLeakedToolCalls(text, knownToolNames);
321
339
  return salvaged.length > 0 ? salvaged : null;
322
340
  }
323
- /**
324
- * Extract text from a Workers AI response, handling multiple response formats:
325
- * - OpenAI format: { choices: [{ message: { content: "..." } }] }
326
- * - Native format: { response: "..." }
327
- * - Structured output quirk: { response: { ... } } (object instead of string)
328
- * - Structured output quirk: { response: "{ ... }" } (JSON string)
329
- */
330
- function processText(output) {
331
- const choiceContent = output.choices?.[0]?.message?.content;
332
- if (choiceContent != null && String(choiceContent).length > 0) return String(choiceContent);
333
- if ("response" in output) {
334
- const response = output.response;
335
- if (typeof response === "object" && response !== null) return JSON.stringify(response);
336
- if (typeof response === "number") return String(response);
337
- if (response === null || response === void 0) return;
338
- return String(response);
339
- }
340
- }
341
341
  //#endregion
342
342
  //#region src/convert-to-workersai-chat-messages.ts
343
343
  /**
@@ -945,37 +945,6 @@ function getMappedStream(response, salvageContext) {
945
945
  }
946
946
  }
947
947
  }
948
- /**
949
- * TransformStream that decodes a raw byte stream into SSE `data:` payloads.
950
- * Each output chunk is the string content after "data: " (one per SSE event).
951
- * Handles line buffering for partial chunks.
952
- */
953
- var SSEDecoder = class extends TransformStream {
954
- constructor() {
955
- let buffer = "";
956
- const decoder = new TextDecoder();
957
- super({
958
- transform(chunk, controller) {
959
- buffer += decoder.decode(chunk, { stream: true });
960
- const lines = buffer.split("\n");
961
- buffer = lines.pop() || "";
962
- for (const line of lines) {
963
- const trimmed = line.trim();
964
- if (!trimmed) continue;
965
- if (trimmed.startsWith("data: ")) controller.enqueue(trimmed.slice(6));
966
- else if (trimmed.startsWith("data:")) controller.enqueue(trimmed.slice(5));
967
- }
968
- },
969
- flush(controller) {
970
- if (buffer.trim()) {
971
- const trimmed = buffer.trim();
972
- if (trimmed.startsWith("data: ")) controller.enqueue(trimmed.slice(6));
973
- else if (trimmed.startsWith("data:")) controller.enqueue(trimmed.slice(5));
974
- }
975
- }
976
- });
977
- }
978
- };
979
948
  //#endregion
980
949
  //#region src/aisearch-chat-language-model.ts
981
950
  var AISearchChatLanguageModel = class {
@@ -1089,12 +1058,21 @@ var WorkersAIEmbeddingModel = class {
1089
1058
  values
1090
1059
  });
1091
1060
  const { gateway, maxEmbeddingsPerCall: _maxEmbeddingsPerCall, supportsParallelCalls: _supportsParallelCalls, ...passthroughOptions } = this.settings;
1092
- return {
1093
- embeddings: (await this.config.binding.run(this.modelId, { text: values }, {
1061
+ let response;
1062
+ try {
1063
+ response = await this.config.binding.run(this.modelId, { text: values }, {
1094
1064
  gateway: this.config.gateway ?? gateway,
1095
1065
  signal: abortSignal,
1096
1066
  ...passthroughOptions
1097
- })).data,
1067
+ });
1068
+ } catch (error) {
1069
+ throw normalizeBindingError(error, {
1070
+ model: this.modelId,
1071
+ requestBodyValues: { text: values }
1072
+ });
1073
+ }
1074
+ return {
1075
+ embeddings: response.data,
1098
1076
  warnings: []
1099
1077
  };
1100
1078
  }
@@ -1254,10 +1232,18 @@ var WorkersAIChatLanguageModel = class {
1254
1232
  const { messages } = convertToWorkersAIChatMessages(options.prompt);
1255
1233
  const inputs = this.buildRunInputs(args, messages, { providerOptions: options.providerOptions });
1256
1234
  const runOptions = this.getRunOptions();
1257
- const output = await this.config.binding.run(args.model, inputs, {
1258
- ...runOptions,
1259
- signal: options.abortSignal
1260
- });
1235
+ let output;
1236
+ try {
1237
+ output = await this.config.binding.run(args.model, inputs, {
1238
+ ...runOptions,
1239
+ signal: options.abortSignal
1240
+ });
1241
+ } catch (error) {
1242
+ throw normalizeBindingError(error, {
1243
+ model: args.model,
1244
+ requestBodyValues: inputs
1245
+ });
1246
+ }
1261
1247
  if (output instanceof ReadableStream) throw new Error("Unexpected streaming response from non-streaming request. Check that `stream: true` was not passed.");
1262
1248
  const outputRecord = output;
1263
1249
  const { reasoningContent, text, toolCalls, finishReason } = this.extractContent(outputRecord, args, warnings);
@@ -1286,10 +1272,18 @@ var WorkersAIChatLanguageModel = class {
1286
1272
  providerOptions: options.providerOptions
1287
1273
  });
1288
1274
  const runOptions = this.getRunOptions();
1289
- const response = await this.config.binding.run(args.model, inputs, {
1290
- ...runOptions,
1291
- signal: options.abortSignal
1292
- });
1275
+ let response;
1276
+ try {
1277
+ response = await this.config.binding.run(args.model, inputs, {
1278
+ ...runOptions,
1279
+ signal: options.abortSignal
1280
+ });
1281
+ } catch (error) {
1282
+ throw normalizeBindingError(error, {
1283
+ model: args.model,
1284
+ requestBodyValues: inputs
1285
+ });
1286
+ }
1293
1287
  if (response instanceof ReadableStream) return { stream: prependStreamStart(getMappedStream(response, {
1294
1288
  tools: args.tools,
1295
1289
  toolChoice: args.tool_choice
@@ -1369,15 +1363,25 @@ var WorkersAIImageModel = class {
1369
1363
  type: "unsupported"
1370
1364
  });
1371
1365
  const generateImage = async () => {
1372
- return toUint8Array$1(await this.config.binding.run(this.modelId, {
1366
+ const inputs = {
1373
1367
  height,
1374
1368
  prompt: prompt ?? "",
1375
1369
  seed,
1376
1370
  width
1377
- }, {
1378
- gateway: this.config.gateway,
1379
- signal: abortSignal
1380
- }));
1371
+ };
1372
+ let output;
1373
+ try {
1374
+ output = await this.config.binding.run(this.modelId, inputs, {
1375
+ gateway: this.config.gateway,
1376
+ signal: abortSignal
1377
+ });
1378
+ } catch (error) {
1379
+ throw normalizeBindingError(error, {
1380
+ model: this.modelId,
1381
+ requestBodyValues: inputs
1382
+ });
1383
+ }
1384
+ return toUint8Array$1(output);
1381
1385
  };
1382
1386
  return {
1383
1387
  images: await Promise.all(Array.from({ length: n }, () => generateImage())),
@@ -1466,8 +1470,15 @@ var WorkersAITranscriptionModel = class {
1466
1470
  const audioBytes = typeof audio === "string" ? Uint8Array.from(atob(audio), (c) => c.charCodeAt(0)) : audio;
1467
1471
  const isNova3 = this.modelId === "@cf/deepgram/nova-3";
1468
1472
  let rawResult;
1469
- if (isNova3) rawResult = await this.runNova3(audioBytes, mediaType, abortSignal);
1470
- else rawResult = await this.runWhisper(audioBytes, abortSignal);
1473
+ try {
1474
+ if (isNova3) rawResult = await this.runNova3(audioBytes, mediaType, abortSignal);
1475
+ else rawResult = await this.runWhisper(audioBytes, abortSignal);
1476
+ } catch (error) {
1477
+ throw normalizeBindingError(error, {
1478
+ model: this.modelId,
1479
+ requestBodyValues: { mediaType }
1480
+ });
1481
+ }
1471
1482
  const result = rawResult;
1472
1483
  if (isNova3) return this.normalizeNova3Response(result, warnings);
1473
1484
  return this.normalizeWhisperResponse(result, warnings);
@@ -1581,12 +1592,28 @@ var WorkersAISpeechModel = class {
1581
1592
  const inputs = { text };
1582
1593
  if (voice) inputs.voice = voice;
1583
1594
  if (speed != null) inputs.speed = speed;
1584
- return {
1585
- audio: await toUint8Array(await this.config.binding.run(this.modelId, inputs, {
1595
+ let result;
1596
+ try {
1597
+ result = await this.config.binding.run(this.modelId, inputs, {
1586
1598
  gateway: this.config.gateway,
1587
1599
  signal: abortSignal,
1588
1600
  returnRawResponse: true
1589
- })),
1601
+ });
1602
+ } catch (error) {
1603
+ throw normalizeBindingError(error, {
1604
+ model: this.modelId,
1605
+ requestBodyValues: inputs
1606
+ });
1607
+ }
1608
+ if (result instanceof Response && !result.ok) {
1609
+ const errorBody = await result.text().catch(() => "<unable to read response body>");
1610
+ throw apiCallErrorFromResponse(result, errorBody, {
1611
+ url: `workers-ai:run/${this.modelId}`,
1612
+ requestBodyValues: inputs
1613
+ });
1614
+ }
1615
+ return {
1616
+ audio: await toUint8Array(result),
1590
1617
  warnings,
1591
1618
  response: {
1592
1619
  timestamp: /* @__PURE__ */ new Date(),
@@ -1661,11 +1688,20 @@ var WorkersAIRerankingModel = class {
1661
1688
  contexts: documentsToContexts(documents, warnings)
1662
1689
  };
1663
1690
  if (topN != null) inputs.top_k = topN;
1664
- return {
1665
- ranking: ((await this.config.binding.run(this.modelId, inputs, {
1691
+ let result;
1692
+ try {
1693
+ result = await this.config.binding.run(this.modelId, inputs, {
1666
1694
  gateway: this.config.gateway,
1667
1695
  signal: abortSignal
1668
- })).response ?? []).map((item) => ({
1696
+ });
1697
+ } catch (error) {
1698
+ throw normalizeBindingError(error, {
1699
+ model: this.modelId,
1700
+ requestBodyValues: inputs
1701
+ });
1702
+ }
1703
+ return {
1704
+ ranking: (result.response ?? []).map((item) => ({
1669
1705
  index: item.id ?? 0,
1670
1706
  relevanceScore: item.score ?? 0
1671
1707
  })).sort((a, b) => b.relevanceScore - a.relevanceScore),
@@ -1750,110 +1786,6 @@ function createClientFallbackModel(legs) {
1750
1786
  };
1751
1787
  }
1752
1788
  //#endregion
1753
- //#region src/resumable-stream.ts
1754
- function concat(a, b) {
1755
- const out = new Uint8Array(new ArrayBuffer(a.length + b.length));
1756
- out.set(a, 0);
1757
- out.set(b, a.length);
1758
- return out;
1759
- }
1760
- /** Index just past the last `\n\n` in `buf`, or -1 if there is no complete event. */
1761
- function lastEventBoundary(buf) {
1762
- for (let i = buf.length - 2; i >= 0; i--) if (buf[i] === 10 && buf[i + 1] === 10) return i + 2;
1763
- return -1;
1764
- }
1765
- /** Count of `\n\n` terminators (= complete SSE events) in `buf`. */
1766
- function countEvents(buf) {
1767
- let n = 0;
1768
- for (let i = 0; i + 1 < buf.length; i++) if (buf[i] === 10 && buf[i + 1] === 10) {
1769
- n++;
1770
- i++;
1771
- }
1772
- return n;
1773
- }
1774
- function resumeUrl(gateway, runId, from) {
1775
- return `https://workers-binding.ai/ai-gateway/gateways/${gateway}/run/${runId}/resume?from=${from}`;
1776
- }
1777
- function createResumableStream(options) {
1778
- const { binding, gateway, runId } = options;
1779
- const maxReconnects = options.maxReconnects ?? 5;
1780
- const onExpired = options.onResumeExpired ?? "error";
1781
- let emittedEvents = options.fromEvent ?? 0;
1782
- let pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
1783
- let reconnects = 0;
1784
- async function fetchResume(controller) {
1785
- let res;
1786
- try {
1787
- res = await binding.fetch(resumeUrl(gateway, runId, emittedEvents), { method: "GET" });
1788
- } catch (fetchErr) {
1789
- controller.error(new GatewayDelegateError("dispatch", `Resume request threw at event ${emittedEvents}.`, fetchErr));
1790
- return null;
1791
- }
1792
- if (res.status === 404) {
1793
- if (onExpired === "accept-partial") {
1794
- controller.close();
1795
- return null;
1796
- }
1797
- controller.error(new GatewayDelegateError("resume-expired", `Resume buffer expired (404) at event ${emittedEvents}. The gateway buffer TTL (~5.5 min) elapsed; fall back to continuation or regeneration.`));
1798
- return null;
1799
- }
1800
- if (!res.ok || !res.body) {
1801
- controller.error(new GatewayDelegateError("dispatch", `Resume failed (${res.status}) at event ${emittedEvents}.`));
1802
- return null;
1803
- }
1804
- return res.body;
1805
- }
1806
- return new ReadableStream({ async start(controller) {
1807
- let current;
1808
- if (options.initial) current = options.initial;
1809
- else {
1810
- const body = await fetchResume(controller);
1811
- if (!body) return;
1812
- current = body;
1813
- }
1814
- for (;;) {
1815
- const reader = current.getReader();
1816
- try {
1817
- for (;;) {
1818
- const { done, value } = await reader.read();
1819
- if (done) {
1820
- if (pending.length > 0) {
1821
- controller.enqueue(pending);
1822
- pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
1823
- }
1824
- controller.close();
1825
- return;
1826
- }
1827
- if (!value || value.length === 0) continue;
1828
- pending = concat(pending, value);
1829
- const boundary = lastEventBoundary(pending);
1830
- if (boundary > 0) {
1831
- const complete = pending.slice(0, boundary);
1832
- controller.enqueue(complete);
1833
- emittedEvents += countEvents(complete);
1834
- options.onProgress?.(emittedEvents);
1835
- pending = pending.slice(boundary);
1836
- }
1837
- }
1838
- } catch (err) {
1839
- try {
1840
- reader.releaseLock();
1841
- } catch {}
1842
- if (reconnects >= maxReconnects) {
1843
- controller.error(new GatewayDelegateError("resume-expired", `Exceeded ${maxReconnects} reconnect attempts at event ${emittedEvents}.`, err));
1844
- return;
1845
- }
1846
- pending = new Uint8Array(/* @__PURE__ */ new ArrayBuffer(0));
1847
- reconnects++;
1848
- options.onReconnect?.(emittedEvents, reconnects);
1849
- const body = await fetchResume(controller);
1850
- if (!body) return;
1851
- current = body;
1852
- }
1853
- }
1854
- } });
1855
- }
1856
- //#endregion
1857
1789
  //#region src/gateway-delegate.ts
1858
1790
  /**
1859
1791
  * Parse a `vendor/model` slug. The first segment is the resolver key (which
@@ -1893,7 +1825,7 @@ function selectTransport(opts, resumeExplicitlyTrue, runCatalog = true, gatewayA
1893
1825
  const wantsCaching = opts.cacheTtl !== void 0 || opts.skipCache === true;
1894
1826
  const gatewayOnly = wantsServerFallback || wantsCaching;
1895
1827
  const feature = wantsServerFallback ? "fallback.mode:\"server\"" : "caching (cacheTtl/skipCache)";
1896
- if (runCatalog && !gatewayAvailable && (opts.transport === "gateway" || gatewayOnly)) throw new GatewayDelegateError("config", `${opts.transport === "gateway" ? "transport:\"gateway\"" : feature} is unavailable: this provider is on the unified run catalog but is not a native gateway provider, so it has no gateway path (no caching, server-side fallback, or transport:"gateway"). Use the default run path, or fallback.mode:"client".`);
1828
+ if (runCatalog && !gatewayAvailable && (opts.transport === "gateway" || opts.byok || gatewayOnly)) throw new GatewayDelegateError("config", `${opts.transport === "gateway" ? "transport:\"gateway\"" : opts.byok ? "byok" : feature} is unavailable: this provider is on the unified run catalog but is not a native gateway provider, so it has no gateway path (no caching, server-side fallback, BYOK, or transport:"gateway"). Use the default run path, or fallback.mode:"client".`);
1897
1829
  if (!runCatalog) {
1898
1830
  if (opts.transport === "run") throw new GatewayDelegateError("config", "transport:\"run\" is unavailable: this provider is not on the unified-billing run catalog, so it can only be reached through the gateway path (BYOK).");
1899
1831
  if (resumeExplicitlyTrue) throw new GatewayDelegateError("config", "resume:true is unavailable: this provider is not on the resumable run catalog (cf-aig-run-id requires the unified-billing run path).");
@@ -1903,6 +1835,15 @@ function selectTransport(opts, resumeExplicitlyTrue, runCatalog = true, gatewayA
1903
1835
  warnings
1904
1836
  };
1905
1837
  }
1838
+ if (opts.byok) {
1839
+ if (opts.transport === "run") throw new GatewayDelegateError("config", "transport:\"run\" cannot forward a BYOK key — BYOK is a gateway-path feature. Drop transport:\"run\" (or set transport:\"gateway\").");
1840
+ if (resumeExplicitlyTrue) throw new GatewayDelegateError("config", "byok cannot provide resume — cf-aig-run-id is only on the unified-billing run path.");
1841
+ return {
1842
+ transport: "gateway",
1843
+ resumeEnabled: false,
1844
+ warnings
1845
+ };
1846
+ }
1906
1847
  if (opts.transport === "run" && gatewayOnly) throw new GatewayDelegateError("config", `transport:"run" cannot satisfy ${feature}: those features are only available on the gateway path. Use the gateway transport, or fallback.mode:"client".`);
1907
1848
  if (opts.transport === "gateway" && resumeExplicitlyTrue) throw new GatewayDelegateError("config", "transport:\"gateway\" cannot provide resume — cf-aig-run-id is only on the run path.");
1908
1849
  if (gatewayOnly) {
@@ -1921,31 +1862,6 @@ function selectTransport(opts, resumeExplicitlyTrue, runCatalog = true, gatewayA
1921
1862
  warnings
1922
1863
  };
1923
1864
  }
1924
- var GatewayDelegateError = class extends Error {
1925
- constructor(kind, message, cause) {
1926
- super(message);
1927
- _defineProperty(this, "kind", void 0);
1928
- _defineProperty(this, "cause", void 0);
1929
- this.name = "GatewayDelegateError";
1930
- this.kind = kind;
1931
- this.cause = cause;
1932
- }
1933
- };
1934
- const STRIP_HEADERS_BASE = new Set(["content-length", "host"]);
1935
- function asText(body) {
1936
- if (typeof body === "string") return body;
1937
- if (body instanceof Uint8Array) return new TextDecoder().decode(body);
1938
- if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
1939
- return "{}";
1940
- }
1941
- function headersToObject(h) {
1942
- const out = {};
1943
- if (!h) return out;
1944
- if (h instanceof Headers) for (const [k, v] of h) out[k] = v;
1945
- else if (Array.isArray(h)) for (const [k, v] of h) out[k] = v;
1946
- else Object.assign(out, h);
1947
- return out;
1948
- }
1949
1865
  function normalizeGateway(gateway) {
1950
1866
  if (!gateway) throw new GatewayDelegateError("config", "A gateway is required for the delegate (resume needs a gateway). Pass `gateway: \"<gateway-id>\"` to createGatewayDelegate or per call.");
1951
1867
  if (typeof gateway === "string") return {
@@ -2005,6 +1921,49 @@ function createGatewayDelegate(config) {
2005
1921
  };
2006
1922
  }));
2007
1923
  }
1924
+ if (options.fallback?.mode === "server") {
1925
+ const resolved = [slug, ...options.fallback.models].map((s) => {
1926
+ const parsed = parseSlug(s);
1927
+ return {
1928
+ slug: s,
1929
+ modelId: parsed.modelId,
1930
+ info: resolveProvider(s, parsed)
1931
+ };
1932
+ });
1933
+ if (resolved.some((r) => r.info.gatewayProviderId !== resolved[0].info.gatewayProviderId)) {
1934
+ const resumeExplicitlyTrue = options.resume === true;
1935
+ const effectiveOptions = {
1936
+ ...options,
1937
+ resume: options.resume ?? defaultResume,
1938
+ onResumeExpired: options.onResumeExpired ?? config.onResumeExpired
1939
+ };
1940
+ const primaryInfo = resolved[0].info;
1941
+ const selection = selectTransport(effectiveOptions, resumeExplicitlyTrue, primaryInfo.runCatalog, primaryInfo.gatewayPath !== false);
1942
+ for (const w of selection.warnings) console.warn(w);
1943
+ const { id: gatewayId, options: gatewayOptions } = normalizeGateway(options.gateway ?? config.gateway);
1944
+ const legs = resolved.map((r) => {
1945
+ if (r.info.gatewayPath === false) throw new GatewayDelegateError("config", `Server-side fallback cannot use "${r.slug}": it is on the unified run catalog but has no native gateway path.`);
1946
+ const wire = r.info.wireFormat;
1947
+ const plugin = plugins.get(wire);
1948
+ if (!plugin) throw new GatewayDelegateError("config", `No provider plugin for wire format "${wire}" (needed by "${r.slug}" for server-side fallback). Registered: ${[...plugins.keys()].join(", ") || "<none>"}.`);
1949
+ return {
1950
+ slug: r.slug,
1951
+ modelId: r.modelId,
1952
+ info: r.info,
1953
+ plugin
1954
+ };
1955
+ });
1956
+ return makeServerFallbackModel({
1957
+ binding: config.binding,
1958
+ gatewayId,
1959
+ gatewayOptions,
1960
+ legs,
1961
+ opts: effectiveOptions,
1962
+ selection,
1963
+ callOptions: options
1964
+ });
1965
+ }
1966
+ }
2008
1967
  return buildOne(slug, options).model;
2009
1968
  };
2010
1969
  }
@@ -2029,9 +1988,27 @@ function mergeMetadata(base, override) {
2029
1988
  ...override
2030
1989
  };
2031
1990
  }
2032
- /** JSON-encode metadata for the `cf-aig-metadata` header (bigint → string). */
2033
- function serializeMetadata(metadata) {
2034
- return JSON.stringify(metadata, (_k, v) => typeof v === "bigint" ? v.toString() : v);
1991
+ /**
1992
+ * Build the `cf-aig-*` request controls for a gateway-path entry. First-class
1993
+ * call options (`cacheTtl`, `skipCache`, `collectLog`, `metadata`, `byokAlias`,
1994
+ * `zdr`) are layered over the binding-style gateway options (`cacheKey`,
1995
+ * `eventId`, `requestTimeoutMs`, `retries`) so the gateway path reaches parity
1996
+ * with the run path, which forwards the whole `GatewayOptions` to `binding.run`.
1997
+ */
1998
+ function buildGatewayControls(gatewayOptions, opts) {
1999
+ const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
2000
+ return {
2001
+ cacheTtl: opts.cacheTtl,
2002
+ skipCache: opts.skipCache,
2003
+ ...gatewayOptions.cacheKey !== void 0 ? { cacheKey: gatewayOptions.cacheKey } : {},
2004
+ ...metadata ? { metadata } : {},
2005
+ ...opts.collectLog !== void 0 ? { collectLog: opts.collectLog } : {},
2006
+ ...gatewayOptions.eventId !== void 0 ? { eventId: gatewayOptions.eventId } : {},
2007
+ ...gatewayOptions.requestTimeoutMs !== void 0 ? { requestTimeoutMs: gatewayOptions.requestTimeoutMs } : {},
2008
+ ...gatewayOptions.retries ? { retries: gatewayOptions.retries } : {},
2009
+ ...opts.byokAlias !== void 0 ? { byokAlias: opts.byokAlias } : {},
2010
+ ...opts.zdr !== void 0 ? { zdr: opts.zdr } : {}
2011
+ };
2035
2012
  }
2036
2013
  function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOptions) {
2037
2014
  return (async (_input, init) => {
@@ -2041,10 +2018,12 @@ function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOption
2041
2018
  const mergedMeta = mergeMetadata(gatewayOptions.metadata, opts.metadata);
2042
2019
  if (mergedMeta) mergedGateway.metadata = mergedMeta;
2043
2020
  if (opts.collectLog !== void 0) mergedGateway.collectLog = opts.collectLog;
2021
+ const extraHeaders = { ...opts.extraHeaders };
2022
+ if (opts.zdr !== void 0) extraHeaders["cf-aig-zdr"] = String(opts.zdr);
2044
2023
  const runOptions = {
2045
2024
  gateway: mergedGateway,
2046
2025
  returnRawResponse: true,
2047
- ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2026
+ ...Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {},
2048
2027
  ...init?.signal ? { signal: init.signal } : {}
2049
2028
  };
2050
2029
  const resp = await binding.run(slug, body, runOptions);
@@ -2068,31 +2047,24 @@ function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOption
2068
2047
  });
2069
2048
  }
2070
2049
  function makeGatewayFetch(binding, info, gatewayId, gatewayOptions, opts, selection, callOptions) {
2071
- const strip = new Set(STRIP_HEADERS_BASE);
2072
- if (!opts.byok) for (const h of info.authHeaders) strip.add(h.toLowerCase());
2073
2050
  return (async (input, init) => {
2074
2051
  const rawUrl = typeof input === "string" ? input : input.toString();
2075
2052
  const endpoint = info.transformEndpoint ? info.transformEndpoint(rawUrl) : new URL(rawUrl).pathname.replace(/^\//, "") + (new URL(rawUrl).search || "");
2076
2053
  const body = JSON.parse(asText(init?.body));
2077
- const headers = {};
2078
- for (const [k, v] of Object.entries(headersToObject(init?.headers))) if (!strip.has(k.toLowerCase())) headers[k] = v;
2079
- if (opts.extraHeaders) Object.assign(headers, opts.extraHeaders);
2080
- if (opts.cacheTtl !== void 0) headers["cf-aig-cache-ttl"] = String(opts.cacheTtl);
2081
- if (opts.skipCache) headers["cf-aig-skip-cache"] = "true";
2082
- const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
2083
- if (metadata) headers["cf-aig-metadata"] = serializeMetadata(metadata);
2084
- if (opts.collectLog !== void 0) headers["cf-aig-collect-log"] = String(opts.collectLog);
2085
- const primary = {
2086
- provider: info.gatewayProviderId,
2054
+ const primary = buildGatewayEntry({
2055
+ providerId: info.gatewayProviderId,
2087
2056
  endpoint,
2088
- headers,
2089
- query: body
2090
- };
2057
+ initHeaders: init?.headers,
2058
+ body,
2059
+ ...opts.byok ? {} : { stripAuthHeaders: info.authHeaders },
2060
+ ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2061
+ cache: buildGatewayControls(gatewayOptions, opts)
2062
+ });
2091
2063
  const entries = [primary];
2092
2064
  if (opts.fallback?.mode === "server") for (const fb of opts.fallback.models) {
2093
2065
  const fbParsed = parseSlug(fb);
2094
2066
  const fbInfo = resolveProvider(fb, fbParsed);
2095
- if (fbInfo.gatewayProviderId !== info.gatewayProviderId) throw new GatewayDelegateError("config", `Cross-vendor server-side fallback (${info.gatewayProviderId} → ${fbInfo.gatewayProviderId}) is not supported yet. Use fallback.mode:"client", or same-vendor fallback models.`);
2067
+ if (fbInfo.gatewayProviderId !== info.gatewayProviderId) throw new GatewayDelegateError("dispatch", `Internal: cross-vendor server fallback (${info.gatewayProviderId} → ${fbInfo.gatewayProviderId}) reached the same-vendor gateway fetch. This should have been routed through makeServerFallbackModel.`);
2096
2068
  entries.push({
2097
2069
  ...primary,
2098
2070
  query: {
@@ -2109,6 +2081,92 @@ function makeGatewayFetch(binding, info, gatewayId, gatewayOptions, opts, select
2109
2081
  return resp;
2110
2082
  });
2111
2083
  }
2084
+ /** Sentinel thrown by the capture fetch to stop a leg before it hits the network. */
2085
+ var ServerFallbackCaptureStop = class extends Error {};
2086
+ /**
2087
+ * Cross-vendor server-side fallback via the gateway universal endpoint.
2088
+ *
2089
+ * Unlike same-vendor fallback (which just swaps `model` in one body), legs here
2090
+ * are DIFFERENT providers with possibly different wire formats, so each leg's own
2091
+ * `@ai-sdk` model must shape its native request. Mirrors ai-gateway-provider's
2092
+ * `processModelRequest`: run each leg's builder with a capture `fetch` that
2093
+ * sentinel-throws before the network, reshape each captured request into a
2094
+ * `{ provider, endpoint, headers, query }` entry, dispatch all N as one
2095
+ * `env.AI.gateway(id).run([...])`, then read `cf-aig-step` to find the leg the
2096
+ * gateway actually served and feed the raw response back into that leg's model.
2097
+ */
2098
+ function makeServerFallbackModel(params) {
2099
+ const { binding, gatewayId, gatewayOptions, legs, opts, selection, callOptions } = params;
2100
+ const first = legs[0];
2101
+ const refModel = first.plugin.create({
2102
+ modelId: first.modelId,
2103
+ fetch: (async () => {
2104
+ throw new ServerFallbackCaptureStop();
2105
+ }),
2106
+ ...first.info.baseURL ? { baseURL: first.info.baseURL } : {}
2107
+ });
2108
+ const cache = buildGatewayControls(gatewayOptions, opts);
2109
+ const dispatch = async (method, options) => {
2110
+ const entries = [];
2111
+ for (const leg of legs) {
2112
+ let captured;
2113
+ const captureFetch = (async (input, init) => {
2114
+ captured = {
2115
+ url: typeof input === "string" ? input : input.toString(),
2116
+ init
2117
+ };
2118
+ throw new ServerFallbackCaptureStop();
2119
+ });
2120
+ const model = leg.plugin.create({
2121
+ modelId: leg.modelId,
2122
+ fetch: captureFetch,
2123
+ ...leg.info.baseURL ? { baseURL: leg.info.baseURL } : {}
2124
+ });
2125
+ try {
2126
+ await model[method](options);
2127
+ } catch (e) {
2128
+ if (!(e instanceof ServerFallbackCaptureStop)) throw e;
2129
+ }
2130
+ if (!captured) throw new GatewayDelegateError("dispatch", `Server-side fallback leg "${leg.slug}" produced no request to dispatch.`);
2131
+ const url = captured.url;
2132
+ const endpoint = leg.info.transformEndpoint ? leg.info.transformEndpoint(url) : new URL(url).pathname.replace(/^\//, "") + (new URL(url).search || "");
2133
+ const body = JSON.parse(asText(captured.init?.body));
2134
+ entries.push(buildGatewayEntry({
2135
+ providerId: leg.info.gatewayProviderId,
2136
+ endpoint,
2137
+ initHeaders: captured.init?.headers,
2138
+ body,
2139
+ ...opts.byok ? {} : { stripAuthHeaders: leg.info.authHeaders },
2140
+ ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2141
+ cache
2142
+ }));
2143
+ }
2144
+ const gw = binding.gateway(gatewayId);
2145
+ const abortSignal = options.abortSignal;
2146
+ const runOptions = {};
2147
+ if (abortSignal) runOptions.signal = abortSignal;
2148
+ const resp = await gw.run(entries, runOptions);
2149
+ fireDispatch(resp, selection, callOptions);
2150
+ const winner = legs[Number.parseInt(resp.headers.get("cf-aig-step") ?? "0", 10)] ?? first;
2151
+ return winner.plugin.create({
2152
+ modelId: winner.modelId,
2153
+ fetch: (async () => resp),
2154
+ ...winner.info.baseURL ? { baseURL: winner.info.baseURL } : {}
2155
+ })[method](options);
2156
+ };
2157
+ return {
2158
+ specificationVersion: "v3",
2159
+ provider: refModel.provider,
2160
+ modelId: refModel.modelId,
2161
+ supportedUrls: refModel.supportedUrls,
2162
+ doGenerate(options) {
2163
+ return dispatch("doGenerate", options);
2164
+ },
2165
+ doStream(options) {
2166
+ return dispatch("doStream", options);
2167
+ }
2168
+ };
2169
+ }
2112
2170
  //#endregion
2113
2171
  //#region src/index.ts
2114
2172
  /**
@@ -2140,8 +2198,11 @@ function createWorkersAI(options) {
2140
2198
  isBinding
2141
2199
  });
2142
2200
  const toGatewayOptions = (gateway) => typeof gateway === "string" ? { id: gateway } : gateway;
2143
- const createDynamicRouteModel = (modelId, settings = {}) => {
2144
- if (settings.fallback || settings.transport === "gateway" || settings.resume === true || settings.onProgress || settings.onResumeExpired || settings.byok) throw new Error(`"${modelId}" is an AI Gateway dynamic route. Dynamic routes use AI.run with OpenAI-compatible chat-completions wire format; fallback, gateway transport, resume, BYOK, and resume callbacks must be configured on the dynamic route or gateway instead of per call.`);
2201
+ const createRunPathModel = (modelId, settings = {}, kind) => {
2202
+ if (settings.fallback || settings.transport === "gateway" || settings.resume === true || settings.onProgress || settings.onResumeExpired || settings.byok) {
2203
+ const subject = kind === "dynamic" ? `"${modelId}" is an AI Gateway dynamic route` : `"${modelId}" routes through the bare unified-billing run path because no \`providers\` are configured`;
2204
+ throw new Error(`${subject}. It uses AI.run with OpenAI-compatible chat-completions wire format; fallback, gateway transport, resume, BYOK, and resume callbacks are gateway-delegate features — configure provider plugins to use them (createWorkersAI({ binding: env.AI, providers: [openai] })), and for dynamic routes set caching/fallback on the route or gateway instead of per call.`);
2205
+ }
2145
2206
  const gateway = { ...toGatewayOptions(settings.gateway) ?? options.gateway ?? { id: DEFAULT_GATEWAY_ID } };
2146
2207
  if (settings.metadata) gateway.metadata = {
2147
2208
  ...gateway.metadata ?? {},
@@ -2167,7 +2228,7 @@ function createWorkersAI(options) {
2167
2228
  delete chatSettings.byok;
2168
2229
  const plugin = options.providers?.find((p) => p.wireFormat === DYNAMIC_ROUTE_WIRE_FORMAT);
2169
2230
  if (!plugin) {
2170
- if (options.providers?.length) throw new Error(`"${modelId}" is an AI Gateway dynamic route. Dynamic routes return OpenAI-compatible chat-completions wire format on the AI.run path, so configure the OpenAI provider plugin: import { openai } from "workers-ai-provider/openai"; createWorkersAI({ binding: env.AI, providers: [openai] }).`);
2231
+ if (kind === "dynamic" && options.providers?.length) throw new Error(`"${modelId}" is an AI Gateway dynamic route. Dynamic routes return OpenAI-compatible chat-completions wire format on the AI.run path, so configure the OpenAI provider plugin: import { openai } from "workers-ai-provider/openai"; createWorkersAI({ binding: env.AI, providers: [openai] }).`);
2171
2232
  return createChatModel(modelId, chatSettings);
2172
2233
  }
2173
2234
  const fetchImpl = (async (_input, init) => {
@@ -2214,8 +2275,11 @@ A gateway defaults to "default" but can be set via \`gateway\`. Otherwise use a
2214
2275
  };
2215
2276
  const isGatewaySlug = (id) => typeof id === "string" && !id.startsWith("@") && !id.startsWith("dynamic/") && id.includes("/");
2216
2277
  const buildChat = (modelId, settings) => {
2217
- if (typeof modelId === "string" && modelId.startsWith("dynamic/")) return createDynamicRouteModel(modelId, settings);
2218
- if (isGatewaySlug(modelId)) return getDelegate(modelId)(modelId, settings);
2278
+ if (typeof modelId === "string" && modelId.startsWith("dynamic/")) return createRunPathModel(modelId, settings, "dynamic");
2279
+ if (isGatewaySlug(modelId)) {
2280
+ if (!options.providers?.length) return createRunPathModel(modelId, settings, "catalog");
2281
+ return getDelegate(modelId)(modelId, settings);
2282
+ }
2219
2283
  return createChatModel(modelId, settings);
2220
2284
  };
2221
2285
  const createImageModel = (modelId, settings = {}) => new WorkersAIImageModel(modelId, settings, {