workers-ai-provider 3.2.0 → 3.3.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.
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-BjPLZLob.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
@@ -1921,31 +1853,6 @@ function selectTransport(opts, resumeExplicitlyTrue, runCatalog = true, gatewayA
1921
1853
  warnings
1922
1854
  };
1923
1855
  }
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
1856
  function normalizeGateway(gateway) {
1950
1857
  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
1858
  if (typeof gateway === "string") return {
@@ -2005,6 +1912,49 @@ function createGatewayDelegate(config) {
2005
1912
  };
2006
1913
  }));
2007
1914
  }
1915
+ if (options.fallback?.mode === "server") {
1916
+ const resolved = [slug, ...options.fallback.models].map((s) => {
1917
+ const parsed = parseSlug(s);
1918
+ return {
1919
+ slug: s,
1920
+ modelId: parsed.modelId,
1921
+ info: resolveProvider(s, parsed)
1922
+ };
1923
+ });
1924
+ if (resolved.some((r) => r.info.gatewayProviderId !== resolved[0].info.gatewayProviderId)) {
1925
+ const resumeExplicitlyTrue = options.resume === true;
1926
+ const effectiveOptions = {
1927
+ ...options,
1928
+ resume: options.resume ?? defaultResume,
1929
+ onResumeExpired: options.onResumeExpired ?? config.onResumeExpired
1930
+ };
1931
+ const primaryInfo = resolved[0].info;
1932
+ const selection = selectTransport(effectiveOptions, resumeExplicitlyTrue, primaryInfo.runCatalog, primaryInfo.gatewayPath !== false);
1933
+ for (const w of selection.warnings) console.warn(w);
1934
+ const { id: gatewayId, options: gatewayOptions } = normalizeGateway(options.gateway ?? config.gateway);
1935
+ const legs = resolved.map((r) => {
1936
+ 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.`);
1937
+ const wire = r.info.wireFormat;
1938
+ const plugin = plugins.get(wire);
1939
+ 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>"}.`);
1940
+ return {
1941
+ slug: r.slug,
1942
+ modelId: r.modelId,
1943
+ info: r.info,
1944
+ plugin
1945
+ };
1946
+ });
1947
+ return makeServerFallbackModel({
1948
+ binding: config.binding,
1949
+ gatewayId,
1950
+ gatewayOptions,
1951
+ legs,
1952
+ opts: effectiveOptions,
1953
+ selection,
1954
+ callOptions: options
1955
+ });
1956
+ }
1957
+ }
2008
1958
  return buildOne(slug, options).model;
2009
1959
  };
2010
1960
  }
@@ -2029,9 +1979,27 @@ function mergeMetadata(base, override) {
2029
1979
  ...override
2030
1980
  };
2031
1981
  }
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);
1982
+ /**
1983
+ * Build the `cf-aig-*` request controls for a gateway-path entry. First-class
1984
+ * call options (`cacheTtl`, `skipCache`, `collectLog`, `metadata`, `byokAlias`,
1985
+ * `zdr`) are layered over the binding-style gateway options (`cacheKey`,
1986
+ * `eventId`, `requestTimeoutMs`, `retries`) so the gateway path reaches parity
1987
+ * with the run path, which forwards the whole `GatewayOptions` to `binding.run`.
1988
+ */
1989
+ function buildGatewayControls(gatewayOptions, opts) {
1990
+ const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
1991
+ return {
1992
+ cacheTtl: opts.cacheTtl,
1993
+ skipCache: opts.skipCache,
1994
+ ...gatewayOptions.cacheKey !== void 0 ? { cacheKey: gatewayOptions.cacheKey } : {},
1995
+ ...metadata ? { metadata } : {},
1996
+ ...opts.collectLog !== void 0 ? { collectLog: opts.collectLog } : {},
1997
+ ...gatewayOptions.eventId !== void 0 ? { eventId: gatewayOptions.eventId } : {},
1998
+ ...gatewayOptions.requestTimeoutMs !== void 0 ? { requestTimeoutMs: gatewayOptions.requestTimeoutMs } : {},
1999
+ ...gatewayOptions.retries ? { retries: gatewayOptions.retries } : {},
2000
+ ...opts.byokAlias !== void 0 ? { byokAlias: opts.byokAlias } : {},
2001
+ ...opts.zdr !== void 0 ? { zdr: opts.zdr } : {}
2002
+ };
2035
2003
  }
