agents 0.24.0 → 0.26.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/{agent-routing-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-routing.js.map +1 -1
- package/dist/agent-tool-types.d.ts +28 -22
- package/dist/agent-tool-types.js +23 -1
- package/dist/agent-tool-types.js.map +1 -1
- package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
- package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
- package/dist/{agent-tools-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
- package/dist/agent-tools.d.ts +20 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-fm7xx1BK.js +312 -0
- package/dist/ai-fm7xx1BK.js.map +1 -0
- package/dist/base64-redaction-C1Uu29t5.js +59 -0
- package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
- package/dist/browser/ai-sdk.d.ts +90 -0
- package/dist/browser/ai-sdk.js +51 -0
- package/dist/browser/ai-sdk.js.map +1 -0
- package/dist/browser/ai.d.ts +4 -4
- package/dist/browser/ai.js +1 -347
- package/dist/browser/index.d.ts +81 -79
- package/dist/browser/index.js +361 -2
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/tanstack-ai.d.ts +96 -1
- package/dist/browser/tanstack-ai.js +62 -2
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/browser-DE3d-RLk.d.ts +275 -0
- package/dist/browser-tool-CQ10e6Kj.js +480 -0
- package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
- package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
- package/dist/capability-B4WbF81e.js.map +1 -1
- package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
- package/dist/chat/index.d.ts +340 -234
- package/dist/chat/index.js +592 -66
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +42 -2
- package/dist/chat/react.js +191 -35
- package/dist/chat/react.js.map +1 -1
- package/dist/chat/transport.d.ts +1 -1
- package/dist/chat/transport.js +1 -1
- package/dist/chat-sdk/index.d.ts +17 -3
- package/dist/chat-sdk/index.js +28 -2
- package/dist/chat-sdk/index.js.map +1 -1
- package/dist/{client-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
- package/dist/client-4OlMIJx-.js.map +1 -0
- package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
- package/dist/client.d.ts +7 -5
- package/dist/client.js +2 -2
- package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
- package/dist/connector-58XDGvc3.js.map +1 -0
- package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
- package/dist/context/index.d.ts +29 -0
- package/dist/context/index.js +64 -17
- package/dist/context/index.js.map +1 -1
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{current-agent-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
- package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
- package/dist/harness/pi/index.d.ts +344 -0
- package/dist/harness/pi/index.js +873 -0
- package/dist/harness/pi/index.js.map +1 -0
- package/dist/images-iiTDSyRu.js +1631 -0
- package/dist/images-iiTDSyRu.js.map +1 -0
- package/dist/{index-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
- package/dist/index-kbSUIer3.d.ts +343 -0
- package/dist/index.d.ts +46 -44
- package/dist/index.js +1 -1
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +3 -3
- package/dist/lifecycle/index.js +1 -1
- package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
- package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/client/x402.js +21 -19
- package/dist/mcp/client/x402.js.map +1 -1
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
- package/dist/message-builder-DAl9AhHH.js.map +1 -0
- package/dist/models/ai-sdk/index.d.ts +353 -0
- package/dist/models/ai-sdk/index.js +2443 -0
- package/dist/models/ai-sdk/index.js.map +1 -0
- package/dist/models/pi-ai/index.d.ts +313 -0
- package/dist/models/pi-ai/index.js +1795 -0
- package/dist/models/pi-ai/index.js.map +1 -0
- package/dist/observability/ai/index.js +20 -4
- package/dist/observability/ai/index.js.map +1 -1
- package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
- package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
- package/dist/queue/index.d.ts +3 -3
- package/dist/queue/index.js +1 -1
- package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
- package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
- package/dist/react.d.ts +6 -6
- package/dist/react.js +160 -64
- package/dist/react.js.map +1 -1
- package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
- package/dist/retries.d.ts +11 -9
- package/dist/retries.js +11 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +2 -2
- package/dist/routing/index.js +1 -1
- package/dist/{scheduler-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
- package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
- package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +70 -3
- package/dist/sessions/index.js +276 -67
- package/dist/sessions/index.js.map +1 -1
- package/dist/skills/index.d.ts +29 -314
- package/dist/socket-address.d.ts +16 -0
- package/dist/socket-address.js +19 -0
- package/dist/socket-address.js.map +1 -0
- package/dist/{src-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
- package/dist/src-BZeu-SCD.js.map +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/dist/streams/index.d.ts +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
- package/dist/{streams-CCPRV6dt.js → streams-DW-UCOUc.js} +9 -2
- package/dist/streams-DW-UCOUc.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/{tasks-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
- package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
- package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
- package/dist/transport-PfFnt-Ld.d.ts +525 -0
- package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
- package/dist/voice/index.d.ts +2 -2
- package/dist/websockets/index.d.ts +23 -6
- package/dist/websockets/index.js +1 -1
- package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
- package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
- package/dist/wire-types-CnMt6_HR.js.map +1 -1
- package/dist/wire-types-CrjKvDyi.d.ts +300 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
- package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
- package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
- package/docs/agent-tools.md +29 -4
- package/docs/browse-the-web.md +87 -5
- package/docs/chat-agents.md +124 -16
- package/docs/context.md +27 -0
- package/docs/durable-execution.md +2 -0
- package/docs/harnesses/pi.md +173 -0
- package/docs/human-in-the-loop.md +4 -0
- package/docs/index.md +3 -0
- package/docs/lifecycle.md +20 -4
- package/docs/models-pi-ai.md +325 -0
- package/docs/models.md +504 -0
- package/docs/resumable-streaming.md +2 -0
- package/docs/routing.md +13 -13
- package/docs/sessions.md +36 -1
- package/docs/state.md +1 -0
- package/docs/sub-agents.md +9 -0
- package/package.json +40 -2
- package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
- package/dist/browser/ai.js.map +0 -1
- package/dist/client-C9DAjZIY.js.map +0 -1
- package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
- package/dist/connector-CptFKzRh.js.map +0 -1
- package/dist/message-builder-BymO4N_D.js.map +0 -1
- package/dist/src-BNU3ZiJM.js.map +0 -1
- package/dist/streams-CCPRV6dt.js.map +0 -1
- package/dist/ws-chat-transport-rWwta645.js.map +0 -1
|
@@ -0,0 +1,1631 @@
|
|
|
1
|
+
//#region src/models/core/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown for every failed Cloudflare AI call, whether it came back as a
|
|
4
|
+
* non-2xx `Response` or as a thrown Workers AI binding error. Framework
|
|
5
|
+
* modules wrap it into their own error type at their boundary (the AI SDK
|
|
6
|
+
* module into an `APICallError`, so `ai`'s retry loop honours
|
|
7
|
+
* {@link CloudflareAIError.isRetryable}).
|
|
8
|
+
*
|
|
9
|
+
* @experimental This surface is experimental and may change.
|
|
10
|
+
*/
|
|
11
|
+
var CloudflareAIError = class extends Error {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
super(options.message);
|
|
14
|
+
this.name = "CloudflareAIError";
|
|
15
|
+
this.status = options.status;
|
|
16
|
+
this.code = options.code;
|
|
17
|
+
this.logId = options.logId;
|
|
18
|
+
this.model = options.model;
|
|
19
|
+
this.url = options.url;
|
|
20
|
+
this.requestBodyValues = options.requestBodyValues;
|
|
21
|
+
this.responseHeaders = options.responseHeaders;
|
|
22
|
+
this.responseBody = options.responseBody;
|
|
23
|
+
this.isRetryable = options.isRetryable ?? isRetryableStatus(options.status);
|
|
24
|
+
this.data = options.data;
|
|
25
|
+
this.cause = options.cause;
|
|
26
|
+
this.attempts = options.attempts;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Whether a status is worth retrying. Missing status means a transport-level
|
|
31
|
+
* failure (connection reset, DNS, abort-adjacent), which is retryable.
|
|
32
|
+
*
|
|
33
|
+
* @experimental This surface is experimental and may change.
|
|
34
|
+
*/
|
|
35
|
+
function isRetryableStatus(status) {
|
|
36
|
+
if (status === void 0) return true;
|
|
37
|
+
return status === 408 || status === 409 || status === 429 || status >= 500;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Classifies an HTTP status into a {@link CloudflareAIErrorCode}. The
|
|
41
|
+
* `AiGatewayError` envelope name catches gateway-level statuses that are not
|
|
42
|
+
* otherwise meaningful (notably 402 "insufficient balance").
|
|
43
|
+
*/
|
|
44
|
+
function classifyStatus(status, envelopeName) {
|
|
45
|
+
if (status === 401 || status === 403) return "auth";
|
|
46
|
+
if (status === 429) return "rate-limit";
|
|
47
|
+
if (status === 404) return "not-found";
|
|
48
|
+
if (status === 400 || status === 422) return "bad-request";
|
|
49
|
+
if (status !== void 0 && status >= 500) return "provider-error";
|
|
50
|
+
if (envelopeName === "AiGatewayError") return "gateway-error";
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Workers AI binding error codes that map onto an HTTP status.
|
|
55
|
+
*
|
|
56
|
+
* @see https://developers.cloudflare.com/workers-ai/platform/errors/
|
|
57
|
+
*/
|
|
58
|
+
const BINDING_CODE_STATUS = {
|
|
59
|
+
3003: 400,
|
|
60
|
+
3006: 413,
|
|
61
|
+
3007: 408,
|
|
62
|
+
3008: 408,
|
|
63
|
+
3023: 403,
|
|
64
|
+
3036: 429,
|
|
65
|
+
3039: 400,
|
|
66
|
+
3040: 429,
|
|
67
|
+
3041: 403,
|
|
68
|
+
3042: 404,
|
|
69
|
+
5004: 400,
|
|
70
|
+
5005: 405,
|
|
71
|
+
5007: 400,
|
|
72
|
+
5016: 403,
|
|
73
|
+
5018: 403,
|
|
74
|
+
5019: 405
|
|
75
|
+
};
|
|
76
|
+
/** Abort-shaped errors must propagate untouched so callers can detect them. */
|
|
77
|
+
function isAbortError(error) {
|
|
78
|
+
const name = error instanceof Error || error instanceof DOMException ? error.name : void 0;
|
|
79
|
+
return name === "AbortError" || name === "ResponseAborted" || name === "TimeoutError";
|
|
80
|
+
}
|
|
81
|
+
function messageOf(error) {
|
|
82
|
+
if (typeof error === "string") return error;
|
|
83
|
+
if (error instanceof Error) return error.message;
|
|
84
|
+
if (error !== null && typeof error === "object" && "message" in error) {
|
|
85
|
+
const message = error.message;
|
|
86
|
+
if (typeof message === "string") return message;
|
|
87
|
+
}
|
|
88
|
+
return String(error);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Extracts a Workers AI internal error code from a thrown binding error. The
|
|
92
|
+
* binding reports codes inside the message text (`"3040: Capacity temporarily
|
|
93
|
+
* exceeded"`) rather than as a status, so only known codes are accepted — an
|
|
94
|
+
* unrelated leading number must not be misread as a code.
|
|
95
|
+
*/
|
|
96
|
+
function bindingErrorCode(error) {
|
|
97
|
+
if (error !== null && typeof error === "object" && "code" in error) {
|
|
98
|
+
const code = error.code;
|
|
99
|
+
if (typeof code === "number" && code in BINDING_CODE_STATUS) return code;
|
|
100
|
+
}
|
|
101
|
+
const matches = messageOf(error).matchAll(/\b(\d{3,5})\s*:/g);
|
|
102
|
+
for (const match of matches) {
|
|
103
|
+
const code = Number(match[1]);
|
|
104
|
+
if (code in BINDING_CODE_STATUS) return code;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Normalizes a thrown Workers AI binding error into a {@link CloudflareAIError}.
|
|
109
|
+
* Abort errors and errors that are already {@link CloudflareAIError}s pass through.
|
|
110
|
+
*/
|
|
111
|
+
function normalizeThrownError(error, context) {
|
|
112
|
+
if (isAbortError(error)) return error;
|
|
113
|
+
if (error instanceof CloudflareAIError) return error;
|
|
114
|
+
const code = bindingErrorCode(error);
|
|
115
|
+
const status = code === void 0 ? void 0 : BINDING_CODE_STATUS[code];
|
|
116
|
+
return new CloudflareAIError({
|
|
117
|
+
cause: error,
|
|
118
|
+
code: classifyStatus(status),
|
|
119
|
+
data: code === void 0 ? void 0 : { code },
|
|
120
|
+
isRetryable: status === void 0 ? false : isRetryableStatus(status),
|
|
121
|
+
message: messageOf(error),
|
|
122
|
+
model: context.model,
|
|
123
|
+
requestBodyValues: context.requestBodyValues,
|
|
124
|
+
status,
|
|
125
|
+
url: context.url
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/** Key that per-call options live under in a framework's per-call options bag. */
|
|
129
|
+
const PROVIDER_OPTIONS_KEY = "cloudflare";
|
|
130
|
+
/** The id legs of a fallback list; model objects are their module's business. */
|
|
131
|
+
function idLegs(fallback) {
|
|
132
|
+
return (fallback ?? []).filter((leg) => typeof leg === "string");
|
|
133
|
+
}
|
|
134
|
+
/** Strips `undefined` values so a later layer never blanks an earlier one. */
|
|
135
|
+
function defined(value) {
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const [key, entry] of Object.entries(value)) if (entry !== void 0) result[key] = entry;
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
function normalizeGateway(gateway) {
|
|
141
|
+
if (gateway === void 0) return {};
|
|
142
|
+
return typeof gateway === "string" ? { id: gateway } : defined(gateway);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Folds one layer's flat gateway sugar into its `gateway` object. The flat key
|
|
146
|
+
* wins over the nested one, and an explicit `undefined` on either never blanks
|
|
147
|
+
* a lower layer.
|
|
148
|
+
*/
|
|
149
|
+
function layerGateway(options) {
|
|
150
|
+
if (options === void 0) return {};
|
|
151
|
+
return {
|
|
152
|
+
...normalizeGateway(options.gateway),
|
|
153
|
+
...defined({
|
|
154
|
+
cacheKey: options.cacheKey,
|
|
155
|
+
cacheTtl: options.cacheTtl,
|
|
156
|
+
collectLog: options.collectLog,
|
|
157
|
+
eventId: options.eventId,
|
|
158
|
+
id: options.id,
|
|
159
|
+
metadata: options.metadata,
|
|
160
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
161
|
+
retries: options.retries,
|
|
162
|
+
skipCache: options.skipCache
|
|
163
|
+
})
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** The flat gateway sugar {@link layerGateway} folds into `gateway`. */
|
|
167
|
+
const FLAT_GATEWAY_KEYS = [
|
|
168
|
+
"cacheKey",
|
|
169
|
+
"cacheTtl",
|
|
170
|
+
"collectLog",
|
|
171
|
+
"eventId",
|
|
172
|
+
"gateway",
|
|
173
|
+
"id",
|
|
174
|
+
"metadata",
|
|
175
|
+
"requestTimeoutMs",
|
|
176
|
+
"retries",
|
|
177
|
+
"skipCache"
|
|
178
|
+
];
|
|
179
|
+
/**
|
|
180
|
+
* One layer's gateway options as a single object, both spellings folded in.
|
|
181
|
+
* Exported so a module that collapses two option layers into one bag (a
|
|
182
|
+
* fallback leg inheriting a chain's gateway) can merge them field by field
|
|
183
|
+
* rather than replacing one object with the other.
|
|
184
|
+
*
|
|
185
|
+
* @experimental This surface is experimental and may change.
|
|
186
|
+
*/
|
|
187
|
+
function gatewayLayerOf(options) {
|
|
188
|
+
return layerGateway(options);
|
|
189
|
+
}
|
|
190
|
+
/** An option bag without the gateway keys, whichever spelling they used. */
|
|
191
|
+
function withoutGatewayKeys(options) {
|
|
192
|
+
const rest = { ...options };
|
|
193
|
+
for (const key of FLAT_GATEWAY_KEYS) delete rest[key];
|
|
194
|
+
return defined(rest);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Collapses two option layers into one bag, `override` winning field by field.
|
|
198
|
+
*
|
|
199
|
+
* The gateway keys of both are folded into a single `gateway` object, so
|
|
200
|
+
* neither layer's flat sugar can out-rank the other's nested spelling when the
|
|
201
|
+
* bag is resolved later, and `metadata` merges rather than replaces — the same
|
|
202
|
+
* rules {@link resolveOptions} applies across its three layers.
|
|
203
|
+
*
|
|
204
|
+
* @experimental This surface is experimental and may change.
|
|
205
|
+
*/
|
|
206
|
+
function mergeModelOptions(base, override) {
|
|
207
|
+
if (base === void 0) return override;
|
|
208
|
+
if (override === void 0) return base;
|
|
209
|
+
const layers = [layerGateway(base), layerGateway(override)];
|
|
210
|
+
const gateway = Object.assign({}, ...layers);
|
|
211
|
+
const metadata = Object.assign({}, ...layers.map((layer) => layer.metadata));
|
|
212
|
+
const retries = mergeRetries(layers);
|
|
213
|
+
const headers = {
|
|
214
|
+
...base.headers,
|
|
215
|
+
...override.headers
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
...withoutGatewayKeys(base),
|
|
219
|
+
...withoutGatewayKeys(override),
|
|
220
|
+
...Object.keys(headers).length > 0 ? { headers } : {},
|
|
221
|
+
gateway: {
|
|
222
|
+
...gateway,
|
|
223
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {},
|
|
224
|
+
...retries === void 0 ? {} : { retries }
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Merges the retry policies field by field, so a provider-wide `backoff` still
|
|
230
|
+
* applies when a call only sets `maxAttempts`. `undefined` on any layer means
|
|
231
|
+
* "not set" and never blanks a lower one.
|
|
232
|
+
*/
|
|
233
|
+
function mergeRetries(layers) {
|
|
234
|
+
const merged = layers.reduce((result, layer) => ({
|
|
235
|
+
...result,
|
|
236
|
+
...layer.retries === void 0 ? {} : defined(layer.retries)
|
|
237
|
+
}), {});
|
|
238
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Merges the option layers. Precedence is per-call
|
|
242
|
+
* (`providerOptions.cloudflare`) > per-model (`ai(id, options)`) >
|
|
243
|
+
* provider (`createAI({ ... })`).
|
|
244
|
+
*
|
|
245
|
+
* The provider layer takes the whole settings object as well as a bare
|
|
246
|
+
* `gateway` value, so `createAI({ binding, id: "prod", cacheTtl: 60 })` reads
|
|
247
|
+
* the same as `createAI({ binding, gateway: { id: "prod", cacheTtl: 60 } })`.
|
|
248
|
+
*
|
|
249
|
+
* @experimental This surface is experimental and may change.
|
|
250
|
+
*/
|
|
251
|
+
function resolveOptions(provider, model, call) {
|
|
252
|
+
const layers = typeof provider === "string" ? [
|
|
253
|
+
normalizeGateway(provider),
|
|
254
|
+
layerGateway(model),
|
|
255
|
+
layerGateway(call)
|
|
256
|
+
] : [
|
|
257
|
+
layerGateway(provider),
|
|
258
|
+
layerGateway(model),
|
|
259
|
+
layerGateway(call)
|
|
260
|
+
];
|
|
261
|
+
const gateway = Object.assign({}, ...layers);
|
|
262
|
+
const metadata = Object.assign({}, ...layers.map((layer) => layer.metadata));
|
|
263
|
+
const retries = mergeRetries(layers);
|
|
264
|
+
return {
|
|
265
|
+
chatTemplateKwargs: call?.chatTemplateKwargs ?? model?.chatTemplateKwargs,
|
|
266
|
+
fallback: idLegs(call?.fallback ?? model?.fallback),
|
|
267
|
+
gateway: {
|
|
268
|
+
...gateway,
|
|
269
|
+
id: gateway.id ?? "default",
|
|
270
|
+
...Object.keys(metadata).length > 0 ? { metadata } : {},
|
|
271
|
+
...retries === void 0 ? {} : { retries }
|
|
272
|
+
},
|
|
273
|
+
headers: {
|
|
274
|
+
...model?.headers,
|
|
275
|
+
...call?.headers
|
|
276
|
+
},
|
|
277
|
+
reasoningEffort: call?.reasoningEffort !== void 0 ? call.reasoningEffort : model?.reasoningEffort,
|
|
278
|
+
sessionAffinity: call?.sessionAffinity ?? model?.sessionAffinity
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function asRecord(value) {
|
|
282
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
283
|
+
}
|
|
284
|
+
function asStringRecord(value) {
|
|
285
|
+
const record = asRecord(value);
|
|
286
|
+
if (record === void 0) return void 0;
|
|
287
|
+
const result = {};
|
|
288
|
+
for (const [key, entry] of Object.entries(record)) if (typeof entry === "string") result[key] = entry;
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
function asString(value) {
|
|
292
|
+
return typeof value === "string" ? value : void 0;
|
|
293
|
+
}
|
|
294
|
+
function asNumber(value) {
|
|
295
|
+
return typeof value === "number" ? value : void 0;
|
|
296
|
+
}
|
|
297
|
+
function asBoolean(value) {
|
|
298
|
+
return typeof value === "boolean" ? value : void 0;
|
|
299
|
+
}
|
|
300
|
+
function asReasoningEffort(value) {
|
|
301
|
+
if (value === null) return null;
|
|
302
|
+
if (value === "low" || value === "medium" || value === "high") return value;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Metadata values the gateway accepts are scalars only; a nested object or
|
|
306
|
+
* array would be rejected by the binding, so drop those rather than forward
|
|
307
|
+
* them.
|
|
308
|
+
*/
|
|
309
|
+
function asMetadata(value) {
|
|
310
|
+
const record = asRecord(value);
|
|
311
|
+
if (record === void 0) return void 0;
|
|
312
|
+
const result = {};
|
|
313
|
+
for (const [key, entry] of Object.entries(record)) if (entry === null || typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean") result[key] = entry;
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
const BACKOFFS = [
|
|
317
|
+
"constant",
|
|
318
|
+
"linear",
|
|
319
|
+
"exponential"
|
|
320
|
+
];
|
|
321
|
+
/** Reads the retry policy field by field; the binding validates it strictly. */
|
|
322
|
+
function asRetries(value) {
|
|
323
|
+
const record = asRecord(value);
|
|
324
|
+
if (record === void 0) return void 0;
|
|
325
|
+
const maxAttempts = asNumber(record.maxAttempts);
|
|
326
|
+
const backoff = asString(record.backoff);
|
|
327
|
+
const retries = defined({
|
|
328
|
+
backoff: BACKOFFS.find((entry) => entry === backoff),
|
|
329
|
+
maxAttempts: maxAttempts !== void 0 && Number.isInteger(maxAttempts) && maxAttempts >= 1 && maxAttempts <= 5 ? maxAttempts : void 0,
|
|
330
|
+
retryDelayMs: asNumber(record.retryDelayMs)
|
|
331
|
+
});
|
|
332
|
+
return Object.keys(retries).length > 0 ? retries : void 0;
|
|
333
|
+
}
|
|
334
|
+
/** Reads the shared gateway keys out of a loosely-typed record. */
|
|
335
|
+
function parseGatewayFields(raw) {
|
|
336
|
+
return defined({
|
|
337
|
+
cacheKey: asString(raw.cacheKey),
|
|
338
|
+
cacheTtl: asNumber(raw.cacheTtl),
|
|
339
|
+
collectLog: asBoolean(raw.collectLog),
|
|
340
|
+
eventId: asString(raw.eventId),
|
|
341
|
+
id: asString(raw.id),
|
|
342
|
+
metadata: asMetadata(raw.metadata),
|
|
343
|
+
requestTimeoutMs: asNumber(raw.requestTimeoutMs),
|
|
344
|
+
retries: asRetries(raw.retries),
|
|
345
|
+
skipCache: asBoolean(raw.skipCache)
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Reads per-call options out of a loosely-typed options bag (the value a
|
|
350
|
+
* framework keeps under {@link PROVIDER_OPTIONS_KEY}).
|
|
351
|
+
*
|
|
352
|
+
* The bag is plain JSON, so every field is read defensively: anything of an
|
|
353
|
+
* unexpected type is dropped rather than thrown, because a malformed
|
|
354
|
+
* observability hint should never fail a generation.
|
|
355
|
+
*
|
|
356
|
+
* @experimental This surface is experimental and may change.
|
|
357
|
+
*/
|
|
358
|
+
function parseModelOptions(value) {
|
|
359
|
+
const raw = asRecord(value);
|
|
360
|
+
if (raw === void 0) return void 0;
|
|
361
|
+
const nested = asRecord(raw.gateway);
|
|
362
|
+
const gateway = typeof raw.gateway === "string" ? raw.gateway : nested !== void 0 ? parseGatewayFields(nested) : void 0;
|
|
363
|
+
const fallback = Array.isArray(raw.fallback) ? raw.fallback.filter((entry) => typeof entry === "string") : void 0;
|
|
364
|
+
return defined({
|
|
365
|
+
...parseGatewayFields(raw),
|
|
366
|
+
chatTemplateKwargs: asRecord(raw.chatTemplateKwargs),
|
|
367
|
+
fallback,
|
|
368
|
+
gateway,
|
|
369
|
+
headers: asStringRecord(raw.headers),
|
|
370
|
+
reasoningEffort: asReasoningEffort(raw.reasoningEffort),
|
|
371
|
+
sessionAffinity: asString(raw.sessionAffinity)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/models/core/transport.ts
|
|
376
|
+
/** The reported `url` for a universal call made through the binding. */
|
|
377
|
+
const UNIVERSAL_BINDING_URL = "ai-gateway-binding";
|
|
378
|
+
/**
|
|
379
|
+
* Drops entries whose value is `undefined`. The AI SDK types call headers as
|
|
380
|
+
* `Record<string, string | undefined>`, and an undefined value reaches the
|
|
381
|
+
* binding as the string "undefined" rather than as an absent header.
|
|
382
|
+
*/
|
|
383
|
+
function definedHeaders(headers) {
|
|
384
|
+
const result = {};
|
|
385
|
+
for (const [key, value] of Object.entries(headers ?? {})) if (value !== void 0) result[key] = value;
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
/** Copies response headers into a plain object. */
|
|
389
|
+
function headersToObject(headers) {
|
|
390
|
+
const result = {};
|
|
391
|
+
headers.forEach((value, key) => {
|
|
392
|
+
result[key] = value;
|
|
393
|
+
});
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Some run-path responses arrive bare and some wrapped in the Cloudflare API
|
|
398
|
+
* envelope. Unwrap only when the envelope keys are actually present, so a
|
|
399
|
+
* provider payload that happens to have a `result` field is left alone.
|
|
400
|
+
*/
|
|
401
|
+
function unwrapEnvelope(value, context) {
|
|
402
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
403
|
+
const record = value;
|
|
404
|
+
if (record.success === false && firstEnvelopeError(record) !== void 0 || "success" in record && "result" in record) {
|
|
405
|
+
if (record.success === true) return record.result;
|
|
406
|
+
throw new CloudflareAIError({
|
|
407
|
+
code: classifyStatus(context?.status, typeof record.name === "string" ? record.name : void 0),
|
|
408
|
+
data: value,
|
|
409
|
+
message: errorMessageFrom(value, context?.status),
|
|
410
|
+
model: context?.model ?? "",
|
|
411
|
+
requestBodyValues: void 0,
|
|
412
|
+
status: context?.status,
|
|
413
|
+
url: context?.url ?? ""
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
function firstEnvelopeError(record) {
|
|
419
|
+
for (const key of ["error", "errors"]) {
|
|
420
|
+
const value = record[key];
|
|
421
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
422
|
+
const first = value[0];
|
|
423
|
+
if (first !== null && typeof first === "object") return first;
|
|
424
|
+
}
|
|
425
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Pulls a human message out of any of the error envelopes the platform emits:
|
|
430
|
+
* the gateway envelope (`error: [{ code, message }]`), the Workers AI
|
|
431
|
+
* validation envelope (`message`/`description` only), a bare
|
|
432
|
+
* `{ error: "..." }`, and the "Model ID missing" internal error, which carries
|
|
433
|
+
* only `description`.
|
|
434
|
+
*/
|
|
435
|
+
function errorMessageFrom(body, status) {
|
|
436
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
|
|
437
|
+
if (record !== void 0) {
|
|
438
|
+
const envelope = firstEnvelopeError(record);
|
|
439
|
+
if (typeof envelope?.message === "string") return envelope.message;
|
|
440
|
+
if (typeof record.error === "string") return record.error;
|
|
441
|
+
if (typeof record.message === "string") return record.message;
|
|
442
|
+
if (typeof record.description === "string") return record.description;
|
|
443
|
+
}
|
|
444
|
+
if (typeof body === "string" && body !== "") return body;
|
|
445
|
+
return `HTTP ${status ?? "error"}`;
|
|
446
|
+
}
|
|
447
|
+
/** Builds a {@link CloudflareAIError} from a non-2xx run-path response. */
|
|
448
|
+
async function errorFromResponse(response, context) {
|
|
449
|
+
const text = await response.text().catch(() => "");
|
|
450
|
+
let body = text;
|
|
451
|
+
try {
|
|
452
|
+
body = text === "" ? void 0 : JSON.parse(text);
|
|
453
|
+
} catch {}
|
|
454
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
|
|
455
|
+
const envelopeName = typeof record?.name === "string" ? record.name : void 0;
|
|
456
|
+
const headers = headersToObject(response.headers);
|
|
457
|
+
return new CloudflareAIError({
|
|
458
|
+
code: classifyStatus(response.status, envelopeName),
|
|
459
|
+
data: body,
|
|
460
|
+
logId: headers["cf-aig-log-id"],
|
|
461
|
+
message: errorMessageFrom(body, response.status),
|
|
462
|
+
model: context.model,
|
|
463
|
+
requestBodyValues: context.requestBodyValues,
|
|
464
|
+
responseBody: text,
|
|
465
|
+
responseHeaders: headers,
|
|
466
|
+
status: response.status,
|
|
467
|
+
url: context.url
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* The gateway's own options for the universal call. The id is not among them:
|
|
472
|
+
* it selects the gateway (`env.AI.gateway(id)`), and the binding's own `id`
|
|
473
|
+
* field is deprecated.
|
|
474
|
+
*/
|
|
475
|
+
function universalGatewayOptions(gateway) {
|
|
476
|
+
const options = { id: gateway.id };
|
|
477
|
+
if (gateway.skipCache !== void 0) options.skipCache = gateway.skipCache;
|
|
478
|
+
if (gateway.cacheTtl !== void 0) options.cacheTtl = gateway.cacheTtl;
|
|
479
|
+
if (gateway.cacheKey !== void 0) options.cacheKey = gateway.cacheKey;
|
|
480
|
+
if (gateway.metadata !== void 0) options.metadata = gateway.metadata;
|
|
481
|
+
if (gateway.collectLog !== void 0) options.collectLog = gateway.collectLog;
|
|
482
|
+
if (gateway.eventId !== void 0) options.eventId = gateway.eventId;
|
|
483
|
+
if (gateway.requestTimeoutMs !== void 0) options.requestTimeoutMs = gateway.requestTimeoutMs;
|
|
484
|
+
if (gateway.retries !== void 0) options.retries = gateway.retries;
|
|
485
|
+
return options;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Whether a parsed body is the gateway's own error envelope rather than a
|
|
489
|
+
* vendor payload — `{ success: false, error: [{ code, message }] }`, named
|
|
490
|
+
* `AiGatewayError`. The gateway answers this for its own failures (no such
|
|
491
|
+
* gateway, unified billing unpaid, provider not configured); everything else
|
|
492
|
+
* on this path is the vendor's own error, which the vendor's provider reads.
|
|
493
|
+
*
|
|
494
|
+
* @experimental This surface is experimental and may change.
|
|
495
|
+
*/
|
|
496
|
+
function isGatewayErrorEnvelope(json) {
|
|
497
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) return false;
|
|
498
|
+
const record = json;
|
|
499
|
+
if (record.name === "AiGatewayError") return true;
|
|
500
|
+
if (record.success !== false) return false;
|
|
501
|
+
const errors = record.error ?? record.errors;
|
|
502
|
+
return Array.isArray(errors) && errors.some((entry) => entry !== null && typeof entry === "object" && typeof entry.message === "string");
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Builds a {@link CloudflareAIError} from the gateway's own error envelope, so
|
|
506
|
+
* a gateway failure on the universal path classifies and retries exactly as
|
|
507
|
+
* one on the Workers AI run path.
|
|
508
|
+
*
|
|
509
|
+
* @experimental This surface is experimental and may change.
|
|
510
|
+
*/
|
|
511
|
+
function errorFromGatewayEnvelope(body, context) {
|
|
512
|
+
const record = body !== null && typeof body === "object" && !Array.isArray(body) ? body : void 0;
|
|
513
|
+
return new CloudflareAIError({
|
|
514
|
+
code: classifyStatus(context.status, typeof record?.name === "string" ? record.name : "AiGatewayError"),
|
|
515
|
+
data: body,
|
|
516
|
+
logId: context.responseHeaders?.["cf-aig-log-id"],
|
|
517
|
+
message: errorMessageFrom(body, context.status),
|
|
518
|
+
model: context.model,
|
|
519
|
+
requestBodyValues: context.requestBodyValues,
|
|
520
|
+
responseBody: context.responseBody,
|
|
521
|
+
responseHeaders: context.responseHeaders,
|
|
522
|
+
status: context.status,
|
|
523
|
+
url: context.url
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The model id a universal request is about, for error reporting. Every vendor
|
|
528
|
+
* body carries `model`; the endpoint is the fallback.
|
|
529
|
+
*/
|
|
530
|
+
function universalModel(request) {
|
|
531
|
+
const query = request.query;
|
|
532
|
+
if (query !== null && typeof query === "object" && !Array.isArray(query)) {
|
|
533
|
+
const model = query.model;
|
|
534
|
+
if (typeof model === "string") return model;
|
|
535
|
+
}
|
|
536
|
+
return `${request.provider}/${request.endpoint}`;
|
|
537
|
+
}
|
|
538
|
+
/** The keyless in-Worker backend: `env.AI.run(...)`. */
|
|
539
|
+
function bindingTransport(binding) {
|
|
540
|
+
const raw = binding;
|
|
541
|
+
const logIds = /* @__PURE__ */ new WeakMap();
|
|
542
|
+
return {
|
|
543
|
+
logIdFallback(response) {
|
|
544
|
+
return logIds.get(response);
|
|
545
|
+
},
|
|
546
|
+
async run(request) {
|
|
547
|
+
try {
|
|
548
|
+
const response = await raw.run(request.model, request.input, {
|
|
549
|
+
...Object.keys(request.headers).length > 0 ? { extraHeaders: request.headers } : {},
|
|
550
|
+
gateway: request.gateway,
|
|
551
|
+
returnRawResponse: true,
|
|
552
|
+
signal: request.signal
|
|
553
|
+
});
|
|
554
|
+
const logId = binding.aiGatewayLogId;
|
|
555
|
+
if (logId !== null && logId !== void 0) logIds.set(response, logId);
|
|
556
|
+
return response;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
throw normalizeThrownError(error, {
|
|
559
|
+
model: request.model,
|
|
560
|
+
requestBodyValues: request.input,
|
|
561
|
+
url: "workers-ai-binding"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
async universal(request) {
|
|
566
|
+
try {
|
|
567
|
+
return await binding.gateway(request.gateway.id).run({
|
|
568
|
+
endpoint: request.endpoint,
|
|
569
|
+
headers: request.headers,
|
|
570
|
+
provider: request.provider,
|
|
571
|
+
query: request.query
|
|
572
|
+
}, {
|
|
573
|
+
...request.extraHeaders === void 0 ? {} : { extraHeaders: request.extraHeaders },
|
|
574
|
+
gateway: universalGatewayOptions(request.gateway),
|
|
575
|
+
signal: request.signal
|
|
576
|
+
});
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw normalizeThrownError(error, {
|
|
579
|
+
model: universalModel(request),
|
|
580
|
+
requestBodyValues: request.query,
|
|
581
|
+
url: UNIVERSAL_BINDING_URL
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
url: "workers-ai-binding"
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Builds the transport for a set of provider settings. The model id, not the
|
|
590
|
+
* caller, decides the wire format; the transport only decides how the request
|
|
591
|
+
* physically leaves the Worker.
|
|
592
|
+
*
|
|
593
|
+
* @experimental This surface is experimental and may change.
|
|
594
|
+
*/
|
|
595
|
+
function createTransport(settings) {
|
|
596
|
+
if (settings.binding === void 0) throw new Error("createAI requires { binding }: the Workers AI binding.");
|
|
597
|
+
return bindingTransport(settings.binding);
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/models/core/gateway-providers.ts
|
|
601
|
+
/**
|
|
602
|
+
* The AI Gateway provider table: which gateway provider slug a vendor's own
|
|
603
|
+
* base URL belongs to, and what the universal request's `endpoint` is for a
|
|
604
|
+
* given upstream URL.
|
|
605
|
+
*
|
|
606
|
+
* This is platform knowledge, not vendor knowledge — the gateway publishes the
|
|
607
|
+
* list, and every entry here appears in `AIGatewayProviders` from
|
|
608
|
+
* `@cloudflare/workers-types`. Nothing about a vendor's wire format, model ids
|
|
609
|
+
* or capabilities lives here: the vendor's own provider package builds the
|
|
610
|
+
* request, and this table only says where it goes.
|
|
611
|
+
*
|
|
612
|
+
* Adapted from `@cloudflare/ai-gateway-provider`'s `providers.ts`.
|
|
613
|
+
*
|
|
614
|
+
* @experimental This surface is experimental and may change.
|
|
615
|
+
*/
|
|
616
|
+
/**
|
|
617
|
+
* The gateway provider slugs this table maps onto. A subset of the ambient
|
|
618
|
+
* `AIGatewayProviders` union; {@link GatewayProviderNameCheck} keeps it one.
|
|
619
|
+
*
|
|
620
|
+
* @experimental This surface is experimental and may change.
|
|
621
|
+
*/
|
|
622
|
+
const GATEWAY_PROVIDER_NAMES = [
|
|
623
|
+
"workers-ai",
|
|
624
|
+
"anthropic",
|
|
625
|
+
"openai",
|
|
626
|
+
"azure-openai",
|
|
627
|
+
"google-ai-studio",
|
|
628
|
+
"google-vertex-ai",
|
|
629
|
+
"grok",
|
|
630
|
+
"mistral",
|
|
631
|
+
"perplexity-ai",
|
|
632
|
+
"replicate",
|
|
633
|
+
"groq",
|
|
634
|
+
"deepseek",
|
|
635
|
+
"openrouter",
|
|
636
|
+
"cerebras",
|
|
637
|
+
"cohere",
|
|
638
|
+
"huggingface",
|
|
639
|
+
"aws-bedrock",
|
|
640
|
+
"elevenlabs",
|
|
641
|
+
"cartesia"
|
|
642
|
+
];
|
|
643
|
+
/** The default credential header; providers add their own on top. */
|
|
644
|
+
const AUTHORIZATION = ["authorization"];
|
|
645
|
+
/** Path after the host, leading slash removed, query string kept. */
|
|
646
|
+
function pathOf(url) {
|
|
647
|
+
return `${url.pathname.replace(/^\/+/, "")}${url.search}`;
|
|
648
|
+
}
|
|
649
|
+
function buildProvider(spec) {
|
|
650
|
+
const map = spec.endpoint ?? ((url) => {
|
|
651
|
+
const path = pathOf(url);
|
|
652
|
+
return spec.pathPrefix === void 0 ? path : path.replace(spec.pathPrefix, "");
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
authHeaders: spec.authHeaders ?? AUTHORIZATION,
|
|
656
|
+
endpoint(url) {
|
|
657
|
+
const parsed = parseUrl(url);
|
|
658
|
+
if (parsed === void 0 || !spec.host.test(parsed.host)) return;
|
|
659
|
+
return map(parsed);
|
|
660
|
+
},
|
|
661
|
+
host: spec.host,
|
|
662
|
+
name: spec.name
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function parseUrl(url) {
|
|
666
|
+
try {
|
|
667
|
+
return new URL(url);
|
|
668
|
+
} catch {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Azure names the deployment in the path; the gateway names it in the
|
|
674
|
+
* endpoint as `<resource>/<deployment>/<rest>`.
|
|
675
|
+
*/
|
|
676
|
+
function azureEndpoint(url) {
|
|
677
|
+
const resource = /^([^.]+)\.openai\.azure\.com$/.exec(url.host)?.[1];
|
|
678
|
+
const rest = /^\/openai\/deployments\/([^/]+)\/(.+)$/.exec(url.pathname);
|
|
679
|
+
if (resource === void 0 || rest === null) return void 0;
|
|
680
|
+
return `${resource}/${rest[1]}/${rest[2]}${url.search}`;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Bedrock's region and service live in the host; the gateway wants them as the
|
|
684
|
+
* first two endpoint segments.
|
|
685
|
+
*/
|
|
686
|
+
function bedrockEndpoint(url) {
|
|
687
|
+
const match = /^(bedrock[a-z0-9-]*)\.([a-z0-9-]+)\.amazonaws\.com$/.exec(url.host);
|
|
688
|
+
if (match === null) return void 0;
|
|
689
|
+
return `${match[1]}/${match[2]}/${pathOf(url)}`;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* The provider table, in declaration order. Hosts do not overlap, so the order
|
|
693
|
+
* only decides which row a future overlapping host would win.
|
|
694
|
+
*
|
|
695
|
+
* @experimental This surface is experimental and may change.
|
|
696
|
+
*/
|
|
697
|
+
const GATEWAY_PROVIDERS = [
|
|
698
|
+
{
|
|
699
|
+
host: /^api\.cloudflare\.com$/,
|
|
700
|
+
name: "workers-ai",
|
|
701
|
+
pathPrefix: /^client\/v4\/accounts\/[^/]+\/ai\//
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
authHeaders: ["authorization", "x-api-key"],
|
|
705
|
+
host: /^api\.anthropic\.com$/,
|
|
706
|
+
name: "anthropic"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
host: /^api\.openai\.com$/,
|
|
710
|
+
name: "openai"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
authHeaders: ["authorization", "api-key"],
|
|
714
|
+
endpoint: azureEndpoint,
|
|
715
|
+
host: /^[^.]+\.openai\.azure\.com$/,
|
|
716
|
+
name: "azure-openai"
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
authHeaders: ["authorization", "x-goog-api-key"],
|
|
720
|
+
host: /^generativelanguage\.googleapis\.com$/,
|
|
721
|
+
name: "google-ai-studio"
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
host: /^(?:[a-z0-9-]+-)?aiplatform\.googleapis\.com$/,
|
|
725
|
+
name: "google-vertex-ai"
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
host: /^api\.x\.ai$/,
|
|
729
|
+
name: "grok"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
host: /^api\.mistral\.ai$/,
|
|
733
|
+
name: "mistral"
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
host: /^api\.perplexity\.ai$/,
|
|
737
|
+
name: "perplexity-ai"
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
host: /^api\.replicate\.com$/,
|
|
741
|
+
name: "replicate"
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
host: /^api\.groq\.com$/,
|
|
745
|
+
name: "groq",
|
|
746
|
+
pathPrefix: /^openai\/v1\//
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
host: /^api\.deepseek\.com$/,
|
|
750
|
+
name: "deepseek"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
host: /^openrouter\.ai$/,
|
|
754
|
+
name: "openrouter",
|
|
755
|
+
pathPrefix: /^api\//
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
host: /^api\.cerebras\.ai$/,
|
|
759
|
+
name: "cerebras"
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
host: /^api\.cohere\.(?:ai|com)$/,
|
|
763
|
+
name: "cohere"
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
host: /^(?:api-inference|router)\.huggingface\.co$/,
|
|
767
|
+
name: "huggingface",
|
|
768
|
+
pathPrefix: /^models\//
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
endpoint: bedrockEndpoint,
|
|
772
|
+
host: /^bedrock.*\.amazonaws\.com$/,
|
|
773
|
+
name: "aws-bedrock"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
host: /^api\.elevenlabs\.io$/,
|
|
777
|
+
name: "elevenlabs"
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
host: /^api\.cartesia\.ai$/,
|
|
781
|
+
name: "cartesia"
|
|
782
|
+
}
|
|
783
|
+
].map(buildProvider);
|
|
784
|
+
/**
|
|
785
|
+
* The table row for a bare host (`api.anthropic.com`) or for anything a `URL`
|
|
786
|
+
* can be built from. pi-ai models carry a `baseUrl` rather than a full request
|
|
787
|
+
* URL, so both spellings resolve.
|
|
788
|
+
*
|
|
789
|
+
* @experimental This surface is experimental and may change.
|
|
790
|
+
*/
|
|
791
|
+
function gatewayProviderForHost(hostOrUrl) {
|
|
792
|
+
const host = parseUrl(hostOrUrl)?.host ?? hostOf(hostOrUrl);
|
|
793
|
+
if (host === void 0) return void 0;
|
|
794
|
+
return GATEWAY_PROVIDERS.find((provider) => provider.host.test(host));
|
|
795
|
+
}
|
|
796
|
+
/** Reads a host out of a bare host, `host/path` or `//host` spelling. */
|
|
797
|
+
function hostOf(value) {
|
|
798
|
+
const host = value.replace(/^\/\//, "").split("/")[0];
|
|
799
|
+
return host === "" ? void 0 : host;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Maps a vendor request URL onto `{ provider, endpoint, authHeaders }` for the
|
|
803
|
+
* universal gateway request, or `undefined` when the host is not a gateway
|
|
804
|
+
* provider.
|
|
805
|
+
*
|
|
806
|
+
* @experimental This surface is experimental and may change.
|
|
807
|
+
*/
|
|
808
|
+
function resolveGatewayProvider(url) {
|
|
809
|
+
const parsed = parseUrl(url);
|
|
810
|
+
if (parsed === void 0) return void 0;
|
|
811
|
+
for (const provider of GATEWAY_PROVIDERS) {
|
|
812
|
+
if (!provider.host.test(parsed.host)) continue;
|
|
813
|
+
const endpoint = provider.endpoint(url);
|
|
814
|
+
if (endpoint === void 0) continue;
|
|
815
|
+
return {
|
|
816
|
+
authHeaders: provider.authHeaders,
|
|
817
|
+
endpoint,
|
|
818
|
+
provider: provider.name
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* {@link resolveGatewayProvider}, throwing a `TypeError` that names the host
|
|
824
|
+
* and the provider list when the URL belongs to no gateway provider.
|
|
825
|
+
*
|
|
826
|
+
* @experimental This surface is experimental and may change.
|
|
827
|
+
*/
|
|
828
|
+
function requireGatewayProvider(url) {
|
|
829
|
+
const resolved = resolveGatewayProvider(url);
|
|
830
|
+
if (resolved !== void 0) return resolved;
|
|
831
|
+
const host = parseUrl(url)?.host ?? url;
|
|
832
|
+
throw new TypeError(`AI Gateway has no provider for ${host}. Requests are routed by host; the supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
|
|
833
|
+
}
|
|
834
|
+
/** The host a gateway-shaped base URL points at. */
|
|
835
|
+
const GATEWAY_HOST = "gateway.ai.cloudflare.com";
|
|
836
|
+
/**
|
|
837
|
+
* The provider slug of a gateway-shaped base URL —
|
|
838
|
+
* `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/{slug}`. Generated
|
|
839
|
+
* registries write models that way (pi-ai's Cloudflare AI Gateway catalog
|
|
840
|
+
* leaves the account and gateway as literal placeholders), so the slug is
|
|
841
|
+
* already in the path rather than implied by a vendor host. The account and
|
|
842
|
+
* gateway segments are ignored: the binding has already chosen both.
|
|
843
|
+
*/
|
|
844
|
+
function gatewayShapedSlug(url) {
|
|
845
|
+
if (url.host !== GATEWAY_HOST) return void 0;
|
|
846
|
+
const segments = url.pathname.split("/").filter((part) => part !== "");
|
|
847
|
+
if (segments[0] !== "v1" || segments.length < 4) return void 0;
|
|
848
|
+
return segments[3];
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Whether a base URL is written in the gateway's own shape
|
|
852
|
+
* (`https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/{slug}`) rather
|
|
853
|
+
* than as a vendor's. A generated Cloudflare registry writes models that way,
|
|
854
|
+
* and the path after the slug is then the vendor's own, so a caller that
|
|
855
|
+
* derives an endpoint from a base URL has to tell the two apart.
|
|
856
|
+
*
|
|
857
|
+
* @experimental This surface is experimental and may change.
|
|
858
|
+
*/
|
|
859
|
+
function isGatewayShapedUrl(url) {
|
|
860
|
+
return parseUrl(url)?.host === GATEWAY_HOST;
|
|
861
|
+
}
|
|
862
|
+
function isGatewayProviderName(slug) {
|
|
863
|
+
return GATEWAY_PROVIDER_NAMES.includes(slug);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* The gateway provider slug a model's base URL belongs to. Two spellings
|
|
867
|
+
* resolve, because two kinds of registry write them:
|
|
868
|
+
*
|
|
869
|
+
* - a vendor's own base URL (`https://api.anthropic.com` → `anthropic`), which
|
|
870
|
+
* is what a provider package configured for the vendor carries;
|
|
871
|
+
* - a gateway-shaped base URL
|
|
872
|
+
* (`https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/anthropic` →
|
|
873
|
+
* `anthropic`), which is what a generated Cloudflare registry carries.
|
|
874
|
+
*
|
|
875
|
+
* A base URL neither shape resolves is a wiring mistake rather than a request
|
|
876
|
+
* failure, so it throws a `TypeError` naming what was not recognised.
|
|
877
|
+
*
|
|
878
|
+
* @experimental This surface is experimental and may change.
|
|
879
|
+
*/
|
|
880
|
+
function gatewaySlugForBaseUrl(baseUrl) {
|
|
881
|
+
const url = parseUrl(baseUrl);
|
|
882
|
+
const shaped = url === void 0 ? void 0 : gatewayShapedSlug(url);
|
|
883
|
+
if (shaped !== void 0) {
|
|
884
|
+
if (isGatewayProviderName(shaped)) return shaped;
|
|
885
|
+
throw new TypeError(`AI Gateway has no provider "${shaped}" (from ${baseUrl}). The supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
|
|
886
|
+
}
|
|
887
|
+
const provider = gatewayProviderForHost(baseUrl);
|
|
888
|
+
if (provider !== void 0) return provider.name;
|
|
889
|
+
throw new TypeError(`AI Gateway has no provider for ${url?.host ?? baseUrl}. Requests are routed by the model's base URL; the supported providers are ${GATEWAY_PROVIDER_NAMES.join(", ")}.`);
|
|
890
|
+
}
|
|
891
|
+
//#endregion
|
|
892
|
+
//#region src/models/core/chat-completions/quirks.ts
|
|
893
|
+
const STRICT_OPENAI = {
|
|
894
|
+
chatTemplateKwargs: "unsupported",
|
|
895
|
+
imageUrls: "any",
|
|
896
|
+
jsonSchema: "supported",
|
|
897
|
+
jsonSchemaStrict: false,
|
|
898
|
+
maxTokensField: "max_tokens",
|
|
899
|
+
modelEchoSuffix: void 0,
|
|
900
|
+
nullAssistantContent: "allowed",
|
|
901
|
+
reasoningEffortLevels: "basic",
|
|
902
|
+
reasoningOff: "unsupported",
|
|
903
|
+
reasoningOffStreamLabel: "content",
|
|
904
|
+
reasoningReplay: "drop",
|
|
905
|
+
requiresAssistantAfterToolResult: false,
|
|
906
|
+
toolCallIds: "any",
|
|
907
|
+
toolChoiceRequired: "supported"
|
|
908
|
+
};
|
|
909
|
+
/** Workers AI's vLLM front end: what every `@cf/` model has in common. */
|
|
910
|
+
const WORKERS_AI = {
|
|
911
|
+
...STRICT_OPENAI,
|
|
912
|
+
chatTemplateKwargs: "supported",
|
|
913
|
+
imageUrls: "data-only",
|
|
914
|
+
nullAssistantContent: "empty-string",
|
|
915
|
+
reasoningOff: "chat-template",
|
|
916
|
+
reasoningReplay: "reasoning_content"
|
|
917
|
+
};
|
|
918
|
+
/**
|
|
919
|
+
* Longest-prefix overrides. Keep each one evidence-backed (the capture that
|
|
920
|
+
* proves it is named in the comment) and delete it when the platform fixes
|
|
921
|
+
* the deviation.
|
|
922
|
+
*/
|
|
923
|
+
const OVERRIDES = [
|
|
924
|
+
["@cf/zai-org/glm-4.7-flash", { toolChoiceRequired: "named-fallback" }],
|
|
925
|
+
["@cf/moonshotai/", { reasoningOff: "unsupported" }],
|
|
926
|
+
["@cf/openai/", { reasoningOff: "unsupported" }],
|
|
927
|
+
["@cf/mistralai/", {
|
|
928
|
+
chatTemplateKwargs: "unsupported",
|
|
929
|
+
jsonSchema: "unsupported",
|
|
930
|
+
reasoningOff: "unsupported",
|
|
931
|
+
requiresAssistantAfterToolResult: true,
|
|
932
|
+
toolCallIds: "alnum-9"
|
|
933
|
+
}],
|
|
934
|
+
["@cf/google/", { modelEchoSuffix: "-external" }],
|
|
935
|
+
["@cf/nvidia/nemotron-", { reasoningOffStreamLabel: "reasoning" }]
|
|
936
|
+
];
|
|
937
|
+
/** The quirks for a catalog id: base profile plus the longest matching override. */
|
|
938
|
+
function quirksFor(modelId) {
|
|
939
|
+
const base = modelId.startsWith("@cf/") ? WORKERS_AI : STRICT_OPENAI;
|
|
940
|
+
let best;
|
|
941
|
+
for (const entry of OVERRIDES) if (modelId.startsWith(entry[0]) && (best === void 0 || entry[0].length > best[0].length)) best = entry;
|
|
942
|
+
return best === void 0 ? base : {
|
|
943
|
+
...base,
|
|
944
|
+
...best[1]
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/** Whether an id is a Workers AI model rather than a third-party catalog model. */
|
|
948
|
+
function isWorkersAI(modelId) {
|
|
949
|
+
return modelId.startsWith("@cf/");
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region src/models/core/chat-completions/request.ts
|
|
953
|
+
/**
|
|
954
|
+
* Rewrites a strict OpenAI chat-completions request body into what a given
|
|
955
|
+
* catalog model accepts. Frameworks build the strict body from their own
|
|
956
|
+
* prompt types; everything Cloudflare- or vendor-specific happens here, once.
|
|
957
|
+
*
|
|
958
|
+
* The rewrite never throws: a feature the model cannot take is dropped and
|
|
959
|
+
* reported as a {@link CompatWarning}.
|
|
960
|
+
*/
|
|
961
|
+
function isRecord$2(value) {
|
|
962
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
963
|
+
}
|
|
964
|
+
function clampEffort(effort) {
|
|
965
|
+
if (effort === "minimal") return "low";
|
|
966
|
+
if (effort === "xhigh") return "high";
|
|
967
|
+
return effort;
|
|
968
|
+
}
|
|
969
|
+
/** Drops URL image parts (keeps inline data URLs) from a user message. */
|
|
970
|
+
function stripImageUrls(message, warnings) {
|
|
971
|
+
if (!Array.isArray(message.content)) return message;
|
|
972
|
+
let dropped = 0;
|
|
973
|
+
const content = message.content.filter((part) => {
|
|
974
|
+
if (!isRecord$2(part) || part.type !== "image_url") return true;
|
|
975
|
+
const image = isRecord$2(part.image_url) ? part.image_url : void 0;
|
|
976
|
+
if ((typeof image?.url === "string" ? image.url : "").startsWith("data:")) return true;
|
|
977
|
+
dropped += 1;
|
|
978
|
+
return false;
|
|
979
|
+
});
|
|
980
|
+
if (dropped > 0) warnings.push({
|
|
981
|
+
feature: "image-url",
|
|
982
|
+
message: `Workers AI models need image bytes; ${dropped} image URL${dropped === 1 ? " was" : "s were"} dropped.`
|
|
983
|
+
});
|
|
984
|
+
return {
|
|
985
|
+
...message,
|
|
986
|
+
content
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Nine `[A-Za-z0-9]` characters: what Mistral accepts as a tool-call id. */
|
|
990
|
+
const CONFORMANT_TOOL_CALL_ID = /^[0-9A-Za-z]{9}$/;
|
|
991
|
+
const TOOL_CALL_ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
992
|
+
/**
|
|
993
|
+
* A deterministic nine-character id for an original id: the same conversation
|
|
994
|
+
* replayed twice produces the same ids, so a provider cache or a log stays
|
|
995
|
+
* comparable. `salt` is bumped only to break a collision.
|
|
996
|
+
*/
|
|
997
|
+
function hashedToolCallId(original, salt) {
|
|
998
|
+
let state = (2166136261 ^ salt) >>> 0;
|
|
999
|
+
for (let index = 0; index < original.length; index += 1) state = Math.imul(state ^ original.charCodeAt(index), 16777619) >>> 0;
|
|
1000
|
+
let id = "";
|
|
1001
|
+
for (let index = 0; index < 9; index += 1) {
|
|
1002
|
+
state = Math.imul(state ^ state >>> 15, 16777619) >>> 0;
|
|
1003
|
+
id += TOOL_CALL_ID_ALPHABET[state % 62];
|
|
1004
|
+
}
|
|
1005
|
+
return id;
|
|
1006
|
+
}
|
|
1007
|
+
/** Every tool-call id a message set mentions, in the order it mentions them. */
|
|
1008
|
+
function toolCallIdsIn(messages) {
|
|
1009
|
+
const ids = [];
|
|
1010
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1011
|
+
const add = (value) => {
|
|
1012
|
+
if (typeof value !== "string" || seen.has(value)) return;
|
|
1013
|
+
seen.add(value);
|
|
1014
|
+
ids.push(value);
|
|
1015
|
+
};
|
|
1016
|
+
for (const message of messages) {
|
|
1017
|
+
if (!isRecord$2(message)) continue;
|
|
1018
|
+
if (Array.isArray(message.tool_calls)) {
|
|
1019
|
+
for (const call of message.tool_calls) if (isRecord$2(call)) add(call.id);
|
|
1020
|
+
}
|
|
1021
|
+
add(message.tool_call_id);
|
|
1022
|
+
}
|
|
1023
|
+
return ids;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Plans the rewrite: which ids have to change, and to what. Ids that already
|
|
1027
|
+
* conform are kept exactly as they are — including as reserved values, so a
|
|
1028
|
+
* rewritten id can never collide with one.
|
|
1029
|
+
*/
|
|
1030
|
+
function planToolCallIds(messages) {
|
|
1031
|
+
const plan = /* @__PURE__ */ new Map();
|
|
1032
|
+
const ids = toolCallIdsIn(messages);
|
|
1033
|
+
const taken = new Set(ids.filter((id) => CONFORMANT_TOOL_CALL_ID.test(id)));
|
|
1034
|
+
for (const id of ids) {
|
|
1035
|
+
if (taken.has(id)) continue;
|
|
1036
|
+
let replacement = hashedToolCallId(id, 0);
|
|
1037
|
+
for (let salt = 1; taken.has(replacement) && salt <= 64; salt += 1) replacement = hashedToolCallId(id, salt);
|
|
1038
|
+
taken.add(replacement);
|
|
1039
|
+
plan.set(id, replacement);
|
|
1040
|
+
}
|
|
1041
|
+
return plan;
|
|
1042
|
+
}
|
|
1043
|
+
/** Applies the plan to an assistant `tool_calls` turn or a `tool` result. */
|
|
1044
|
+
function rewriteToolCallIds(message, plan) {
|
|
1045
|
+
let rewritten = message;
|
|
1046
|
+
if (Array.isArray(rewritten.tool_calls)) rewritten = {
|
|
1047
|
+
...rewritten,
|
|
1048
|
+
tool_calls: rewritten.tool_calls.map((call) => {
|
|
1049
|
+
if (!isRecord$2(call) || typeof call.id !== "string") return call;
|
|
1050
|
+
const replacement = plan.get(call.id);
|
|
1051
|
+
return replacement === void 0 ? call : {
|
|
1052
|
+
...call,
|
|
1053
|
+
id: replacement
|
|
1054
|
+
};
|
|
1055
|
+
})
|
|
1056
|
+
};
|
|
1057
|
+
if (typeof rewritten.tool_call_id === "string") {
|
|
1058
|
+
const replacement = plan.get(rewritten.tool_call_id);
|
|
1059
|
+
if (replacement !== void 0) rewritten = {
|
|
1060
|
+
...rewritten,
|
|
1061
|
+
tool_call_id: replacement
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
return rewritten;
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Applies the model's quirks to a strict OpenAI chat-completions body.
|
|
1068
|
+
*
|
|
1069
|
+
* Conventions the strict body follows, which this function translates:
|
|
1070
|
+
* - `max_tokens` is the cap; it becomes `max_completion_tokens` where needed.
|
|
1071
|
+
* - `reasoning_effort: null` means "turn reasoning off".
|
|
1072
|
+
* - Replayed assistant reasoning sits in `reasoning_content`.
|
|
1073
|
+
* - `response_format.json_schema` is the OpenAI `{ name, schema }` envelope.
|
|
1074
|
+
* - `chat_template_kwargs` may be present; it is only meaningful on Workers AI.
|
|
1075
|
+
*/
|
|
1076
|
+
function prepareChatCompletionsRequest(input, modelId) {
|
|
1077
|
+
const quirks = quirksFor(modelId);
|
|
1078
|
+
const warnings = [];
|
|
1079
|
+
const body = { ...input };
|
|
1080
|
+
if (quirks.maxTokensField === "max_completion_tokens" && "max_tokens" in body) {
|
|
1081
|
+
body.max_completion_tokens = body.max_tokens;
|
|
1082
|
+
delete body.max_tokens;
|
|
1083
|
+
}
|
|
1084
|
+
if ("reasoning_effort" in body) {
|
|
1085
|
+
const effort = body.reasoning_effort;
|
|
1086
|
+
if (effort === null) {
|
|
1087
|
+
delete body.reasoning_effort;
|
|
1088
|
+
if (quirks.reasoningOff === "chat-template") body.chat_template_kwargs = {
|
|
1089
|
+
...isRecord$2(body.chat_template_kwargs) ? body.chat_template_kwargs : {},
|
|
1090
|
+
enable_thinking: false
|
|
1091
|
+
};
|
|
1092
|
+
else warnings.push({
|
|
1093
|
+
feature: "reasoning-off",
|
|
1094
|
+
message: `Reasoning cannot be turned off on ${modelId}; the request was sent with the model's default effort.`
|
|
1095
|
+
});
|
|
1096
|
+
} else if (typeof effort === "string" && quirks.reasoningEffortLevels === "basic") body.reasoning_effort = clampEffort(effort);
|
|
1097
|
+
}
|
|
1098
|
+
if ("chat_template_kwargs" in body && quirks.chatTemplateKwargs === "unsupported") {
|
|
1099
|
+
delete body.chat_template_kwargs;
|
|
1100
|
+
warnings.push({
|
|
1101
|
+
feature: "chat-template-kwargs",
|
|
1102
|
+
message: `${modelId} does not take chat_template_kwargs; they were dropped.`
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
if (isRecord$2(body.response_format)) {
|
|
1106
|
+
if (quirks.jsonSchema === "unsupported") {
|
|
1107
|
+
delete body.response_format;
|
|
1108
|
+
warnings.push({
|
|
1109
|
+
feature: "response-format",
|
|
1110
|
+
message: `${modelId} does not support response_format; the request was sent without it.`
|
|
1111
|
+
});
|
|
1112
|
+
} else if (body.response_format.type === "json_schema" && isRecord$2(body.response_format.json_schema)) {
|
|
1113
|
+
const { strict: _strict, ...envelope } = body.response_format.json_schema;
|
|
1114
|
+
body.response_format = {
|
|
1115
|
+
...body.response_format,
|
|
1116
|
+
json_schema: quirks.jsonSchemaStrict ? {
|
|
1117
|
+
...envelope,
|
|
1118
|
+
strict: true
|
|
1119
|
+
} : envelope
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if (body.tool_choice === "required" && quirks.toolChoiceRequired === "named-fallback") {
|
|
1124
|
+
const tools = Array.isArray(body.tools) ? body.tools : [];
|
|
1125
|
+
const only = tools.length === 1 && isRecord$2(tools[0]) ? tools[0] : void 0;
|
|
1126
|
+
const name = only !== void 0 && isRecord$2(only.function) && typeof only.function.name === "string" ? only.function.name : void 0;
|
|
1127
|
+
if (name !== void 0) body.tool_choice = {
|
|
1128
|
+
function: { name },
|
|
1129
|
+
type: "function"
|
|
1130
|
+
};
|
|
1131
|
+
else {
|
|
1132
|
+
delete body.tool_choice;
|
|
1133
|
+
warnings.push({
|
|
1134
|
+
feature: "tool-choice-required",
|
|
1135
|
+
message: `${modelId} rejects tool_choice "required"; the model was left free to choose.`
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (Array.isArray(body.messages)) {
|
|
1140
|
+
const toolCallIds = quirks.toolCallIds === "alnum-9" ? planToolCallIds(body.messages) : /* @__PURE__ */ new Map();
|
|
1141
|
+
if (toolCallIds.size > 0) warnings.push({
|
|
1142
|
+
feature: "tool-call-id",
|
|
1143
|
+
message: `${modelId} needs 9-character alphanumeric tool-call ids; ${toolCallIds.size} id${toolCallIds.size === 1 ? " was" : "s were"} rewritten.`
|
|
1144
|
+
});
|
|
1145
|
+
const messages = [];
|
|
1146
|
+
let previousRole;
|
|
1147
|
+
for (const entry of body.messages) {
|
|
1148
|
+
if (!isRecord$2(entry)) continue;
|
|
1149
|
+
let message = entry;
|
|
1150
|
+
if (toolCallIds.size > 0) message = rewriteToolCallIds(message, toolCallIds);
|
|
1151
|
+
if (message.role === "user" && quirks.imageUrls === "data-only") message = stripImageUrls(message, warnings);
|
|
1152
|
+
if (message.role === "assistant" && message.content === null && quirks.nullAssistantContent === "empty-string") message = {
|
|
1153
|
+
...message,
|
|
1154
|
+
content: ""
|
|
1155
|
+
};
|
|
1156
|
+
if (message.role === "assistant" && "reasoning_content" in message) {
|
|
1157
|
+
if (quirks.reasoningReplay === "drop") {
|
|
1158
|
+
const { reasoning_content: _reasoning, ...rest } = message;
|
|
1159
|
+
message = rest;
|
|
1160
|
+
warnings.push({
|
|
1161
|
+
feature: "assistant-reasoning",
|
|
1162
|
+
message: `${modelId} has no field for replayed reasoning; the assistant reasoning was dropped.`
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (quirks.requiresAssistantAfterToolResult && previousRole === "tool" && message.role === "user") messages.push({
|
|
1167
|
+
content: "",
|
|
1168
|
+
role: "assistant"
|
|
1169
|
+
});
|
|
1170
|
+
messages.push(message);
|
|
1171
|
+
previousRole = message.role;
|
|
1172
|
+
}
|
|
1173
|
+
body.messages = messages;
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
body,
|
|
1177
|
+
reasoningOff: isRecord$2(body.chat_template_kwargs) && body.chat_template_kwargs.enable_thinking === false,
|
|
1178
|
+
warnings
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/models/core/chat-completions/response.ts
|
|
1183
|
+
/**
|
|
1184
|
+
* Normalizes a chat-completions response body from the run path into the
|
|
1185
|
+
* strict OpenAI `chat.completion` shape, so framework parsers never see
|
|
1186
|
+
* Workers AI's native envelope or its field spellings.
|
|
1187
|
+
*/
|
|
1188
|
+
function isRecord$1(value) {
|
|
1189
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1190
|
+
}
|
|
1191
|
+
/** Native `{ name, arguments: object }` → OpenAI `{ id, type, function }`. */
|
|
1192
|
+
function normalizeToolCall(entry, index) {
|
|
1193
|
+
if (!isRecord$1(entry)) return void 0;
|
|
1194
|
+
const fn = isRecord$1(entry.function) ? entry.function : void 0;
|
|
1195
|
+
const name = typeof fn?.name === "string" ? fn.name : typeof entry.name === "string" ? entry.name : void 0;
|
|
1196
|
+
if (name === void 0) return void 0;
|
|
1197
|
+
const rawArguments = fn !== void 0 && "arguments" in fn ? fn.arguments : entry.arguments;
|
|
1198
|
+
const args = typeof rawArguments === "string" ? rawArguments : rawArguments === void 0 || rawArguments === null ? "{}" : JSON.stringify(rawArguments);
|
|
1199
|
+
return {
|
|
1200
|
+
...typeof entry.index === "number" ? { index: entry.index } : { index },
|
|
1201
|
+
...typeof entry.id === "string" ? { id: entry.id } : {},
|
|
1202
|
+
function: {
|
|
1203
|
+
arguments: args,
|
|
1204
|
+
name
|
|
1205
|
+
},
|
|
1206
|
+
type: "function",
|
|
1207
|
+
...entry.extra_content !== void 0 ? { extra_content: entry.extra_content } : {}
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/** Text as the model produced it; structured-output models may return an object. */
|
|
1211
|
+
function contentText(value) {
|
|
1212
|
+
if (typeof value === "string") return value;
|
|
1213
|
+
if (value === null || value === void 0) return null;
|
|
1214
|
+
return JSON.stringify(value);
|
|
1215
|
+
}
|
|
1216
|
+
function stripEchoSuffix$1(model, modelId) {
|
|
1217
|
+
const suffix = quirksFor(modelId).modelEchoSuffix;
|
|
1218
|
+
if (typeof model !== "string" || suffix === void 0) return model;
|
|
1219
|
+
return model.endsWith(suffix) ? model.slice(0, -suffix.length) : model;
|
|
1220
|
+
}
|
|
1221
|
+
/** Puts reasoning under `reasoning_content` whichever spelling the model used. */
|
|
1222
|
+
function normalizeMessage(message) {
|
|
1223
|
+
const out = { ...message };
|
|
1224
|
+
const reasoning = typeof out.reasoning_content === "string" && out.reasoning_content !== "" ? out.reasoning_content : typeof out.reasoning === "string" && out.reasoning !== "" ? out.reasoning : void 0;
|
|
1225
|
+
delete out.reasoning;
|
|
1226
|
+
if (reasoning !== void 0) out.reasoning_content = reasoning;
|
|
1227
|
+
else delete out.reasoning_content;
|
|
1228
|
+
if ("content" in out) out.content = contentText(out.content);
|
|
1229
|
+
if (Array.isArray(out.tool_calls)) {
|
|
1230
|
+
const calls = out.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0);
|
|
1231
|
+
if (calls.length > 0) out.tool_calls = calls;
|
|
1232
|
+
else delete out.tool_calls;
|
|
1233
|
+
}
|
|
1234
|
+
return out;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Returns the body in the strict `chat.completion` shape. Already-OpenAI
|
|
1238
|
+
* bodies come back with their reasoning spelling unified and the echoed model
|
|
1239
|
+
* id cleaned; native Workers AI `{ response, tool_calls, usage }` bodies are
|
|
1240
|
+
* lifted into `choices[0]`.
|
|
1241
|
+
*/
|
|
1242
|
+
function normalizeChatCompletion(body, modelId) {
|
|
1243
|
+
if (!isRecord$1(body)) return { choices: [] };
|
|
1244
|
+
if (Array.isArray(body.choices)) {
|
|
1245
|
+
const choices = body.choices.map((choice) => {
|
|
1246
|
+
if (!isRecord$1(choice)) return choice;
|
|
1247
|
+
const message = isRecord$1(choice.message) ? normalizeMessage(choice.message) : choice.message;
|
|
1248
|
+
const hasToolCalls = isRecord$1(message) && Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
1249
|
+
return {
|
|
1250
|
+
...choice,
|
|
1251
|
+
message,
|
|
1252
|
+
finish_reason: choice.finish_reason ?? (hasToolCalls ? "tool_calls" : "stop")
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
return {
|
|
1256
|
+
...body,
|
|
1257
|
+
choices,
|
|
1258
|
+
model: stripEchoSuffix$1(body.model, modelId)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
if ("response" in body || "tool_calls" in body) {
|
|
1262
|
+
const toolCalls = Array.isArray(body.tool_calls) ? body.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0) : [];
|
|
1263
|
+
const { response, tool_calls: _toolCalls, usage, ...rest } = body;
|
|
1264
|
+
return {
|
|
1265
|
+
...rest,
|
|
1266
|
+
choices: [{
|
|
1267
|
+
finish_reason: typeof body.finish_reason === "string" ? body.finish_reason : toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
1268
|
+
index: 0,
|
|
1269
|
+
message: {
|
|
1270
|
+
content: contentText(response),
|
|
1271
|
+
role: "assistant",
|
|
1272
|
+
...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
|
|
1273
|
+
}
|
|
1274
|
+
}],
|
|
1275
|
+
model: stripEchoSuffix$1(body.model, modelId),
|
|
1276
|
+
...usage !== void 0 ? { usage } : {}
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
return body;
|
|
1280
|
+
}
|
|
1281
|
+
//#endregion
|
|
1282
|
+
//#region src/models/core/chat-completions/stream.ts
|
|
1283
|
+
/**
|
|
1284
|
+
* Normalizes a chat-completions SSE stream from the run path into strict
|
|
1285
|
+
* OpenAI `chat.completion.chunk` events.
|
|
1286
|
+
*
|
|
1287
|
+
* Every Workers AI model streams the same three-part hybrid (live captures,
|
|
1288
|
+
* all families): OpenAI-shaped delta chunks that each carry a per-delta
|
|
1289
|
+
* `usage`, then a `choices: []` heartbeat, then a native
|
|
1290
|
+
* `{ response: "", usage }` tail with the cumulative usage, then `[DONE]`.
|
|
1291
|
+
* Some models also emit native `{ response: "<text>" }` deltas or top-level
|
|
1292
|
+
* `tool_calls`. After this transform a consumer sees only:
|
|
1293
|
+
*
|
|
1294
|
+
* - delta chunks without per-delta usage and with reasoning under
|
|
1295
|
+
* `reasoning_content`;
|
|
1296
|
+
* - at most one final `choices: []` chunk carrying the cumulative usage
|
|
1297
|
+
* (the OpenAI `stream_options.include_usage` shape);
|
|
1298
|
+
* - a `finish_reason` on some chunk before the end, synthesized when the
|
|
1299
|
+
* model finished cleanly without sending one;
|
|
1300
|
+
* - `[DONE]`.
|
|
1301
|
+
*
|
|
1302
|
+
* Input and output are SSE `data` payloads (strings), so it slots between the
|
|
1303
|
+
* SSE decoder and either framework's OpenAI chunk parser.
|
|
1304
|
+
*/
|
|
1305
|
+
function isRecord(value) {
|
|
1306
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1307
|
+
}
|
|
1308
|
+
const NOISE_FIELDS = [
|
|
1309
|
+
"p",
|
|
1310
|
+
"token_ids",
|
|
1311
|
+
"prompt_token_ids",
|
|
1312
|
+
"kv_transfer_params"
|
|
1313
|
+
];
|
|
1314
|
+
/** Fields the run path adds to every chunk that mean nothing to a consumer. */
|
|
1315
|
+
function stripNoise(chunk) {
|
|
1316
|
+
const out = { ...chunk };
|
|
1317
|
+
for (const key of NOISE_FIELDS) delete out[key];
|
|
1318
|
+
return out;
|
|
1319
|
+
}
|
|
1320
|
+
function stripEchoSuffix(chunk, modelId) {
|
|
1321
|
+
const suffix = quirksFor(modelId).modelEchoSuffix;
|
|
1322
|
+
if (suffix === void 0 || typeof chunk.model !== "string") return chunk;
|
|
1323
|
+
return chunk.model.endsWith(suffix) ? {
|
|
1324
|
+
...chunk,
|
|
1325
|
+
model: chunk.model.slice(0, -suffix.length)
|
|
1326
|
+
} : chunk;
|
|
1327
|
+
}
|
|
1328
|
+
function normalizeDelta(delta, relabelReasoning) {
|
|
1329
|
+
const out = { ...delta };
|
|
1330
|
+
const reasoning = typeof out.reasoning_content === "string" && out.reasoning_content !== "" ? out.reasoning_content : typeof out.reasoning === "string" && out.reasoning !== "" ? out.reasoning : void 0;
|
|
1331
|
+
delete out.reasoning;
|
|
1332
|
+
delete out.reasoning_content;
|
|
1333
|
+
if (reasoning !== void 0) if (relabelReasoning) out.content = (typeof out.content === "string" ? out.content : "") + reasoning;
|
|
1334
|
+
else out.reasoning_content = reasoning;
|
|
1335
|
+
if (Array.isArray(out.tool_calls)) out.tool_calls = out.tool_calls.map((call, index) => {
|
|
1336
|
+
if (!isRecord(call)) return call;
|
|
1337
|
+
const fn = isRecord(call.function) ? call.function : void 0;
|
|
1338
|
+
const args = fn !== void 0 ? fn.arguments : call.arguments;
|
|
1339
|
+
return fn === void 0 && typeof call.name === "string" || isRecord(args) ? normalizeToolCall(call, index) ?? call : call;
|
|
1340
|
+
});
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* The transform. Backpressure is preserved: nothing is buffered except the
|
|
1345
|
+
* single pending usage chunk, which must wait for the tail.
|
|
1346
|
+
*/
|
|
1347
|
+
function normalizeChatCompletionsStream(modelId, options = {}) {
|
|
1348
|
+
const quirks = quirksFor(modelId);
|
|
1349
|
+
const relabelReasoning = options.reasoningOff === true && quirks.reasoningOffStreamLabel === "reasoning";
|
|
1350
|
+
let id;
|
|
1351
|
+
let model;
|
|
1352
|
+
let created;
|
|
1353
|
+
let sawFinishReason = false;
|
|
1354
|
+
let sawContent = false;
|
|
1355
|
+
let sawToolCalls = false;
|
|
1356
|
+
let sawDone = false;
|
|
1357
|
+
let finalUsage;
|
|
1358
|
+
let finalUsageTemplate;
|
|
1359
|
+
const perDeltaUsage = isWorkersAI(modelId);
|
|
1360
|
+
const envelope = (extra) => ({
|
|
1361
|
+
...id !== void 0 ? { id } : {},
|
|
1362
|
+
object: "chat.completion.chunk",
|
|
1363
|
+
...created !== void 0 ? { created } : {},
|
|
1364
|
+
...model !== void 0 ? { model } : {},
|
|
1365
|
+
...extra
|
|
1366
|
+
});
|
|
1367
|
+
const emit = (controller, chunk) => {
|
|
1368
|
+
controller.enqueue(JSON.stringify(chunk));
|
|
1369
|
+
};
|
|
1370
|
+
const emitTail = (controller) => {
|
|
1371
|
+
if (!sawFinishReason && sawDone && (sawContent || sawToolCalls)) {
|
|
1372
|
+
emit(controller, envelope({ choices: [{
|
|
1373
|
+
delta: {},
|
|
1374
|
+
finish_reason: sawToolCalls ? "tool_calls" : "stop",
|
|
1375
|
+
index: 0
|
|
1376
|
+
}] }));
|
|
1377
|
+
sawFinishReason = true;
|
|
1378
|
+
}
|
|
1379
|
+
if (finalUsage !== void 0) {
|
|
1380
|
+
emit(controller, {
|
|
1381
|
+
...finalUsageTemplate ?? envelope({}),
|
|
1382
|
+
choices: [],
|
|
1383
|
+
usage: finalUsage
|
|
1384
|
+
});
|
|
1385
|
+
finalUsage = void 0;
|
|
1386
|
+
}
|
|
1387
|
+
if (sawDone) controller.enqueue("[DONE]");
|
|
1388
|
+
};
|
|
1389
|
+
return new TransformStream({
|
|
1390
|
+
flush(controller) {
|
|
1391
|
+
emitTail(controller);
|
|
1392
|
+
},
|
|
1393
|
+
transform(data, controller) {
|
|
1394
|
+
if (data === "") return;
|
|
1395
|
+
if (data === "[DONE]") {
|
|
1396
|
+
sawDone = true;
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
let parsed;
|
|
1400
|
+
try {
|
|
1401
|
+
parsed = JSON.parse(data);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (!isRecord(parsed)) return;
|
|
1406
|
+
const chunk = stripEchoSuffix(stripNoise(parsed), modelId);
|
|
1407
|
+
if (typeof chunk.id === "string") id ??= chunk.id;
|
|
1408
|
+
if (typeof chunk.model === "string") model ??= chunk.model;
|
|
1409
|
+
if (typeof chunk.created === "number") created ??= chunk.created;
|
|
1410
|
+
if (isRecord(chunk.error)) {
|
|
1411
|
+
sawFinishReason = true;
|
|
1412
|
+
controller.enqueue(JSON.stringify(chunk));
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
if (Array.isArray(chunk.choices)) {
|
|
1416
|
+
if (chunk.choices.length === 0) {
|
|
1417
|
+
if (isRecord(chunk.usage)) {
|
|
1418
|
+
finalUsage = chunk.usage;
|
|
1419
|
+
finalUsageTemplate = {
|
|
1420
|
+
...chunk,
|
|
1421
|
+
usage: void 0
|
|
1422
|
+
};
|
|
1423
|
+
delete finalUsageTemplate.usage;
|
|
1424
|
+
}
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
const choices = chunk.choices.map((choice) => {
|
|
1428
|
+
if (!isRecord(choice)) return choice;
|
|
1429
|
+
if (typeof choice.finish_reason === "string") sawFinishReason = true;
|
|
1430
|
+
const delta = isRecord(choice.delta) ? normalizeDelta(choice.delta, relabelReasoning) : choice.delta;
|
|
1431
|
+
if (isRecord(delta)) {
|
|
1432
|
+
if (typeof delta.content === "string" && delta.content !== "") sawContent = true;
|
|
1433
|
+
if (typeof delta.reasoning_content === "string") sawContent = true;
|
|
1434
|
+
if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) sawToolCalls = true;
|
|
1435
|
+
}
|
|
1436
|
+
if (!perDeltaUsage) return {
|
|
1437
|
+
...choice,
|
|
1438
|
+
delta
|
|
1439
|
+
};
|
|
1440
|
+
const { usage: _perDeltaUsage, ...rest } = choice;
|
|
1441
|
+
return {
|
|
1442
|
+
...rest,
|
|
1443
|
+
delta
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
if (perDeltaUsage) {
|
|
1447
|
+
const { usage: _usage, ...rest } = chunk;
|
|
1448
|
+
emit(controller, {
|
|
1449
|
+
...rest,
|
|
1450
|
+
choices
|
|
1451
|
+
});
|
|
1452
|
+
} else emit(controller, {
|
|
1453
|
+
...chunk,
|
|
1454
|
+
choices
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if ("response" in chunk || "tool_calls" in chunk) {
|
|
1459
|
+
const text = chunk.response;
|
|
1460
|
+
const hasText = typeof text === "string" && text !== "";
|
|
1461
|
+
const calls = Array.isArray(chunk.tool_calls) ? chunk.tool_calls.map((call, index) => normalizeToolCall(call, index)).filter((call) => call !== void 0) : [];
|
|
1462
|
+
if (hasText || calls.length > 0) {
|
|
1463
|
+
if (hasText) sawContent = true;
|
|
1464
|
+
if (calls.length > 0) sawToolCalls = true;
|
|
1465
|
+
emit(controller, envelope({ choices: [{
|
|
1466
|
+
delta: {
|
|
1467
|
+
...hasText ? { content: text } : {},
|
|
1468
|
+
...calls.length > 0 ? { tool_calls: calls } : {}
|
|
1469
|
+
},
|
|
1470
|
+
finish_reason: null,
|
|
1471
|
+
index: 0
|
|
1472
|
+
}] }));
|
|
1473
|
+
}
|
|
1474
|
+
if (isRecord(chunk.usage)) {
|
|
1475
|
+
finalUsage = chunk.usage;
|
|
1476
|
+
finalUsageTemplate = void 0;
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof chunk.finish_reason === "string") {
|
|
1479
|
+
sawFinishReason = true;
|
|
1480
|
+
emit(controller, envelope({ choices: [{
|
|
1481
|
+
delta: {},
|
|
1482
|
+
finish_reason: chunk.finish_reason,
|
|
1483
|
+
index: 0
|
|
1484
|
+
}] }));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
//#endregion
|
|
1491
|
+
//#region src/models/core/sse.ts
|
|
1492
|
+
/**
|
|
1493
|
+
* Decodes a `text/event-stream` byte stream into the `data` payload of each
|
|
1494
|
+
* event, as a `TransformStream` so the whole pipeline keeps backpressure and
|
|
1495
|
+
* nothing is ever buffered beyond the current event.
|
|
1496
|
+
*
|
|
1497
|
+
* Handles all three line terminators the SSE spec allows (LF, CRLF and a bare
|
|
1498
|
+
* CR), multi-line `data:` fields (joined with `\n`, per the spec), comment
|
|
1499
|
+
* lines (`:` prefixed), and non-`data` fields (`event:`, `id:`, `retry:`),
|
|
1500
|
+
* which are ignored. A trailing event that the server never terminated with a
|
|
1501
|
+
* blank line is still flushed.
|
|
1502
|
+
*
|
|
1503
|
+
* @experimental This surface is experimental and may change.
|
|
1504
|
+
*/
|
|
1505
|
+
function sseDataStream() {
|
|
1506
|
+
const decoder = new TextDecoder();
|
|
1507
|
+
let buffer = "";
|
|
1508
|
+
let data = [];
|
|
1509
|
+
const flushEvent = (controller) => {
|
|
1510
|
+
if (data.length === 0) return;
|
|
1511
|
+
controller.enqueue(data.join("\n"));
|
|
1512
|
+
data = [];
|
|
1513
|
+
};
|
|
1514
|
+
const consumeLine = (line, controller) => {
|
|
1515
|
+
if (line === "") {
|
|
1516
|
+
flushEvent(controller);
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (line.startsWith(":")) return;
|
|
1520
|
+
const colon = line.indexOf(":");
|
|
1521
|
+
if ((colon === -1 ? line : line.slice(0, colon)) !== "data") return;
|
|
1522
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
1523
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1524
|
+
data.push(value);
|
|
1525
|
+
};
|
|
1526
|
+
/**
|
|
1527
|
+
* Consumes whole lines out of the buffer. A buffer ending in a lone `\r` is
|
|
1528
|
+
* held back while more bytes may arrive, because the next byte decides
|
|
1529
|
+
* whether that `\r` was a CRLF pair or a bare-CR terminator.
|
|
1530
|
+
*/
|
|
1531
|
+
const drain = (controller, final) => {
|
|
1532
|
+
for (;;) {
|
|
1533
|
+
const match = /\r\n|[\r\n]/.exec(buffer);
|
|
1534
|
+
if (match === null) break;
|
|
1535
|
+
const end = match.index + match[0].length;
|
|
1536
|
+
if (!final && match[0] === "\r" && end === buffer.length) break;
|
|
1537
|
+
consumeLine(buffer.slice(0, match.index), controller);
|
|
1538
|
+
buffer = buffer.slice(end);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
return new TransformStream({
|
|
1542
|
+
flush(controller) {
|
|
1543
|
+
buffer += decoder.decode();
|
|
1544
|
+
drain(controller, true);
|
|
1545
|
+
if (buffer !== "") {
|
|
1546
|
+
consumeLine(buffer, controller);
|
|
1547
|
+
buffer = "";
|
|
1548
|
+
}
|
|
1549
|
+
flushEvent(controller);
|
|
1550
|
+
},
|
|
1551
|
+
transform(chunk, controller) {
|
|
1552
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1553
|
+
drain(controller, false);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
//#endregion
|
|
1558
|
+
//#region src/models/core/images.ts
|
|
1559
|
+
/**
|
|
1560
|
+
* `flux-1-schnell` takes `{ prompt, steps }` and nothing else — no size, no
|
|
1561
|
+
* seed, no guidance. Sending those anyway would be silently ignored, so they
|
|
1562
|
+
* come back as warnings instead.
|
|
1563
|
+
*
|
|
1564
|
+
* @see https://developers.cloudflare.com/workers-ai/models/flux-1-schnell/
|
|
1565
|
+
*/
|
|
1566
|
+
const PROMPT_AND_STEPS_ONLY = /^@cf\/black-forest-labs\/flux-1/;
|
|
1567
|
+
/**
|
|
1568
|
+
* Builds the run-path body for a text-to-image model: `prompt`, `width`,
|
|
1569
|
+
* `height`, `num_steps`, `guidance`, `strength`, `negative_prompt` and `seed`,
|
|
1570
|
+
* or the flux-1 two-key subset with a warning for everything dropped.
|
|
1571
|
+
*
|
|
1572
|
+
* @experimental This surface is experimental and may change.
|
|
1573
|
+
*/
|
|
1574
|
+
function buildImageInput(modelId, prompt, settings) {
|
|
1575
|
+
const warnings = [];
|
|
1576
|
+
const input = { prompt };
|
|
1577
|
+
if (PROMPT_AND_STEPS_ONLY.test(modelId)) {
|
|
1578
|
+
if (settings.steps !== void 0) input.steps = settings.steps;
|
|
1579
|
+
for (const [feature, value] of [
|
|
1580
|
+
["size", settings.width ?? settings.height],
|
|
1581
|
+
["seed", settings.seed],
|
|
1582
|
+
["guidance", settings.guidance],
|
|
1583
|
+
["negativePrompt", settings.negativePrompt],
|
|
1584
|
+
["strength", settings.strength],
|
|
1585
|
+
["files", settings.sourceImage === true ? true : void 0],
|
|
1586
|
+
["mask", settings.mask === true ? true : void 0]
|
|
1587
|
+
]) if (value !== void 0) warnings.push({
|
|
1588
|
+
feature,
|
|
1589
|
+
message: `${modelId} takes only a prompt and steps.`
|
|
1590
|
+
});
|
|
1591
|
+
return {
|
|
1592
|
+
input,
|
|
1593
|
+
minimal: true,
|
|
1594
|
+
warnings
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if (settings.width !== void 0) input.width = settings.width;
|
|
1598
|
+
if (settings.height !== void 0) input.height = settings.height;
|
|
1599
|
+
if (settings.seed !== void 0) input.seed = settings.seed;
|
|
1600
|
+
if (settings.steps !== void 0) input.num_steps = settings.steps;
|
|
1601
|
+
if (settings.guidance !== void 0) input.guidance = settings.guidance;
|
|
1602
|
+
if (settings.strength !== void 0) input.strength = settings.strength;
|
|
1603
|
+
if (settings.negativePrompt !== void 0) input.negative_prompt = settings.negativePrompt;
|
|
1604
|
+
return {
|
|
1605
|
+
input,
|
|
1606
|
+
minimal: false,
|
|
1607
|
+
warnings
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
const JPEG_PREFIX = "/9j/";
|
|
1611
|
+
const PNG_PREFIX = "iVBORw0KGgo";
|
|
1612
|
+
const WEBP_PREFIX = "UklGR";
|
|
1613
|
+
const GIF_PREFIX = "R0lGOD";
|
|
1614
|
+
/**
|
|
1615
|
+
* Sniffs an image media type from the head of its base64. The bytes decide,
|
|
1616
|
+
* never the model name: `flux-1-schnell` answers JPEG where every other model
|
|
1617
|
+
* in this catalog answers PNG.
|
|
1618
|
+
*
|
|
1619
|
+
* @experimental This surface is experimental and may change.
|
|
1620
|
+
*/
|
|
1621
|
+
function imageMediaTypeOf(base64) {
|
|
1622
|
+
if (base64.startsWith(JPEG_PREFIX)) return "image/jpeg";
|
|
1623
|
+
if (base64.startsWith(PNG_PREFIX)) return "image/png";
|
|
1624
|
+
if (base64.startsWith(WEBP_PREFIX)) return "image/webp";
|
|
1625
|
+
if (base64.startsWith(GIF_PREFIX)) return "image/gif";
|
|
1626
|
+
return "image/png";
|
|
1627
|
+
}
|
|
1628
|
+
//#endregion
|
|
1629
|
+
export { resolveOptions as C, isRetryableStatus as D, isAbortError as E, parseModelOptions as S, classifyStatus as T, isGatewayErrorEnvelope as _, normalizeChatCompletion as a, gatewayLayerOf as b, gatewaySlugForBaseUrl as c, resolveGatewayProvider as d, createTransport as f, headersToObject as g, errorFromResponse as h, normalizeChatCompletionsStream as i, isGatewayShapedUrl as l, errorFromGatewayEnvelope as m, imageMediaTypeOf as n, prepareChatCompletionsRequest as o, definedHeaders as p, sseDataStream as r, GATEWAY_PROVIDER_NAMES as s, buildImageInput as t, requireGatewayProvider as u, unwrapEnvelope as v, CloudflareAIError as w, mergeModelOptions as x, PROVIDER_OPTIONS_KEY as y };
|
|
1630
|
+
|
|
1631
|
+
//# sourceMappingURL=images-iiTDSyRu.js.map
|