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/README.md +28 -19
- package/dist/anthropic.d.mts +1 -1
- package/dist/{gateway-delegate-BfaUTwDZ.d.mts → gateway-delegate-D_zkIp5r.d.mts} +90 -15
- package/dist/{gateway-provider-1USFWm7c.mjs → gateway-provider-CQU-v2IO.mjs} +562 -170
- package/dist/gateway-provider-CQU-v2IO.mjs.map +1 -0
- package/dist/gateway-provider.mjs +1 -1
- package/dist/google.d.mts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +348 -284
- package/dist/index.mjs.map +1 -1
- package/dist/openai.d.mts +1 -1
- package/package.json +5 -4
- package/src/gateway-delegate.ts +318 -89
- package/src/gateway-provider.ts +10 -34
- package/src/gateway-providers.ts +13 -455
- package/src/index.ts +64 -20
- package/src/resumable-stream.ts +9 -221
- package/src/streaming.ts +1 -41
- package/src/utils.ts +40 -125
- package/src/workersai-chat-language-model.ts +37 -16
- package/src/workersai-embedding-model.ts +22 -11
- package/src/workersai-error.ts +70 -0
- package/src/workersai-image-model.ts +19 -11
- package/src/workersai-reranking-model.ts +16 -5
- package/src/workersai-speech-model.ts +35 -13
- package/src/workersai-transcription-model.ts +15 -4
- package/dist/gateway-provider-1USFWm7c.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
|
-
import { a as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
293
|
+
* `tool_calls`, assigning AI-SDK tool-call ids.
|
|
250
294
|
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1366
|
+
const inputs = {
|
|
1373
1367
|
height,
|
|
1374
1368
|
prompt: prompt ?? "",
|
|
1375
1369
|
seed,
|
|
1376
1370
|
width
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
1585
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
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
|
-
})
|
|
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
|
-
/**
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
...
|
|
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
|
|
2078
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
2144
|
-
if (settings.fallback || settings.transport === "gateway" || settings.resume === true || settings.onProgress || settings.onResumeExpired || settings.byok)
|
|
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
|
|
2218
|
-
if (isGatewaySlug(modelId))
|
|
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, {
|