2036
2004
  function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOptions) {
2037
2005
  return (async (_input, init) => {
@@ -2041,10 +2009,12 @@ function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOption
2041
2009
  const mergedMeta = mergeMetadata(gatewayOptions.metadata, opts.metadata);
2042
2010
  if (mergedMeta) mergedGateway.metadata = mergedMeta;
2043
2011
  if (opts.collectLog !== void 0) mergedGateway.collectLog = opts.collectLog;
2012
+ const extraHeaders = { ...opts.extraHeaders };
2013
+ if (opts.zdr !== void 0) extraHeaders["cf-aig-zdr"] = String(opts.zdr);
2044
2014
  const runOptions = {
2045
2015
  gateway: mergedGateway,
2046
2016
  returnRawResponse: true,
2047
- ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2017
+ ...Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {},
2048
2018
  ...init?.signal ? { signal: init.signal } : {}
2049
2019
  };
2050
2020
  const resp = await binding.run(slug, body, runOptions);
@@ -2068,31 +2038,24 @@ function makeRunFetch(binding, slug, gatewayOptions, opts, selection, callOption
2068
2038
  });
2069
2039
  }
2070
2040
  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
2041
  return (async (input, init) => {
2074
2042
  const rawUrl = typeof input === "string" ? input : input.toString();
2075
2043
  const endpoint = info.transformEndpoint ? info.transformEndpoint(rawUrl) : new URL(rawUrl).pathname.replace(/^\//, "") + (new URL(rawUrl).search || "");
2076
2044
  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,
2045
+ const primary = buildGatewayEntry({
2046
+ providerId: info.gatewayProviderId,
2087
2047
  endpoint,
2088
- headers,
2089
- query: body
2090
- };
2048
+ initHeaders: init?.headers,
2049
+ body,
2050
+ ...opts.byok ? {} : { stripAuthHeaders: info.authHeaders },
2051
+ ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2052
+ cache: buildGatewayControls(gatewayOptions, opts)
2053
+ });
2091
2054
  const entries = [primary];
2092
2055
  if (opts.fallback?.mode === "server") for (const fb of opts.fallback.models) {
2093
2056
  const fbParsed = parseSlug(fb);
2094
2057
  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.`);
2058
+ 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
2059
  entries.push({
2097
2060
  ...primary,
2098
2061
  query: {
@@ -2109,6 +2072,92 @@ function makeGatewayFetch(binding, info, gatewayId, gatewayOptions, opts, select
2109
2072
  return resp;
2110
2073
  });
2111
2074
  }
2075
+ /** Sentinel thrown by the capture fetch to stop a leg before it hits the network. */
2076
+ var ServerFallbackCaptureStop = class extends Error {};
2077
+ /**
2078
+ * Cross-vendor server-side fallback via the gateway universal endpoint.
2079
+ *
2080
+ * Unlike same-vendor fallback (which just swaps `model` in one body), legs here
2081
+ * are DIFFERENT providers with possibly different wire formats, so each leg's own
2082
+ * `@ai-sdk` model must shape its native request. Mirrors ai-gateway-provider's
2083
+ * `processModelRequest`: run each leg's builder with a capture `fetch` that
2084
+ * sentinel-throws before the network, reshape each captured request into a
2085
+ * `{ provider, endpoint, headers, query }` entry, dispatch all N as one
2086
+ * `env.AI.gateway(id).run([...])`, then read `cf-aig-step` to find the leg the
2087
+ * gateway actually served and feed the raw response back into that leg's model.
2088
+ */
2089
+ function makeServerFallbackModel(params) {
2090
+ const { binding, gatewayId, gatewayOptions, legs, opts, selection, callOptions } = params;
2091
+ const first = legs[0];
2092
+ const refModel = first.plugin.create({
2093
+ modelId: first.modelId,
2094
+ fetch: (async () => {
2095
+ throw new ServerFallbackCaptureStop();
2096
+ }),
2097
+ ...first.info.baseURL ? { baseURL: first.info.baseURL } : {}
2098
+ });
2099
+ const cache = buildGatewayControls(gatewayOptions, opts);
2100
+ const dispatch = async (method, options) => {
2101
+ const entries = [];
2102
+ for (const leg of legs) {
2103
+ let captured;
2104
+ const captureFetch = (async (input, init) => {
2105
+ captured = {
2106
+ url: typeof input === "string" ? input : input.toString(),
2107
+ init
2108
+ };
2109
+ throw new ServerFallbackCaptureStop();
2110
+ });
2111
+ const model = leg.plugin.create({
2112
+ modelId: leg.modelId,
2113
+ fetch: captureFetch,
2114
+ ...leg.info.baseURL ? { baseURL: leg.info.baseURL } : {}
2115
+ });
2116
+ try {
2117
+ await model[method](options);
2118
+ } catch (e) {
2119
+ if (!(e instanceof ServerFallbackCaptureStop)) throw e;
2120
+ }
2121
+ if (!captured) throw new GatewayDelegateError("dispatch", `Server-side fallback leg "${leg.slug}" produced no request to dispatch.`);
2122
+ const url = captured.url;
2123
+ const endpoint = leg.info.transformEndpoint ? leg.info.transformEndpoint(url) : new URL(url).pathname.replace(/^\//, "") + (new URL(url).search || "");
2124
+ const body = JSON.parse(asText(captured.init?.body));
2125
+ entries.push(buildGatewayEntry({
2126
+ providerId: leg.info.gatewayProviderId,
2127
+ endpoint,
2128
+ initHeaders: captured.init?.headers,
2129
+ body,
2130
+ ...opts.byok ? {} : { stripAuthHeaders: leg.info.authHeaders },
2131
+ ...opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {},
2132
+ cache
2133
+ }));
2134
+ }
2135
+ const gw = binding.gateway(gatewayId);
2136
+ const abortSignal = options.abortSignal;
2137
+ const runOptions = {};
2138
+ if (abortSignal) runOptions.signal = abortSignal;
2139
+ const resp = await gw.run(entries, runOptions);
2140
+ fireDispatch(resp, selection, callOptions);
2141
+ const winner = legs[Number.parseInt(resp.headers.get("cf-aig-step") ?? "0", 10)] ?? first;
2142
+ return winner.plugin.create({
2143
+ modelId: winner.modelId,
2144
+ fetch: (async () => resp),
2145
+ ...winner.info.baseURL ? { baseURL: winner.info.baseURL } : {}
2146
+ })[method](options);
2147
+ };
2148
+ return {
2149
+ specificationVersion: "v3",
2150
+ provider: refModel.provider,
2151
+ modelId: refModel.modelId,
2152
+ supportedUrls: refModel.supportedUrls,
2153
+ doGenerate(options) {
2154
+ return dispatch("doGenerate", options);
2155
+ },
2156
+ doStream(options) {
2157
+ return dispatch("doStream", options);
2158
+ }
2159
+ };
2160
+ }
2112
2161
  //#endregion
2113
2162
  //#region src/index.ts
2114
2163
  /**
@@ -2116,6 +2165,7 @@ function makeGatewayFetch(binding, info, gatewayId, gatewayOptions, opts, select
2116
2165
  * configured. Every Cloudflare account has a `"default"` gateway.
2117
2166
  */
2118
2167
  const DEFAULT_GATEWAY_ID = "default";
2168
+ const DYNAMIC_ROUTE_WIRE_FORMAT = "openai";
2119
2169
  /**
2120
2170
  * Create a Workers AI provider instance.
2121
2171
  */
@@ -2138,6 +2188,64 @@ function createWorkersAI(options) {
2138
2188
  provider: "workersai.chat",
2139
2189
  isBinding
2140
2190
  });
2191
+ const toGatewayOptions = (gateway) => typeof gateway === "string" ? { id: gateway } : gateway;
2192
+ const createDynamicRouteModel = (modelId, settings = {}) => {
2193
+ 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.`);
2194
+ const gateway = { ...toGatewayOptions(settings.gateway) ?? options.gateway ?? { id: DEFAULT_GATEWAY_ID } };
2195
+ if (settings.metadata) gateway.metadata = {
2196
+ ...gateway.metadata ?? {},
2197
+ ...settings.metadata
2198
+ };
2199
+ if (settings.collectLog !== void 0) gateway.collectLog = settings.collectLog;
2200
+ if (settings.cacheTtl !== void 0) gateway.cacheTtl = settings.cacheTtl;
2201
+ if (settings.skipCache !== void 0) gateway.skipCache = settings.skipCache;
2202
+ const chatSettings = {
2203
+ ...settings,
2204
+ gateway
2205
+ };
2206
+ delete chatSettings.metadata;
2207
+ delete chatSettings.collectLog;
2208
+ delete chatSettings.cacheTtl;
2209
+ delete chatSettings.skipCache;
2210
+ delete chatSettings.resume;
2211
+ delete chatSettings.fallback;
2212
+ delete chatSettings.transport;
2213
+ delete chatSettings.onDispatch;
2214
+ delete chatSettings.onProgress;
2215
+ delete chatSettings.onResumeExpired;
2216
+ delete chatSettings.byok;
2217
+ const plugin = options.providers?.find((p) => p.wireFormat === DYNAMIC_ROUTE_WIRE_FORMAT);
2218
+ if (!plugin) {
2219
+ 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] }).`);
2220
+ return createChatModel(modelId, chatSettings);
2221
+ }
2222
+ const fetchImpl = (async (_input, init) => {
2223
+ const body = JSON.parse(String(init?.body ?? "{}"));
2224
+ delete body.model;
2225
+ const runOptions = {
2226
+ gateway,
2227
+ returnRawResponse: true,
2228
+ ...settings.extraHeaders ? { extraHeaders: settings.extraHeaders } : {},
2229
+ ...init?.signal ? { signal: init.signal } : {}
2230
+ };
2231
+ const response = await binding.run(modelId, body, runOptions);
2232
+ settings.onDispatch?.({
2233
+ transport: "run",
2234
+ resumeEnabled: false,
2235
+ warnings: [],
2236
+ status: response.status,
2237
+ runId: response.headers.get("cf-aig-run-id"),
2238
+ cfStep: response.headers.get("cf-aig-step"),
2239
+ cacheStatus: response.headers.get("cf-aig-cache-status"),
2240
+ logId: response.headers.get("cf-aig-log-id")
2241
+ });
2242
+ return response;
2243
+ });
2244
+ return plugin.create({
2245
+ modelId,
2246
+ fetch: fetchImpl
2247
+ });
2248
+ };
2141
2249
  let delegate;
2142
2250
  const getDelegate = (slug) => {
2143
2251
  if (!options.providers?.length) throw new Error(`"${slug}" looks like a third-party AI Gateway catalog model, but this Workers AI provider was not configured to route them. Pass provider plugins, e.g.:
@@ -2153,8 +2261,9 @@ A gateway defaults to "default" but can be set via \`gateway\`. Otherwise use a
2153
2261
  }));
2154
2262
  return delegate;
2155
2263
  };
2156
- const isGatewaySlug = (id) => typeof id === "string" && !id.startsWith("@") && id.includes("/");
2264
+ const isGatewaySlug = (id) => typeof id === "string" && !id.startsWith("@") && !id.startsWith("dynamic/") && id.includes("/");
2157
2265
  const buildChat = (modelId, settings) => {
2266
+ if (typeof modelId === "string" && modelId.startsWith("dynamic/")) return createDynamicRouteModel(modelId, settings);
2158
2267
  if (isGatewaySlug(modelId)) return getDelegate(modelId)(modelId, settings);
2159
2268
  return createChatModel(modelId, settings);
2160
2269
  };