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,1795 @@
|
|
|
1
|
+
import { C as resolveOptions, E as isAbortError, _ as isGatewayErrorEnvelope, a as normalizeChatCompletion, b as gatewayLayerOf, c as gatewaySlugForBaseUrl, d as resolveGatewayProvider, f as createTransport, g as headersToObject, h as errorFromResponse, i as normalizeChatCompletionsStream, l as isGatewayShapedUrl, m as errorFromGatewayEnvelope, n as imageMediaTypeOf, o as prepareChatCompletionsRequest, r as sseDataStream, s as GATEWAY_PROVIDER_NAMES, t as buildImageInput, v as unwrapEnvelope, w as CloudflareAIError, x as mergeModelOptions } from "../../images-iiTDSyRu.js";
|
|
2
|
+
import { calculateCost, clampThinkingLevel, createAssistantMessageEventStream, createProvider, getCurrentTools, normalizeContext, parseStreamingJson } from "@earendil-works/pi-ai";
|
|
3
|
+
import { CLOUDFLARE_AI_GATEWAY_MODELS } from "@earendil-works/pi-ai/providers/cloudflare-ai-gateway.models";
|
|
4
|
+
import { CLOUDFLARE_WORKERS_AI_MODELS } from "@earendil-works/pi-ai/providers/cloudflare-workers-ai.models";
|
|
5
|
+
import { convertMessages } from "@earendil-works/pi-ai/api/openai-completions";
|
|
6
|
+
import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
7
|
+
import { adjustMaxTokensForThinking } from "@earendil-works/pi-ai/api/simple-options";
|
|
8
|
+
import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "@earendil-works/pi-ai/api/openai-responses-shared";
|
|
9
|
+
import { clampOpenAIPromptCacheKey } from "@earendil-works/pi-ai/api/openai-prompt-cache";
|
|
10
|
+
//#region src/models/pi-ai/catalog.ts
|
|
11
|
+
/**
|
|
12
|
+
* The `api` marker Workers AI models from this provider carry. It is not a
|
|
13
|
+
* wire format: Workers AI runs on `env.AI.run`, and the compat layer maps that
|
|
14
|
+
* onto strict OpenAI chat completions.
|
|
15
|
+
*
|
|
16
|
+
* @beta This surface is in beta and may change.
|
|
17
|
+
*/
|
|
18
|
+
const CLOUDFLARE_AI_API = "cloudflare-ai";
|
|
19
|
+
/**
|
|
20
|
+
* The pi-ai provider id. A `Models` registry resolves specifiers as
|
|
21
|
+
* `cloudflare/<id>`, e.g. `cloudflare/anthropic/claude-opus-4-8`.
|
|
22
|
+
*
|
|
23
|
+
* @beta This surface is in beta and may change.
|
|
24
|
+
*/
|
|
25
|
+
const CLOUDFLARE_PROVIDER_ID = "cloudflare";
|
|
26
|
+
const MODEL_OPTIONS = Symbol.for("agents.models.pi-ai.options");
|
|
27
|
+
const MODEL_ROUTING = Symbol.for("agents.models.pi-ai.routing");
|
|
28
|
+
function tag(model, key, value) {
|
|
29
|
+
Object.defineProperty(model, key, {
|
|
30
|
+
configurable: true,
|
|
31
|
+
enumerable: true,
|
|
32
|
+
value,
|
|
33
|
+
writable: false
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Reads the options a model was created with, if it came from this provider. */
|
|
37
|
+
function optionsOf(model) {
|
|
38
|
+
const options = model[MODEL_OPTIONS];
|
|
39
|
+
return options !== null && typeof options === "object" ? options : void 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The gateway registry entry a model's own id names, for a model this provider
|
|
43
|
+
* built from a `<slug>/<id>` specifier. It is looked up rather than inferred,
|
|
44
|
+
* so a model that lost its symbol tags — `JSON.stringify` and
|
|
45
|
+
* `structuredClone` drop them, and an Agent's state goes through both — still
|
|
46
|
+
* reaches the vendor under the vendor's own spelling.
|
|
47
|
+
*/
|
|
48
|
+
function registryEntryFor(model) {
|
|
49
|
+
if (model.provider !== "cloudflare" || model.id.startsWith("@cf/")) return;
|
|
50
|
+
gatewayRegistry ??= buildGatewayRegistry();
|
|
51
|
+
return gatewayRegistry.get(model.id);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The id the request body must carry. It differs from `model.id` only for a
|
|
55
|
+
* `<slug>/<id>` specifier, whose model is listed under the specifier so a
|
|
56
|
+
* `Models` registry can find it while the vendor still sees its own spelling.
|
|
57
|
+
*/
|
|
58
|
+
function wireModelId(model) {
|
|
59
|
+
const tagged = model[MODEL_ROUTING]?.modelId;
|
|
60
|
+
if (tagged !== void 0) return tagged;
|
|
61
|
+
return registryEntryFor(model)?.id ?? model.id;
|
|
62
|
+
}
|
|
63
|
+
/** The `<slug>/<id>` specifier a model was resolved from, for diagnostics. */
|
|
64
|
+
function specifierOf(model) {
|
|
65
|
+
const tagged = model[MODEL_ROUTING]?.specifier;
|
|
66
|
+
if (tagged !== void 0) return tagged;
|
|
67
|
+
return registryEntryFor(model) === void 0 ? void 0 : model.id;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Copies a caller's model and tags the copy with the resolved options. The
|
|
71
|
+
* caller's object is never mutated, and every field it declares — `api`,
|
|
72
|
+
* `provider`, `id`, `baseUrl`, `compat`, `thinkingLevelMap`, cost and limits —
|
|
73
|
+
* is kept verbatim, because that metadata is the model's own author's, not
|
|
74
|
+
* ours.
|
|
75
|
+
*/
|
|
76
|
+
function withOptions(model, options) {
|
|
77
|
+
const copy = { ...model };
|
|
78
|
+
if (options !== void 0) tag(copy, MODEL_OPTIONS, options);
|
|
79
|
+
return copy;
|
|
80
|
+
}
|
|
81
|
+
const DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
82
|
+
const DEFAULT_MAX_TOKENS = 8192;
|
|
83
|
+
const ZERO_COST = {
|
|
84
|
+
cacheRead: 0,
|
|
85
|
+
cacheWrite: 0,
|
|
86
|
+
input: 0,
|
|
87
|
+
output: 0
|
|
88
|
+
};
|
|
89
|
+
let workersAI;
|
|
90
|
+
function workersAIEntry(modelId) {
|
|
91
|
+
workersAI ??= new Map(Object.values(CLOUDFLARE_WORKERS_AI_MODELS).map((entry) => [entry.id, entry]));
|
|
92
|
+
return workersAI.get(modelId);
|
|
93
|
+
}
|
|
94
|
+
/** Every Workers AI id pi-ai knows, for the provider's static model list. */
|
|
95
|
+
function workersAIModelIds() {
|
|
96
|
+
workersAI ??= new Map(Object.values(CLOUDFLARE_WORKERS_AI_MODELS).map((entry) => [entry.id, entry]));
|
|
97
|
+
return [...workersAI.keys()].sort();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Builds the pi-ai model for a Workers AI id. Registry metadata is used when
|
|
101
|
+
* pi-ai knows the id; explicit options override it; everything else falls back
|
|
102
|
+
* to defaults that keep the model usable, because the Workers AI catalog moves
|
|
103
|
+
* faster than any generated registry.
|
|
104
|
+
*/
|
|
105
|
+
function buildWorkersAIModel(modelId, options) {
|
|
106
|
+
const known = workersAIEntry(modelId);
|
|
107
|
+
const model = {
|
|
108
|
+
api: CLOUDFLARE_AI_API,
|
|
109
|
+
baseUrl: "",
|
|
110
|
+
contextWindow: options?.contextWindow ?? known?.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
|
|
111
|
+
cost: options?.cost ?? known?.cost ?? ZERO_COST,
|
|
112
|
+
id: modelId,
|
|
113
|
+
input: options?.input ?? known?.input ?? ["text"],
|
|
114
|
+
maxTokens: options?.maxTokens ?? known?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
115
|
+
name: options?.name ?? known?.name ?? modelId,
|
|
116
|
+
provider: CLOUDFLARE_PROVIDER_ID,
|
|
117
|
+
reasoning: options?.reasoning ?? known?.reasoning ?? true,
|
|
118
|
+
...known?.thinkingLevelMap !== void 0 ? { thinkingLevelMap: known.thinkingLevelMap } : {}
|
|
119
|
+
};
|
|
120
|
+
if (options !== void 0) tag(model, MODEL_OPTIONS, options);
|
|
121
|
+
return model;
|
|
122
|
+
}
|
|
123
|
+
/** The gateway provider slug a registry entry's base URL names. */
|
|
124
|
+
function slugOf(entry) {
|
|
125
|
+
return entry.baseUrl.split("/").filter((part) => part !== "").at(-1);
|
|
126
|
+
}
|
|
127
|
+
let gatewayRegistry;
|
|
128
|
+
/**
|
|
129
|
+
* pi-ai's generated Cloudflare AI Gateway registry, keyed by `<slug>/<id>`.
|
|
130
|
+
* `/compat` entries are skipped: they alias `@cf/` ids the Workers AI registry
|
|
131
|
+
* already declares, and those belong on the run path.
|
|
132
|
+
*/
|
|
133
|
+
function buildGatewayRegistry() {
|
|
134
|
+
const map = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const entry of Object.values(CLOUDFLARE_AI_GATEWAY_MODELS)) {
|
|
136
|
+
const slug = slugOf(entry);
|
|
137
|
+
if (slug === void 0 || slug === "compat") continue;
|
|
138
|
+
map.set(`${slug}/${entry.id}`, entry);
|
|
139
|
+
}
|
|
140
|
+
return map;
|
|
141
|
+
}
|
|
142
|
+
/** Every `<slug>/<id>` specifier pi-ai's gateway registry declares. */
|
|
143
|
+
function gatewayModelIds() {
|
|
144
|
+
gatewayRegistry ??= buildGatewayRegistry();
|
|
145
|
+
return [...gatewayRegistry.keys()].sort();
|
|
146
|
+
}
|
|
147
|
+
/** An id with case and separators (`.`, `-`, `_`) ignored. */
|
|
148
|
+
function looseId(id) {
|
|
149
|
+
return id.toLowerCase().replace(/[._-]/g, "-");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Catalog specifiers that differ from `specifier` only in case or separators.
|
|
153
|
+
* AI Gateway's catalog spells `anthropic/claude-opus-4.8` where Anthropic
|
|
154
|
+
* spells `claude-opus-4-8`, and the difference is easy to miss.
|
|
155
|
+
*/
|
|
156
|
+
function nearSpecifiers(specifier, specifiers) {
|
|
157
|
+
const wanted = looseId(specifier);
|
|
158
|
+
return [...specifiers].filter((id) => looseId(id) === wanted).sort();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolves a `<slug>/<id>` specifier through pi-ai's generated Cloudflare AI
|
|
162
|
+
* Gateway registry. The entry's metadata — api, base URL, compat,
|
|
163
|
+
* `thinkingLevelMap`, cost, limits — is kept exactly as pi generated it; only
|
|
164
|
+
* `id` and `provider` are restated so a `Models` registry can find the model
|
|
165
|
+
* under `cloudflare/<slug>/<id>`, and the vendor's own spelling rides along for
|
|
166
|
+
* the request body.
|
|
167
|
+
*
|
|
168
|
+
* There is no prefix guessing: an id pi-ai does not list is a `TypeError`,
|
|
169
|
+
* pointing at the model-object form, which needs no catalog of ours at all.
|
|
170
|
+
*/
|
|
171
|
+
function buildGatewayModel(specifier, options) {
|
|
172
|
+
gatewayRegistry ??= buildGatewayRegistry();
|
|
173
|
+
const separator = specifier.indexOf("/");
|
|
174
|
+
const slug = separator === -1 ? "" : specifier.slice(0, separator);
|
|
175
|
+
const vendorId = specifier.slice(separator + 1);
|
|
176
|
+
if (slug === "workers-ai") throw new TypeError(`"${specifier}" is a Workers AI model: pass its id directly, ai("${vendorId}").`);
|
|
177
|
+
const entry = gatewayRegistry.get(specifier);
|
|
178
|
+
if (entry === void 0) {
|
|
179
|
+
const near = nearSpecifiers(specifier, gatewayRegistry.keys());
|
|
180
|
+
const suggestion = near.length === 0 ? "" : ` Did you mean ${near.map((id) => `ai("${id}")`).join(" or ")}? The catalog spells ids as AI Gateway does.`;
|
|
181
|
+
throw new TypeError(`"${specifier}" is not in pi-ai's Cloudflare AI Gateway catalog.${suggestion} Pass the model object instead, e.g. ai(getBuiltinModel("${slug || "anthropic"}", "${vendorId}")) from "@earendil-works/pi-ai/providers/all", or any model from a provider registry you imported.`);
|
|
182
|
+
}
|
|
183
|
+
const model = {
|
|
184
|
+
...entry,
|
|
185
|
+
id: specifier,
|
|
186
|
+
provider: CLOUDFLARE_PROVIDER_ID
|
|
187
|
+
};
|
|
188
|
+
tag(model, MODEL_ROUTING, {
|
|
189
|
+
modelId: entry.id,
|
|
190
|
+
specifier
|
|
191
|
+
});
|
|
192
|
+
if (options !== void 0) tag(model, MODEL_OPTIONS, options);
|
|
193
|
+
return model;
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/models/pi-ai/routing.ts
|
|
197
|
+
/**
|
|
198
|
+
* The path each pi api speaks, relative to the model's own `baseUrl` — exactly
|
|
199
|
+
* what pi's SDK clients append to it.
|
|
200
|
+
*/
|
|
201
|
+
const API_PATHS = {
|
|
202
|
+
"anthropic-messages": "v1/messages",
|
|
203
|
+
"openai-completions": "chat/completions",
|
|
204
|
+
"openai-responses": "responses"
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* The universal request's `endpoint` for a gateway-shaped base URL, whose path
|
|
208
|
+
* already ends at the provider slug, so what follows is the vendor's own path.
|
|
209
|
+
*/
|
|
210
|
+
const GATEWAY_ENDPOINTS = {
|
|
211
|
+
"anthropic-messages": "v1/messages",
|
|
212
|
+
"openai-completions": "v1/chat/completions",
|
|
213
|
+
"openai-responses": "v1/responses"
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* The gateway provider slug that serves a model, from its own base URL.
|
|
217
|
+
*
|
|
218
|
+
* @throws TypeError when the model carries no base URL, or one AI Gateway has
|
|
219
|
+
* no provider for.
|
|
220
|
+
*/
|
|
221
|
+
function gatewaySlugForModel(model) {
|
|
222
|
+
if (model.baseUrl === "") throw new TypeError(`The model "${model.id}" has no baseUrl, so AI Gateway cannot tell which provider serves it.`);
|
|
223
|
+
return gatewaySlugForBaseUrl(model.baseUrl);
|
|
224
|
+
}
|
|
225
|
+
/** `https://api.groq.com/openai/v1` → `https://api.groq.com/openai/v1/`. */
|
|
226
|
+
function asBase(baseUrl) {
|
|
227
|
+
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The provider slug and endpoint one model's request travels on.
|
|
231
|
+
*
|
|
232
|
+
* @throws TypeError when the model has no base URL, when AI Gateway has no
|
|
233
|
+
* provider for it, or when this module knows no path for the model's api.
|
|
234
|
+
*/
|
|
235
|
+
function routeForModel(model) {
|
|
236
|
+
const provider = gatewaySlugForModel(model);
|
|
237
|
+
if (isGatewayShapedUrl(model.baseUrl)) {
|
|
238
|
+
const endpoint = GATEWAY_ENDPOINTS[model.api];
|
|
239
|
+
if (endpoint === void 0) throw noEndpoint(model.api);
|
|
240
|
+
return {
|
|
241
|
+
endpoint,
|
|
242
|
+
provider
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const path = API_PATHS[model.api];
|
|
246
|
+
if (path === void 0) throw noEndpoint(model.api);
|
|
247
|
+
const resolved = resolveGatewayProvider(new URL(path, asBase(model.baseUrl)).toString());
|
|
248
|
+
if (resolved === void 0) return {
|
|
249
|
+
endpoint: GATEWAY_ENDPOINTS[model.api] ?? path,
|
|
250
|
+
provider
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
endpoint: resolved.endpoint,
|
|
254
|
+
provider: resolved.provider
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function noEndpoint(api) {
|
|
258
|
+
return /* @__PURE__ */ new TypeError(`No AI Gateway endpoint is known for the "${api}" API.`);
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/models/pi-ai/wires/shared.ts
|
|
262
|
+
/** Diagnostic type under which gateway correlation ids are recorded. */
|
|
263
|
+
const CLOUDFLARE_DIAGNOSTIC = "cloudflare";
|
|
264
|
+
/** The diagnostic type under which compat-layer warnings are recorded. */
|
|
265
|
+
const COMPAT_DIAGNOSTIC = "cloudflare-compat";
|
|
266
|
+
/** Records compat warnings on a message as a `cloudflare-compat` diagnostic. */
|
|
267
|
+
/**
|
|
268
|
+
* pi-ai types diagnostic details as JSON. These details are plain data this
|
|
269
|
+
* module built, so a JSON round trip only drops their undefined fields.
|
|
270
|
+
*/
|
|
271
|
+
function jsonDetails(value) {
|
|
272
|
+
return JSON.parse(JSON.stringify(value));
|
|
273
|
+
}
|
|
274
|
+
function recordWarnings(output, warnings) {
|
|
275
|
+
if (warnings.length === 0) return;
|
|
276
|
+
output.diagnostics = [...output.diagnostics ?? [], {
|
|
277
|
+
details: jsonDetails({ warnings }),
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
type: COMPAT_DIAGNOSTIC
|
|
280
|
+
}];
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The Workers-AI-only knobs a vendor model cannot honour. They are not sent —
|
|
284
|
+
* the vendor's own reasoning follows the model's metadata and the pi
|
|
285
|
+
* `reasoning` level — and the drop is recorded rather than silent.
|
|
286
|
+
*/
|
|
287
|
+
function ignoredWorkersAIKnobs(request) {
|
|
288
|
+
const warnings = [];
|
|
289
|
+
if (request.resolved.reasoningEffort !== void 0) warnings.push({
|
|
290
|
+
feature: "reasoning-effort",
|
|
291
|
+
message: "reasoningEffort is a Workers AI setting and does not apply to a model routed through AI Gateway; use the pi `reasoning` level, which the model's own metadata maps."
|
|
292
|
+
});
|
|
293
|
+
if (request.resolved.chatTemplateKwargs !== void 0) warnings.push({
|
|
294
|
+
feature: "chat-template-kwargs",
|
|
295
|
+
message: "chatTemplateKwargs is a Workers AI setting and was not sent to a model routed through AI Gateway."
|
|
296
|
+
});
|
|
297
|
+
return warnings;
|
|
298
|
+
}
|
|
299
|
+
function emptyUsage() {
|
|
300
|
+
return {
|
|
301
|
+
cacheRead: 0,
|
|
302
|
+
cacheWrite: 0,
|
|
303
|
+
cost: {
|
|
304
|
+
cacheRead: 0,
|
|
305
|
+
cacheWrite: 0,
|
|
306
|
+
input: 0,
|
|
307
|
+
output: 0,
|
|
308
|
+
total: 0
|
|
309
|
+
},
|
|
310
|
+
input: 0,
|
|
311
|
+
output: 0,
|
|
312
|
+
totalTokens: 0
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** The assistant message a stream mutates as events arrive. */
|
|
316
|
+
function startMessage(model) {
|
|
317
|
+
return {
|
|
318
|
+
api: model.api,
|
|
319
|
+
content: [],
|
|
320
|
+
model: model.id,
|
|
321
|
+
provider: model.provider,
|
|
322
|
+
role: "assistant",
|
|
323
|
+
stopReason: "stop",
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
usage: emptyUsage()
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Maps OpenAI-shaped usage onto pi-ai's. `input` excludes cached tokens, as
|
|
330
|
+
* pi-ai's own OpenAI adapter does, and cost comes from the model metadata.
|
|
331
|
+
*/
|
|
332
|
+
function usageFromOpenAI(model, raw) {
|
|
333
|
+
if (raw === void 0) return emptyUsage();
|
|
334
|
+
const prompt = raw.prompt_tokens ?? 0;
|
|
335
|
+
const completion = raw.completion_tokens ?? 0;
|
|
336
|
+
const cacheRead = raw.prompt_tokens_details?.cached_tokens ?? raw.prompt_cache_hit_tokens ?? 0;
|
|
337
|
+
const cacheWrite = raw.prompt_tokens_details?.cache_write_tokens ?? 0;
|
|
338
|
+
const reasoning = raw.completion_tokens_details?.reasoning_tokens;
|
|
339
|
+
const totalTokens = raw.total_tokens ?? prompt + completion;
|
|
340
|
+
const output = raw.total_tokens === void 0 ? completion : Math.max(completion, totalTokens - prompt);
|
|
341
|
+
const usage = {
|
|
342
|
+
cacheRead,
|
|
343
|
+
cacheWrite,
|
|
344
|
+
cost: {
|
|
345
|
+
cacheRead: 0,
|
|
346
|
+
cacheWrite: 0,
|
|
347
|
+
input: 0,
|
|
348
|
+
output: 0,
|
|
349
|
+
total: 0
|
|
350
|
+
},
|
|
351
|
+
input: Math.max(0, prompt - cacheRead - cacheWrite),
|
|
352
|
+
output,
|
|
353
|
+
...reasoning !== void 0 ? { reasoning } : {},
|
|
354
|
+
totalTokens
|
|
355
|
+
};
|
|
356
|
+
usage.cost = calculateCost(model, usage);
|
|
357
|
+
return usage;
|
|
358
|
+
}
|
|
359
|
+
function correlationOf(response, transport) {
|
|
360
|
+
const get = (name) => response.headers.get(name) ?? void 0;
|
|
361
|
+
return {
|
|
362
|
+
cacheStatus: get("cf-aig-cache-status"),
|
|
363
|
+
eventId: get("cf-aig-event-id"),
|
|
364
|
+
logId: get("cf-aig-log-id") ?? transport.logIdFallback(response),
|
|
365
|
+
runId: get("cf-aig-run-id"),
|
|
366
|
+
step: get("cf-aig-step"),
|
|
367
|
+
traceId: get("cf-aig-trace-id")
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function definedEntries$1(record) {
|
|
371
|
+
const result = {};
|
|
372
|
+
for (const [key, value] of Object.entries(record)) if (value !== void 0) result[key] = value;
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Records where a response came from on the assistant message, as a pi-ai
|
|
377
|
+
* diagnostic: the gateway log id and friends, the answering model and the
|
|
378
|
+
* gateway id. Callers read it back from `message.diagnostics`.
|
|
379
|
+
*/
|
|
380
|
+
function attachCorrelation(message, details) {
|
|
381
|
+
const diagnostic = {
|
|
382
|
+
details: jsonDetails(definedEntries$1({ ...details })),
|
|
383
|
+
timestamp: Date.now(),
|
|
384
|
+
type: CLOUDFLARE_DIAGNOSTIC
|
|
385
|
+
};
|
|
386
|
+
message.diagnostics = [...message.diagnostics ?? [], diagnostic];
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Wraps a body so a gap longer than `idleMs` between chunks rejects the read
|
|
390
|
+
* instead of pending forever. Only an outstanding read is timed, so a slow
|
|
391
|
+
* consumer never trips it; `idleMs <= 0` disables the guard.
|
|
392
|
+
*/
|
|
393
|
+
function withIdleDeadline(body, idleMs) {
|
|
394
|
+
if (idleMs <= 0) return body;
|
|
395
|
+
const reader = body.getReader();
|
|
396
|
+
return new ReadableStream({
|
|
397
|
+
cancel(reason) {
|
|
398
|
+
return reader.cancel(reason);
|
|
399
|
+
},
|
|
400
|
+
async pull(controller) {
|
|
401
|
+
let timer;
|
|
402
|
+
try {
|
|
403
|
+
const result = await Promise.race([reader.read(), new Promise((_, reject) => {
|
|
404
|
+
timer = setTimeout(() => {
|
|
405
|
+
reject(/* @__PURE__ */ new Error(`Model stream stalled: no data received for ${Math.round(idleMs / 1e3)}s.`));
|
|
406
|
+
}, idleMs);
|
|
407
|
+
})]);
|
|
408
|
+
if (result.done) controller.close();
|
|
409
|
+
else controller.enqueue(result.value);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
reader.cancel().catch(() => {});
|
|
412
|
+
controller.error(error);
|
|
413
|
+
} finally {
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Iterates the JSON payload of each SSE event. `[DONE]` sentinels and
|
|
421
|
+
* unparseable payloads are skipped; the reader is cancelled if iteration
|
|
422
|
+
* stops early so the upstream request is not left generating.
|
|
423
|
+
*/
|
|
424
|
+
async function* sseJson(body, onDone) {
|
|
425
|
+
const reader = body.pipeThrough(sseDataStream()).getReader();
|
|
426
|
+
let finished = false;
|
|
427
|
+
try {
|
|
428
|
+
while (true) {
|
|
429
|
+
const { done, value } = await reader.read();
|
|
430
|
+
if (done) {
|
|
431
|
+
finished = true;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (value === "") continue;
|
|
435
|
+
if (value === "[DONE]") {
|
|
436
|
+
onDone?.();
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
yield JSON.parse(value);
|
|
441
|
+
} catch {}
|
|
442
|
+
}
|
|
443
|
+
} finally {
|
|
444
|
+
if (!finished) await reader.cancel().catch(() => {});
|
|
445
|
+
reader.releaseLock();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** `response.headers` as pi-ai's `onResponse` callback wants them. */
|
|
449
|
+
function responseInfo(response) {
|
|
450
|
+
return {
|
|
451
|
+
headers: headersToObject(response.headers),
|
|
452
|
+
status: response.status
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The Anthropic API version every Messages request must name. pi-ai leaves it
|
|
457
|
+
* to the Anthropic SDK, which we replace, so the wire states it here.
|
|
458
|
+
*/
|
|
459
|
+
const ANTHROPIC_VERSION = "2023-06-01";
|
|
460
|
+
/**
|
|
461
|
+
* The vendor headers an Anthropic Messages request needs. pi-ai normally gets
|
|
462
|
+
* them from the Anthropic SDK's defaults plus its own `createClient`; this
|
|
463
|
+
* wire owns the transport instead, so it sets the ones the vendor requires and
|
|
464
|
+
* sends the beta features pi-ai chose as `anthropic-beta`, as the SDK would.
|
|
465
|
+
* No credential header is sent: the gateway holds the key (unified billing or
|
|
466
|
+
* BYOK).
|
|
467
|
+
*/
|
|
468
|
+
function anthropicVendorHeaders(betas) {
|
|
469
|
+
return {
|
|
470
|
+
accept: "application/json",
|
|
471
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
472
|
+
...betas.length > 0 ? { "anthropic-beta": betas.join(",") } : {},
|
|
473
|
+
"content-type": "application/json"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/** The vendor headers an OpenAI-shaped request needs. */
|
|
477
|
+
function openAIVendorHeaders() {
|
|
478
|
+
return {
|
|
479
|
+
accept: "application/json",
|
|
480
|
+
"content-type": "application/json"
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Sends a vendor's own request body through AI Gateway's universal endpoint.
|
|
485
|
+
*
|
|
486
|
+
* The provider slug comes from the model's base URL and the endpoint from its
|
|
487
|
+
* `api`; the body is what pi-ai's converter built, `model` included, because
|
|
488
|
+
* that is what the universal request carries. The gateway's *own* failures
|
|
489
|
+
* (no such gateway, unified billing unpaid) are lifted into a
|
|
490
|
+
* `CloudflareAIError` so fallback and retry classification behave as they do on
|
|
491
|
+
* the Workers AI run path; a vendor's own error response is left alone for the
|
|
492
|
+
* wire's parser to read.
|
|
493
|
+
*/
|
|
494
|
+
async function sendUniversal(request, transport, body, vendorHeaders, onResponse) {
|
|
495
|
+
const { model, options, resolved } = request;
|
|
496
|
+
const route = routeForModel(model);
|
|
497
|
+
const response = await transport.universal({
|
|
498
|
+
endpoint: route.endpoint,
|
|
499
|
+
extraHeaders: Object.keys(request.headers).length > 0 ? request.headers : void 0,
|
|
500
|
+
gateway: resolved.gateway,
|
|
501
|
+
headers: vendorHeaders,
|
|
502
|
+
provider: route.provider,
|
|
503
|
+
query: body,
|
|
504
|
+
signal: options.signal
|
|
505
|
+
});
|
|
506
|
+
onResponse?.(response);
|
|
507
|
+
return await liftGatewayError(response, {
|
|
508
|
+
model: wireModelId(model),
|
|
509
|
+
requestBodyValues: body,
|
|
510
|
+
url: transport.url
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Turns the gateway's own error envelope into a `CloudflareAIError`. Only a
|
|
515
|
+
* JSON body can be one, so a streaming response is passed straight through and
|
|
516
|
+
* never buffered.
|
|
517
|
+
*/
|
|
518
|
+
async function liftGatewayError(response, context) {
|
|
519
|
+
if (!(response.headers.get("content-type") ?? "").includes("application/json")) return response;
|
|
520
|
+
const text = await response.text().catch(() => "");
|
|
521
|
+
let parsed;
|
|
522
|
+
try {
|
|
523
|
+
parsed = text === "" ? void 0 : JSON.parse(text);
|
|
524
|
+
} catch {
|
|
525
|
+
parsed = void 0;
|
|
526
|
+
}
|
|
527
|
+
if (isGatewayErrorEnvelope(parsed)) throw errorFromGatewayEnvelope(parsed, {
|
|
528
|
+
...context,
|
|
529
|
+
responseBody: text,
|
|
530
|
+
responseHeaders: headersToObject(response.headers),
|
|
531
|
+
status: response.status
|
|
532
|
+
});
|
|
533
|
+
return new Response(text, response);
|
|
534
|
+
}
|
|
535
|
+
/** The correlation details every wire records, with the routing it used. */
|
|
536
|
+
function correlationDetails(request, response, transport) {
|
|
537
|
+
const { model, resolved } = request;
|
|
538
|
+
const specifier = specifierOf(model);
|
|
539
|
+
return {
|
|
540
|
+
...correlationOf(response, transport),
|
|
541
|
+
gateway: resolved.gateway.id,
|
|
542
|
+
model: model.id,
|
|
543
|
+
...specifier !== void 0 ? { specifier } : {},
|
|
544
|
+
...model.api === "cloudflare-ai" ? {} : { provider: gatewaySlugForModel(model) }
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/models/pi-ai/errors.ts
|
|
549
|
+
/** Diagnostic type under which a failed dispatch is recorded. */
|
|
550
|
+
const CLOUDFLARE_ERROR_DIAGNOSTIC = "cloudflare-error";
|
|
551
|
+
function definedEntries(record) {
|
|
552
|
+
const result = {};
|
|
553
|
+
for (const [key, value] of Object.entries(record)) if (value !== void 0) result[key] = value;
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Terminates a stream with a pi-ai `error` event built from a thrown value.
|
|
558
|
+
* `aborted` when the caller's signal fired or the error is an abort; `error`
|
|
559
|
+
* otherwise, with the failed dispatch recorded as a diagnostic.
|
|
560
|
+
*/
|
|
561
|
+
function failStream(stream, message, error, signal) {
|
|
562
|
+
const aborted = signal?.aborted === true || isAbortError(error);
|
|
563
|
+
const reason = aborted ? "aborted" : "error";
|
|
564
|
+
message.stopReason = reason;
|
|
565
|
+
message.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
566
|
+
if (!aborted) {
|
|
567
|
+
const details = {};
|
|
568
|
+
if (error instanceof CloudflareAIError) Object.assign(details, definedEntries({
|
|
569
|
+
code: error.code,
|
|
570
|
+
logId: error.logId,
|
|
571
|
+
model: error.model,
|
|
572
|
+
status: error.status
|
|
573
|
+
}));
|
|
574
|
+
message.diagnostics = [...message.diagnostics ?? [], {
|
|
575
|
+
details: jsonDetails(details),
|
|
576
|
+
error: {
|
|
577
|
+
message: message.errorMessage,
|
|
578
|
+
name: error instanceof Error ? error.name : "Error",
|
|
579
|
+
...error instanceof CloudflareAIError ? { code: error.code } : {}
|
|
580
|
+
},
|
|
581
|
+
timestamp: Date.now(),
|
|
582
|
+
type: CLOUDFLARE_ERROR_DIAGNOSTIC
|
|
583
|
+
}];
|
|
584
|
+
}
|
|
585
|
+
stream.push({
|
|
586
|
+
error: message,
|
|
587
|
+
reason,
|
|
588
|
+
type: "error"
|
|
589
|
+
});
|
|
590
|
+
stream.end(message);
|
|
591
|
+
}
|
|
592
|
+
/** Throws the typed error for a non-2xx response. */
|
|
593
|
+
async function assertOk(response, context) {
|
|
594
|
+
if (response.ok) return;
|
|
595
|
+
throw await errorFromResponse(response, context);
|
|
596
|
+
}
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/models/pi-ai/wires/chat-completions.ts
|
|
599
|
+
/**
|
|
600
|
+
* Chat-completions wire: Workers AI on the run path, and any pi-ai model whose
|
|
601
|
+
* `api` is `openai-completions` on AI Gateway's universal endpoint.
|
|
602
|
+
*
|
|
603
|
+
* The request is shaped with pi-ai's own message converter so replay rules
|
|
604
|
+
* (tool-call id normalization, reasoning replay, role bridging) match pi's
|
|
605
|
+
* other OpenAI-compatible providers. Workers AI then runs through the shared
|
|
606
|
+
* compat layer, which absorbs its native events, heartbeats, per-delta usage
|
|
607
|
+
* and per-family request quirks so the parser below only ever sees strict
|
|
608
|
+
* OpenAI chunks. A vendor model brings its own `compat` profile and needs none
|
|
609
|
+
* of that: its bytes are already what its author says they are.
|
|
610
|
+
*/
|
|
611
|
+
/**
|
|
612
|
+
* pi-ai's message converter wants a fully resolved compat profile. This is
|
|
613
|
+
* the strict OpenAI profile: replayed reasoning goes to `reasoning_content`,
|
|
614
|
+
* `max_tokens` is the cap, and nothing vendor-specific is applied here — the
|
|
615
|
+
* shared compat layer handles that per model afterwards.
|
|
616
|
+
*/
|
|
617
|
+
const STRICT_OPENAI_COMPAT = {
|
|
618
|
+
chatTemplateArgs: {},
|
|
619
|
+
chatTemplateKwargs: {},
|
|
620
|
+
maxTokensField: "max_tokens",
|
|
621
|
+
openRouterRouting: {},
|
|
622
|
+
requiresAssistantAfterToolResult: false,
|
|
623
|
+
requiresReasoningContentOnAssistantMessages: false,
|
|
624
|
+
requiresThinkingAsText: false,
|
|
625
|
+
requiresToolResultName: false,
|
|
626
|
+
sendSessionAffinityHeaders: false,
|
|
627
|
+
sessionAffinityFormat: "openai",
|
|
628
|
+
supportsDeveloperRole: false,
|
|
629
|
+
supportsFinishReason: true,
|
|
630
|
+
supportsLongCacheRetention: false,
|
|
631
|
+
supportsOpenAIGrammarTools: false,
|
|
632
|
+
supportsReasoningEffort: true,
|
|
633
|
+
supportsStore: false,
|
|
634
|
+
supportsStrictMode: false,
|
|
635
|
+
supportsUsageInStreaming: true,
|
|
636
|
+
thinkingFormat: "openai",
|
|
637
|
+
vercelGatewayRouting: {},
|
|
638
|
+
zaiToolStream: false
|
|
639
|
+
};
|
|
640
|
+
/**
|
|
641
|
+
* The fields an OpenAI-compatible endpoint puts reasoning in, in pi-ai's own
|
|
642
|
+
* order (`dist/api/openai-completions.js`). The first non-empty one wins and
|
|
643
|
+
* the rest are skipped: some endpoints send the same text under two spellings,
|
|
644
|
+
* and reading both would double the thinking block.
|
|
645
|
+
*/
|
|
646
|
+
const REASONING_FIELDS = [
|
|
647
|
+
"reasoning_content",
|
|
648
|
+
"reasoning",
|
|
649
|
+
"reasoning_text"
|
|
650
|
+
];
|
|
651
|
+
function mapFinishReason(reason) {
|
|
652
|
+
switch (reason) {
|
|
653
|
+
case "stop":
|
|
654
|
+
case "eos": return "stop";
|
|
655
|
+
case "length":
|
|
656
|
+
case "model_length": return "length";
|
|
657
|
+
case "tool_calls":
|
|
658
|
+
case "function_call": return "toolUse";
|
|
659
|
+
default: return "error";
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function convertTools(tools) {
|
|
663
|
+
return tools.map((tool) => ({
|
|
664
|
+
function: {
|
|
665
|
+
description: tool.description,
|
|
666
|
+
name: tool.name,
|
|
667
|
+
parameters: tool.parameters
|
|
668
|
+
},
|
|
669
|
+
type: "function"
|
|
670
|
+
}));
|
|
671
|
+
}
|
|
672
|
+
/** Whether a tool-call chunk is the null finalizer some vLLM parsers emit. */
|
|
673
|
+
function isFinalizer(call) {
|
|
674
|
+
const fn = call.function;
|
|
675
|
+
return call.index === void 0 && (call.id === null || call.id === void 0) && (fn?.name === null || fn?.name === void 0) && (fn?.arguments === null || fn?.arguments === void 0 || fn?.arguments === "");
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Incremental state for one streamed completion: the blocks being built on
|
|
679
|
+
* the assistant message and the events that announce them.
|
|
680
|
+
*/
|
|
681
|
+
var CompletionAssembler = class {
|
|
682
|
+
constructor(model, output, stream) {
|
|
683
|
+
this.model = model;
|
|
684
|
+
this.output = output;
|
|
685
|
+
this.stream = stream;
|
|
686
|
+
this.openToolCalls = /* @__PURE__ */ new Map();
|
|
687
|
+
this.closedToolCalls = /* @__PURE__ */ new Set();
|
|
688
|
+
this.blocks = output.content;
|
|
689
|
+
}
|
|
690
|
+
indexOf(block) {
|
|
691
|
+
return this.blocks.indexOf(block);
|
|
692
|
+
}
|
|
693
|
+
stripScratch() {
|
|
694
|
+
for (const block of this.blocks) if (block.type === "toolCall") delete block.partialArguments;
|
|
695
|
+
}
|
|
696
|
+
endText() {
|
|
697
|
+
if (this.textBlock === void 0) return;
|
|
698
|
+
this.stream.push({
|
|
699
|
+
content: this.textBlock.text,
|
|
700
|
+
contentIndex: this.indexOf(this.textBlock),
|
|
701
|
+
partial: this.output,
|
|
702
|
+
type: "text_end"
|
|
703
|
+
});
|
|
704
|
+
this.textBlock = void 0;
|
|
705
|
+
}
|
|
706
|
+
endThinking() {
|
|
707
|
+
if (this.thinkingBlock === void 0) return;
|
|
708
|
+
this.stream.push({
|
|
709
|
+
content: this.thinkingBlock.thinking,
|
|
710
|
+
contentIndex: this.indexOf(this.thinkingBlock),
|
|
711
|
+
partial: this.output,
|
|
712
|
+
type: "thinking_end"
|
|
713
|
+
});
|
|
714
|
+
this.thinkingBlock = void 0;
|
|
715
|
+
}
|
|
716
|
+
pushText(delta) {
|
|
717
|
+
if (delta.length === 0) return;
|
|
718
|
+
this.endThinking();
|
|
719
|
+
if (this.textBlock === void 0) {
|
|
720
|
+
this.textBlock = {
|
|
721
|
+
text: "",
|
|
722
|
+
type: "text"
|
|
723
|
+
};
|
|
724
|
+
this.blocks.push(this.textBlock);
|
|
725
|
+
this.stream.push({
|
|
726
|
+
contentIndex: this.indexOf(this.textBlock),
|
|
727
|
+
partial: this.output,
|
|
728
|
+
type: "text_start"
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
this.textBlock.text += delta;
|
|
732
|
+
this.stream.push({
|
|
733
|
+
contentIndex: this.indexOf(this.textBlock),
|
|
734
|
+
delta,
|
|
735
|
+
partial: this.output,
|
|
736
|
+
type: "text_delta"
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
pushThinking(delta, signature) {
|
|
740
|
+
if (delta.length === 0) return;
|
|
741
|
+
this.endText();
|
|
742
|
+
if (this.thinkingBlock === void 0) {
|
|
743
|
+
this.thinkingBlock = {
|
|
744
|
+
thinking: "",
|
|
745
|
+
thinkingSignature: signature,
|
|
746
|
+
type: "thinking"
|
|
747
|
+
};
|
|
748
|
+
this.blocks.push(this.thinkingBlock);
|
|
749
|
+
this.stream.push({
|
|
750
|
+
contentIndex: this.indexOf(this.thinkingBlock),
|
|
751
|
+
partial: this.output,
|
|
752
|
+
type: "thinking_start"
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
this.thinkingBlock.thinking += delta;
|
|
756
|
+
this.stream.push({
|
|
757
|
+
contentIndex: this.indexOf(this.thinkingBlock),
|
|
758
|
+
delta,
|
|
759
|
+
partial: this.output,
|
|
760
|
+
type: "thinking_delta"
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
closeToolCall(index) {
|
|
764
|
+
const block = this.openToolCalls.get(index);
|
|
765
|
+
if (block === void 0 || this.closedToolCalls.has(index)) return;
|
|
766
|
+
this.closedToolCalls.add(index);
|
|
767
|
+
block.arguments = parseStreamingJson(block.partialArguments ?? "");
|
|
768
|
+
delete block.partialArguments;
|
|
769
|
+
this.stream.push({
|
|
770
|
+
contentIndex: this.indexOf(block),
|
|
771
|
+
partial: this.output,
|
|
772
|
+
toolCall: block,
|
|
773
|
+
type: "toolcall_end"
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
pushToolCalls(calls) {
|
|
777
|
+
this.endThinking();
|
|
778
|
+
this.endText();
|
|
779
|
+
for (const call of calls) {
|
|
780
|
+
if (isFinalizer(call)) {
|
|
781
|
+
if (this.lastToolIndex !== void 0) this.closeToolCall(this.lastToolIndex);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
const name = call.function?.name ?? void 0;
|
|
785
|
+
const args = call.function?.arguments ?? "";
|
|
786
|
+
const index = typeof call.index === "number" ? call.index : this.lastToolIndex ?? 0;
|
|
787
|
+
if (this.closedToolCalls.has(index)) continue;
|
|
788
|
+
let block = this.openToolCalls.get(index);
|
|
789
|
+
if (block === void 0) {
|
|
790
|
+
if (this.lastToolIndex !== void 0 && this.lastToolIndex !== index) this.closeToolCall(this.lastToolIndex);
|
|
791
|
+
block = {
|
|
792
|
+
arguments: {},
|
|
793
|
+
id: call.id ?? crypto.randomUUID(),
|
|
794
|
+
name: name ?? "",
|
|
795
|
+
partialArguments: "",
|
|
796
|
+
type: "toolCall"
|
|
797
|
+
};
|
|
798
|
+
this.openToolCalls.set(index, block);
|
|
799
|
+
this.blocks.push(block);
|
|
800
|
+
this.stream.push({
|
|
801
|
+
contentIndex: this.indexOf(block),
|
|
802
|
+
partial: this.output,
|
|
803
|
+
type: "toolcall_start"
|
|
804
|
+
});
|
|
805
|
+
} else {
|
|
806
|
+
if (block.name === "" && name) block.name = name;
|
|
807
|
+
if (call.id) block.id = call.id;
|
|
808
|
+
}
|
|
809
|
+
this.lastToolIndex = index;
|
|
810
|
+
if (args.length > 0) {
|
|
811
|
+
block.partialArguments = (block.partialArguments ?? "") + args;
|
|
812
|
+
block.arguments = parseStreamingJson(block.partialArguments);
|
|
813
|
+
this.stream.push({
|
|
814
|
+
contentIndex: this.indexOf(block),
|
|
815
|
+
delta: args,
|
|
816
|
+
partial: this.output,
|
|
817
|
+
type: "toolcall_delta"
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
applyFinishReason(reason) {
|
|
823
|
+
if (reason === null || reason === void 0) return false;
|
|
824
|
+
this.output.stopReason = mapFinishReason(reason);
|
|
825
|
+
if (this.output.stopReason === "error") this.output.errorMessage = `The model stopped with finish_reason "${reason}".`;
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
/** Applies one strict chunk (or a whole normalized body); true if it carried a finish reason. */
|
|
829
|
+
apply(chunk) {
|
|
830
|
+
if (chunk.error !== void 0) {
|
|
831
|
+
this.output.stopReason = "error";
|
|
832
|
+
this.output.errorMessage = chunk.error.message ?? "The model reported an error.";
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
const choice = chunk.choices?.[0];
|
|
836
|
+
if (chunk.id !== void 0) this.output.responseId ??= chunk.id;
|
|
837
|
+
if (typeof chunk.model === "string" && chunk.model.length > 0 && chunk.model !== this.model.id) this.output.responseModel ??= chunk.model;
|
|
838
|
+
if (chunk.usage !== void 0) this.output.usage = usageFromOpenAI(this.model, chunk.usage);
|
|
839
|
+
const finished = this.applyFinishReason(choice?.finish_reason);
|
|
840
|
+
const delta = choice?.delta ?? choice?.message;
|
|
841
|
+
if (delta !== void 0) {
|
|
842
|
+
for (const field of REASONING_FIELDS) {
|
|
843
|
+
const value = delta[field];
|
|
844
|
+
if (typeof value === "string" && value.length > 0) {
|
|
845
|
+
this.pushThinking(value, field);
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (typeof delta.content === "string") this.pushText(delta.content);
|
|
850
|
+
if (Array.isArray(delta.tool_calls)) {
|
|
851
|
+
const complete = choice?.delta === void 0;
|
|
852
|
+
this.pushToolCalls(complete ? delta.tool_calls.map((call, position) => ({
|
|
853
|
+
...call,
|
|
854
|
+
index: typeof call.index === "number" ? call.index : position
|
|
855
|
+
})) : delta.tool_calls);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return finished;
|
|
859
|
+
}
|
|
860
|
+
/** Closes open blocks and settles the stop reason. */
|
|
861
|
+
finish(sawFinishReason, sawDone) {
|
|
862
|
+
for (const index of this.openToolCalls.keys()) this.closeToolCall(index);
|
|
863
|
+
this.endThinking();
|
|
864
|
+
this.endText();
|
|
865
|
+
const hasToolCalls = this.blocks.some((b) => b.type === "toolCall");
|
|
866
|
+
if (this.output.stopReason === "error") return;
|
|
867
|
+
if (!sawFinishReason) if (sawDone) this.output.stopReason = hasToolCalls ? "toolUse" : "stop";
|
|
868
|
+
else {
|
|
869
|
+
this.output.stopReason = "error";
|
|
870
|
+
this.output.errorMessage = this.blocks.length > 0 ? "The stream ended before the model finished its answer." : "The stream ended before the model produced any output.";
|
|
871
|
+
}
|
|
872
|
+
else if (this.output.stopReason === "stop" && hasToolCalls) this.output.stopReason = "toolUse";
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
/**
|
|
876
|
+
* Puts the reasoning knobs on a chat-completions body, and reports the ones a
|
|
877
|
+
* model cannot take.
|
|
878
|
+
*
|
|
879
|
+
* Workers AI is the one model family with knobs of ours: `reasoning_effort` on
|
|
880
|
+
* its own three-level scale and `chat_template_kwargs`, both of which the
|
|
881
|
+
* compat layer re-checks against the quirk table afterwards. For every other
|
|
882
|
+
* model the decision is the model author's, read off the merged compat profile
|
|
883
|
+
* exactly as pi-ai's own `openai-completions` implementation reads it: the
|
|
884
|
+
* effort is emitted only when the model reasons, declares
|
|
885
|
+
* `supportsReasoningEffort` and asks for OpenAI's `reasoning_effort` shape,
|
|
886
|
+
* and it is remapped through the model's own `thinkingLevelMap`.
|
|
887
|
+
*/
|
|
888
|
+
function applyReasoning(strict, context) {
|
|
889
|
+
const { chatTemplateKwargs, compat, effort, model, workersAI } = context;
|
|
890
|
+
if (workersAI) {
|
|
891
|
+
if (effort !== void 0) strict.reasoning_effort = effort;
|
|
892
|
+
if (chatTemplateKwargs !== void 0) strict.chat_template_kwargs = chatTemplateKwargs;
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
const ignored = [];
|
|
896
|
+
const thinkingFormat = compat.thinkingFormat;
|
|
897
|
+
if (typeof effort === "string") {
|
|
898
|
+
if (model.reasoning === true && compat.supportsReasoningEffort !== false && thinkingFormat === "openai") strict.reasoning_effort = model.thinkingLevelMap?.[effort] ?? effort;
|
|
899
|
+
else if (thinkingFormat !== "openai") ignored.push({
|
|
900
|
+
feature: "reasoning-effort",
|
|
901
|
+
message: `This model declares the "${thinkingFormat}" thinking format, which this wire does not build; its reasoning level was dropped. Reach it through pi-ai's own provider if you need that shape.`
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (context.workersAIEffort === null) ignored.push({
|
|
905
|
+
feature: "reasoning-off",
|
|
906
|
+
message: "`reasoningEffort: null` is a Workers AI setting; a model routed through AI Gateway decides its own reasoning, so it was dropped."
|
|
907
|
+
});
|
|
908
|
+
else if (context.workersAIEffort !== void 0) ignored.push({
|
|
909
|
+
feature: "reasoning-effort",
|
|
910
|
+
message: "`reasoningEffort` is a Workers AI setting and does not apply to a model routed through AI Gateway; use the pi `reasoning` level, which the model's own metadata maps."
|
|
911
|
+
});
|
|
912
|
+
if (chatTemplateKwargs !== void 0) ignored.push({
|
|
913
|
+
feature: "chat-template-kwargs",
|
|
914
|
+
message: "`chatTemplateKwargs` is a Workers AI setting and does not apply to a model routed through AI Gateway; it was dropped."
|
|
915
|
+
});
|
|
916
|
+
return ignored;
|
|
917
|
+
}
|
|
918
|
+
/** Streams a chat-completions request and maps it onto pi-ai events. */
|
|
919
|
+
function streamCompletions(request, transport) {
|
|
920
|
+
const { model, context, options, resolved } = request;
|
|
921
|
+
const stream = createAssistantMessageEventStream();
|
|
922
|
+
const output = startMessage(model);
|
|
923
|
+
const assembler = new CompletionAssembler(model, output, stream);
|
|
924
|
+
const workersAI = model.api === CLOUDFLARE_AI_API;
|
|
925
|
+
(async () => {
|
|
926
|
+
let response;
|
|
927
|
+
try {
|
|
928
|
+
const compat = workersAI ? STRICT_OPENAI_COMPAT : {
|
|
929
|
+
...STRICT_OPENAI_COMPAT,
|
|
930
|
+
...model.compat ?? {}
|
|
931
|
+
};
|
|
932
|
+
const strict = {
|
|
933
|
+
messages: convertMessages(model, context, compat),
|
|
934
|
+
stream: true,
|
|
935
|
+
stream_options: { include_usage: true }
|
|
936
|
+
};
|
|
937
|
+
if (!workersAI) strict.model = wireModelId(model);
|
|
938
|
+
const tools = getCurrentTools(context.messages);
|
|
939
|
+
if (tools.length > 0) strict.tools = convertTools(tools);
|
|
940
|
+
if (request.toolChoice !== void 0) strict.tool_choice = request.toolChoice;
|
|
941
|
+
if (options.maxTokens !== void 0) strict.max_tokens = options.maxTokens;
|
|
942
|
+
if (options.temperature !== void 0) strict.temperature = options.temperature;
|
|
943
|
+
const ignored = applyReasoning(strict, {
|
|
944
|
+
chatTemplateKwargs: resolved.chatTemplateKwargs,
|
|
945
|
+
compat,
|
|
946
|
+
effort: request.reasoningEffort,
|
|
947
|
+
model,
|
|
948
|
+
workersAI,
|
|
949
|
+
workersAIEffort: resolved.reasoningEffort
|
|
950
|
+
});
|
|
951
|
+
const sampling = options.samplingParams;
|
|
952
|
+
if (sampling !== void 0) Object.assign(strict, sampling);
|
|
953
|
+
const prepared = workersAI ? prepareChatCompletionsRequest(strict, model.id) : {
|
|
954
|
+
body: strict,
|
|
955
|
+
reasoningOff: false,
|
|
956
|
+
warnings: []
|
|
957
|
+
};
|
|
958
|
+
recordWarnings(output, [...ignored, ...prepared.warnings]);
|
|
959
|
+
const overridden = await options.onPayload?.(prepared.body, model);
|
|
960
|
+
const payload = overridden === void 0 ? prepared.body : overridden;
|
|
961
|
+
response = workersAI ? await transport.run({
|
|
962
|
+
gateway: resolved.gateway,
|
|
963
|
+
headers: request.headers,
|
|
964
|
+
input: payload,
|
|
965
|
+
model: model.id,
|
|
966
|
+
signal: options.signal
|
|
967
|
+
}) : await sendUniversal(request, transport, payload, openAIVendorHeaders());
|
|
968
|
+
await options.onResponse?.(responseInfo(response), model);
|
|
969
|
+
attachCorrelation(output, correlationDetails(request, response, transport));
|
|
970
|
+
await assertOk(response, {
|
|
971
|
+
model: model.id,
|
|
972
|
+
requestBodyValues: payload,
|
|
973
|
+
url: transport.url
|
|
974
|
+
});
|
|
975
|
+
if (response.body === null) throw new Error("The model returned an empty response body.");
|
|
976
|
+
stream.push({
|
|
977
|
+
partial: output,
|
|
978
|
+
type: "start"
|
|
979
|
+
});
|
|
980
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
981
|
+
let sawFinishReason = false;
|
|
982
|
+
let sawDone = false;
|
|
983
|
+
const decoded = withIdleDeadline(response.body, request.streamIdleTimeoutMs).pipeThrough(sseDataStream());
|
|
984
|
+
const reader = (workersAI ? decoded.pipeThrough(normalizeChatCompletionsStream(model.id, { reasoningOff: prepared.reasoningOff })) : decoded).getReader();
|
|
985
|
+
try {
|
|
986
|
+
for (;;) {
|
|
987
|
+
const { done, value } = await reader.read();
|
|
988
|
+
if (done) break;
|
|
989
|
+
if (value === "[DONE]") {
|
|
990
|
+
sawDone = true;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
let parsed;
|
|
994
|
+
try {
|
|
995
|
+
parsed = JSON.parse(value);
|
|
996
|
+
} catch {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (parsed === null || typeof parsed !== "object") continue;
|
|
1000
|
+
sawFinishReason = assembler.apply(parsed) || sawFinishReason;
|
|
1001
|
+
}
|
|
1002
|
+
} finally {
|
|
1003
|
+
reader.releaseLock();
|
|
1004
|
+
}
|
|
1005
|
+
assembler.finish(sawFinishReason, sawDone);
|
|
1006
|
+
} else {
|
|
1007
|
+
const body = await response.json();
|
|
1008
|
+
const json = workersAI ? normalizeChatCompletion(body, model.id) : body;
|
|
1009
|
+
assembler.finish(assembler.apply(json), true);
|
|
1010
|
+
}
|
|
1011
|
+
if (options.signal?.aborted) throw new Error("Request was aborted.");
|
|
1012
|
+
if (output.stopReason === "error") throw new Error(output.errorMessage ?? "The model reported an error stop reason.");
|
|
1013
|
+
stream.push({
|
|
1014
|
+
message: output,
|
|
1015
|
+
reason: output.stopReason,
|
|
1016
|
+
type: "done"
|
|
1017
|
+
});
|
|
1018
|
+
stream.end(output);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
if (response?.body !== null && response?.body?.locked === false) response.body.cancel().catch(() => {});
|
|
1021
|
+
assembler.stripScratch();
|
|
1022
|
+
failStream(stream, output, error, options.signal);
|
|
1023
|
+
}
|
|
1024
|
+
})();
|
|
1025
|
+
return stream;
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/models/pi-ai/models/image.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* The `api` marker on image models from this provider.
|
|
1031
|
+
*
|
|
1032
|
+
* @beta This surface is in beta and may change.
|
|
1033
|
+
*/
|
|
1034
|
+
const CLOUDFLARE_AI_IMAGES_API = "cloudflare-ai-images";
|
|
1035
|
+
const IMAGE_OPTIONS = Symbol.for("agents.models.pi-ai.image-options");
|
|
1036
|
+
/** Builds the pi image model for a catalog id. */
|
|
1037
|
+
function buildImagesModel(modelId, options) {
|
|
1038
|
+
const model = {
|
|
1039
|
+
api: CLOUDFLARE_AI_IMAGES_API,
|
|
1040
|
+
baseUrl: "",
|
|
1041
|
+
cost: {
|
|
1042
|
+
cacheRead: 0,
|
|
1043
|
+
cacheWrite: 0,
|
|
1044
|
+
input: 0,
|
|
1045
|
+
output: 0
|
|
1046
|
+
},
|
|
1047
|
+
id: modelId,
|
|
1048
|
+
input: ["text", "image"],
|
|
1049
|
+
name: options?.name ?? modelId,
|
|
1050
|
+
output: ["image"],
|
|
1051
|
+
provider: CLOUDFLARE_PROVIDER_ID,
|
|
1052
|
+
type: "image"
|
|
1053
|
+
};
|
|
1054
|
+
if (options !== void 0) Object.defineProperty(model, IMAGE_OPTIONS, {
|
|
1055
|
+
configurable: true,
|
|
1056
|
+
enumerable: true,
|
|
1057
|
+
value: options,
|
|
1058
|
+
writable: false
|
|
1059
|
+
});
|
|
1060
|
+
return model;
|
|
1061
|
+
}
|
|
1062
|
+
function imageOptionsOf(model) {
|
|
1063
|
+
const options = model[IMAGE_OPTIONS];
|
|
1064
|
+
return options !== null && typeof options === "object" ? options : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function base64ToBytes(base64) {
|
|
1067
|
+
const binary = atob(base64);
|
|
1068
|
+
const bytes = new Uint8Array(binary.length);
|
|
1069
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1070
|
+
return bytes;
|
|
1071
|
+
}
|
|
1072
|
+
function bytesToBase64(bytes) {
|
|
1073
|
+
let binary = "";
|
|
1074
|
+
const chunk = 8192;
|
|
1075
|
+
for (let i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
1076
|
+
return btoa(binary);
|
|
1077
|
+
}
|
|
1078
|
+
/** Reads an image out of the run path's response, whichever shape it used. */
|
|
1079
|
+
async function readImage(response) {
|
|
1080
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1081
|
+
if (contentType.includes("application/json")) {
|
|
1082
|
+
const body = unwrapEnvelope(await response.json());
|
|
1083
|
+
const record = body !== null && typeof body === "object" ? body : {};
|
|
1084
|
+
if (typeof record.image === "string") return {
|
|
1085
|
+
data: record.image,
|
|
1086
|
+
mimeType: imageMediaTypeOf(record.image)
|
|
1087
|
+
};
|
|
1088
|
+
throw new Error("The image model returned JSON without an image field.");
|
|
1089
|
+
}
|
|
1090
|
+
const data = bytesToBase64(new Uint8Array(await response.arrayBuffer()));
|
|
1091
|
+
return {
|
|
1092
|
+
data,
|
|
1093
|
+
mimeType: contentType.split(";")[0] || imageMediaTypeOf(data)
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Generates images with a Cloudflare catalog model. Text input parts join into
|
|
1098
|
+
* the prompt; the first image part, when present, is passed as the source
|
|
1099
|
+
* image for image-to-image models. Failures are reported through
|
|
1100
|
+
* `stopReason`/`errorMessage`, as pi-ai's images contract expects.
|
|
1101
|
+
*/
|
|
1102
|
+
async function generateImages(config, model, context, options) {
|
|
1103
|
+
const modelOptions = imageOptionsOf(model);
|
|
1104
|
+
const resolved = resolveOptions(config.providerGateway, modelOptions, void 0);
|
|
1105
|
+
const prompt = context.input.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
1106
|
+
const source = context.input.find((part) => part.type === "image");
|
|
1107
|
+
const setting = (key) => options?.[key] !== void 0 ? options[key] : modelOptions?.[key];
|
|
1108
|
+
const built = buildImageInput(model.id, prompt, {
|
|
1109
|
+
...setting("width") === void 0 ? {} : { width: setting("width") },
|
|
1110
|
+
...setting("height") === void 0 ? {} : { height: setting("height") },
|
|
1111
|
+
...setting("steps") === void 0 ? {} : { steps: setting("steps") },
|
|
1112
|
+
...setting("guidance") === void 0 ? {} : { guidance: setting("guidance") },
|
|
1113
|
+
...setting("negativePrompt") === void 0 ? {} : { negativePrompt: setting("negativePrompt") },
|
|
1114
|
+
...setting("seed") === void 0 ? {} : { seed: setting("seed") },
|
|
1115
|
+
...source === void 0 ? {} : { sourceImage: true }
|
|
1116
|
+
});
|
|
1117
|
+
const input = built.input;
|
|
1118
|
+
const warnings = built.warnings;
|
|
1119
|
+
if (!built.minimal && source !== void 0 && source.type === "image") input.image = Array.from(base64ToBytes(source.data));
|
|
1120
|
+
const result = {
|
|
1121
|
+
api: model.api,
|
|
1122
|
+
model: model.id,
|
|
1123
|
+
output: [],
|
|
1124
|
+
provider: model.provider,
|
|
1125
|
+
stopReason: "stop",
|
|
1126
|
+
timestamp: Date.now(),
|
|
1127
|
+
usage: emptyUsage()
|
|
1128
|
+
};
|
|
1129
|
+
try {
|
|
1130
|
+
const response = await config.transport.run({
|
|
1131
|
+
gateway: resolved.gateway,
|
|
1132
|
+
headers: resolved.headers,
|
|
1133
|
+
input,
|
|
1134
|
+
model: model.id,
|
|
1135
|
+
signal: options?.signal
|
|
1136
|
+
});
|
|
1137
|
+
await assertOk(response, {
|
|
1138
|
+
model: model.id,
|
|
1139
|
+
requestBodyValues: input,
|
|
1140
|
+
url: config.transport.url
|
|
1141
|
+
});
|
|
1142
|
+
const image = await readImage(response);
|
|
1143
|
+
result.output.push({
|
|
1144
|
+
data: image.data,
|
|
1145
|
+
mimeType: image.mimeType,
|
|
1146
|
+
type: "image"
|
|
1147
|
+
});
|
|
1148
|
+
const requestId = response.headers.get("cf-aig-log-id");
|
|
1149
|
+
if (requestId !== null) result.responseId = requestId;
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
result.stopReason = options?.signal?.aborted ? "aborted" : "error";
|
|
1152
|
+
result.errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
1153
|
+
}
|
|
1154
|
+
if (warnings.length > 0) result.diagnostics = [{
|
|
1155
|
+
details: { warnings },
|
|
1156
|
+
timestamp: Date.now(),
|
|
1157
|
+
type: COMPAT_DIAGNOSTIC
|
|
1158
|
+
}];
|
|
1159
|
+
return result;
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/models/pi-ai/settings.ts
|
|
1163
|
+
/** The pi thinking levels, for reading a caller's explicit effort. */
|
|
1164
|
+
const THINKING_LEVELS = [
|
|
1165
|
+
"minimal",
|
|
1166
|
+
"low",
|
|
1167
|
+
"medium",
|
|
1168
|
+
"high",
|
|
1169
|
+
"xhigh"
|
|
1170
|
+
];
|
|
1171
|
+
function isThinkingLevel(value) {
|
|
1172
|
+
return typeof value === "string" && THINKING_LEVELS.includes(value);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Resolves a pi thinking level onto the effort a request carries.
|
|
1176
|
+
*
|
|
1177
|
+
* Workers AI is the one catalog with an effort scale of ours: its quirk table
|
|
1178
|
+
* declares three levels, so the run path collapses onto them. Every other
|
|
1179
|
+
* model keeps its own level name — the wire then applies the model's own
|
|
1180
|
+
* `thinkingLevelMap`, the way pi-ai's implementations do — because a table of
|
|
1181
|
+
* ours has no business deciding a vendor's efforts.
|
|
1182
|
+
*/
|
|
1183
|
+
function effortForLevel$1(model, level) {
|
|
1184
|
+
if (level === void 0 || !model.reasoning) return void 0;
|
|
1185
|
+
const clamped = clampThinkingLevel(model, level);
|
|
1186
|
+
if (clamped === "off") return void 0;
|
|
1187
|
+
if (model.api !== "cloudflare-ai") return clamped;
|
|
1188
|
+
switch (clamped) {
|
|
1189
|
+
case "minimal":
|
|
1190
|
+
case "low": return "low";
|
|
1191
|
+
case "medium": return "medium";
|
|
1192
|
+
case "high":
|
|
1193
|
+
case "xhigh":
|
|
1194
|
+
case "max": return "high";
|
|
1195
|
+
default: return clamped;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/** Scalars only: the gateway rejects nested metadata. */
|
|
1199
|
+
function scalarMetadata(metadata) {
|
|
1200
|
+
if (metadata === void 0) return void 0;
|
|
1201
|
+
const result = {};
|
|
1202
|
+
for (const [key, value] of Object.entries(metadata)) if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") result[key] = value;
|
|
1203
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1204
|
+
}
|
|
1205
|
+
/** pi headers allow `null` to suppress a default; those are dropped here. */
|
|
1206
|
+
function stringHeaders(headers) {
|
|
1207
|
+
if (headers === void 0) return void 0;
|
|
1208
|
+
const result = {};
|
|
1209
|
+
for (const [key, value] of Object.entries(headers)) if (typeof value === "string") result[key] = value;
|
|
1210
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1211
|
+
}
|
|
1212
|
+
/** The per-call layer of options, read off pi-ai's call options. */
|
|
1213
|
+
function callLayer(options) {
|
|
1214
|
+
const layer = {};
|
|
1215
|
+
const headers = stringHeaders(options.headers);
|
|
1216
|
+
if (headers !== void 0) layer.headers = headers;
|
|
1217
|
+
const metadata = scalarMetadata(options.metadata);
|
|
1218
|
+
if (metadata !== void 0) layer.metadata = metadata;
|
|
1219
|
+
if (options.sessionId !== void 0) layer.sessionAffinity = options.sessionId;
|
|
1220
|
+
return layer;
|
|
1221
|
+
}
|
|
1222
|
+
function toolChoiceOf(options) {
|
|
1223
|
+
const raw = options.toolChoice;
|
|
1224
|
+
if (raw === "auto" || raw === "none" || raw === "required") return raw;
|
|
1225
|
+
if (raw === "any") return "required";
|
|
1226
|
+
if (raw !== null && typeof raw === "object") {
|
|
1227
|
+
const record = raw;
|
|
1228
|
+
const name = typeof record.function?.name === "string" ? record.function.name : typeof record.name === "string" ? record.name : void 0;
|
|
1229
|
+
if (name !== void 0) return {
|
|
1230
|
+
function: { name },
|
|
1231
|
+
type: "function"
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function buildRequest(model, context, options, simple, config) {
|
|
1236
|
+
const modelOptions = optionsOf(model);
|
|
1237
|
+
const resolved = resolveOptions(config.providerGateway, modelOptions, callLayer(options));
|
|
1238
|
+
const headers = { ...resolved.headers };
|
|
1239
|
+
if (resolved.sessionAffinity !== void 0 && model.api === "cloudflare-ai") headers["x-session-affinity"] = resolved.sessionAffinity;
|
|
1240
|
+
const explicitEffort = options.reasoningEffort;
|
|
1241
|
+
const reasoningEffort = resolved.reasoningEffort !== void 0 && model.api === "cloudflare-ai" ? resolved.reasoningEffort : isThinkingLevel(explicitEffort) ? effortForLevel$1(model, explicitEffort) : explicitEffort === "off" ? void 0 : typeof explicitEffort === "string" ? explicitEffort : simple ? effortForLevel$1(model, options.reasoning) : void 0;
|
|
1242
|
+
return {
|
|
1243
|
+
context,
|
|
1244
|
+
fallback: modelOptions?.fallback ?? [],
|
|
1245
|
+
headers,
|
|
1246
|
+
model,
|
|
1247
|
+
options,
|
|
1248
|
+
reasoningEffort,
|
|
1249
|
+
resolved,
|
|
1250
|
+
simple,
|
|
1251
|
+
streamIdleTimeoutMs: modelOptions?.streamIdleTimeoutMs ?? 3e5,
|
|
1252
|
+
toolChoice: toolChoiceOf(options)
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
//#endregion
|
|
1256
|
+
//#region src/models/pi-ai/wires/anthropic.ts
|
|
1257
|
+
/**
|
|
1258
|
+
* Anthropic Messages wire: pi-ai's own Anthropic implementation, over AI
|
|
1259
|
+
* Gateway's universal endpoint.
|
|
1260
|
+
*
|
|
1261
|
+
* Everything about the wire — request shaping, thinking, tool use, stream
|
|
1262
|
+
* parsing, thinking-signature replay — is pi-ai's. It accepts a `client` whose
|
|
1263
|
+
* `messages.create(...).asResponse()` returns the raw HTTP response; the one
|
|
1264
|
+
* built here routes that call through the gateway instead of the Anthropic
|
|
1265
|
+
* SDK, and sets the vendor headers the SDK would otherwise have added.
|
|
1266
|
+
*
|
|
1267
|
+
* The model object is never rewritten: `api`, `provider` and `id` reach pi's
|
|
1268
|
+
* implementation exactly as they will be recorded on the assistant message, so
|
|
1269
|
+
* a later turn replaying a thinking block is recognised as the same model.
|
|
1270
|
+
*/
|
|
1271
|
+
/**
|
|
1272
|
+
* Maps a pi thinking level onto an Anthropic effort the way pi-ai's own
|
|
1273
|
+
* `streamSimple` does: the model's `thinkingLevelMap` first, then the level's
|
|
1274
|
+
* own name. The map is the model author's; nothing here decides it.
|
|
1275
|
+
*/
|
|
1276
|
+
function effortForLevel(model, level) {
|
|
1277
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
1278
|
+
if (typeof mapped === "string") return mapped;
|
|
1279
|
+
switch (level) {
|
|
1280
|
+
case "minimal":
|
|
1281
|
+
case "low": return "low";
|
|
1282
|
+
case "medium": return "medium";
|
|
1283
|
+
default: return "high";
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* pi-ai's `streamSimple` thinking rules, applied here because that entry point
|
|
1288
|
+
* insists on a credential and this transport has none. Which branch a model
|
|
1289
|
+
* takes is the model's own `compat.forceAdaptiveThinking`, not a rule of ours.
|
|
1290
|
+
*/
|
|
1291
|
+
function thinkingOptions(model, options) {
|
|
1292
|
+
const level = options.reasoning;
|
|
1293
|
+
if (level === void 0 || !model.reasoning) return { thinkingEnabled: false };
|
|
1294
|
+
if (model.compat?.forceAdaptiveThinking === true) return {
|
|
1295
|
+
effort: effortForLevel(model, level),
|
|
1296
|
+
thinkingEnabled: true
|
|
1297
|
+
};
|
|
1298
|
+
const { maxTokens, thinkingBudget } = adjustMaxTokensForThinking(options.maxTokens, model.maxTokens, level, options.thinkingBudgets);
|
|
1299
|
+
return {
|
|
1300
|
+
maxTokens,
|
|
1301
|
+
thinkingBudgetTokens: thinkingBudget,
|
|
1302
|
+
thinkingEnabled: true
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
/** Streams an Anthropic request through pi-ai's implementation. */
|
|
1306
|
+
function streamAnthropic(request, transport) {
|
|
1307
|
+
const { model, context, options } = request;
|
|
1308
|
+
const outer = createAssistantMessageEventStream();
|
|
1309
|
+
const placeholder = startMessage(model);
|
|
1310
|
+
let correlation;
|
|
1311
|
+
let transportError;
|
|
1312
|
+
const create = (params) => ({ async asResponse() {
|
|
1313
|
+
const { betas, ...vendorParams } = params;
|
|
1314
|
+
const body = {
|
|
1315
|
+
...vendorParams,
|
|
1316
|
+
model: wireModelId(model)
|
|
1317
|
+
};
|
|
1318
|
+
try {
|
|
1319
|
+
const response = await sendUniversal(request, transport, body, anthropicVendorHeaders(Array.isArray(betas) ? betas : []), (raw) => {
|
|
1320
|
+
correlation = correlationDetails(request, raw, transport);
|
|
1321
|
+
});
|
|
1322
|
+
await assertOk(response, {
|
|
1323
|
+
model: model.id,
|
|
1324
|
+
requestBodyValues: body,
|
|
1325
|
+
url: transport.url
|
|
1326
|
+
});
|
|
1327
|
+
if (response.body === null) return response;
|
|
1328
|
+
return new Response(withIdleDeadline(response.body, request.streamIdleTimeoutMs), response);
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
transportError = error;
|
|
1331
|
+
throw error;
|
|
1332
|
+
}
|
|
1333
|
+
} });
|
|
1334
|
+
const client = {
|
|
1335
|
+
beta: { messages: { create } },
|
|
1336
|
+
messages: { create }
|
|
1337
|
+
};
|
|
1338
|
+
const anthropicOptions = {
|
|
1339
|
+
...options,
|
|
1340
|
+
...request.simple ? thinkingOptions(model, options) : {},
|
|
1341
|
+
client,
|
|
1342
|
+
...request.toolChoice !== void 0 ? { toolChoice: typeof request.toolChoice === "string" ? request.toolChoice === "required" ? "any" : request.toolChoice : {
|
|
1343
|
+
name: request.toolChoice.function.name,
|
|
1344
|
+
type: "tool"
|
|
1345
|
+
} } : {}
|
|
1346
|
+
};
|
|
1347
|
+
(async () => {
|
|
1348
|
+
try {
|
|
1349
|
+
const inner = anthropicMessagesApi().stream(model, context, anthropicOptions);
|
|
1350
|
+
for await (const event of inner) {
|
|
1351
|
+
if (event.type === "error" && transportError !== void 0) {
|
|
1352
|
+
failFrom(event.error);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (event.type === "done" || event.type === "error") {
|
|
1356
|
+
const message = event.type === "done" ? event.message : event.error;
|
|
1357
|
+
if (correlation !== void 0) attachCorrelation(message, correlation);
|
|
1358
|
+
recordWarnings(message, ignoredWorkersAIKnobs(request));
|
|
1359
|
+
}
|
|
1360
|
+
outer.push(event);
|
|
1361
|
+
}
|
|
1362
|
+
if (transportError !== void 0) {
|
|
1363
|
+
failFrom(placeholder);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
outer.end(await inner.result());
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
failStream(outer, placeholder, transportError ?? error, options.signal);
|
|
1369
|
+
}
|
|
1370
|
+
})();
|
|
1371
|
+
/** Ends the stream with the transport's own error, keeping any content. */
|
|
1372
|
+
function failFrom(message) {
|
|
1373
|
+
if (correlation !== void 0) attachCorrelation(message, correlation);
|
|
1374
|
+
failStream(outer, message, transportError, options.signal);
|
|
1375
|
+
}
|
|
1376
|
+
return outer;
|
|
1377
|
+
}
|
|
1378
|
+
//#endregion
|
|
1379
|
+
//#region src/models/pi-ai/wires/responses.ts
|
|
1380
|
+
/**
|
|
1381
|
+
* OpenAI Responses wire: pi-ai's own Responses converters and stream
|
|
1382
|
+
* processor, over AI Gateway's universal endpoint.
|
|
1383
|
+
*
|
|
1384
|
+
* Only the transport differs from talking to OpenAI directly: the body goes to
|
|
1385
|
+
* the gateway, which forwards it to `api.openai.com/v1/responses` with the key
|
|
1386
|
+
* it holds, and the raw SSE `Response` comes back. The model object is passed
|
|
1387
|
+
* through untouched, so the encrypted reasoning items pi replays on a later
|
|
1388
|
+
* turn are recognised as the same model's.
|
|
1389
|
+
*/
|
|
1390
|
+
/**
|
|
1391
|
+
* Providers whose tool-call ids carry OpenAI `call|item` pairing across
|
|
1392
|
+
* turns. This provider sanitizes ids instead, like pi-ai's own gateway
|
|
1393
|
+
* provider, so the set is empty.
|
|
1394
|
+
*/
|
|
1395
|
+
const TOOL_CALL_ID_PROVIDERS = /* @__PURE__ */ new Set();
|
|
1396
|
+
const TERMINAL_EVENTS = /* @__PURE__ */ new Set([
|
|
1397
|
+
"response.completed",
|
|
1398
|
+
"response.incomplete",
|
|
1399
|
+
"response.failed"
|
|
1400
|
+
]);
|
|
1401
|
+
/** A level through the model's own map, or the level's own name. */
|
|
1402
|
+
function mappedEffort(model, effort) {
|
|
1403
|
+
return model.thinkingLevelMap?.[effort] ?? effort;
|
|
1404
|
+
}
|
|
1405
|
+
/** Streams a Responses request and maps it onto pi-ai events. */
|
|
1406
|
+
function streamResponses(request, transport) {
|
|
1407
|
+
const { model, context, options, resolved } = request;
|
|
1408
|
+
const stream = createAssistantMessageEventStream();
|
|
1409
|
+
const output = startMessage(model);
|
|
1410
|
+
const observed = {
|
|
1411
|
+
endedCleanly: false,
|
|
1412
|
+
sawTerminalEvent: false
|
|
1413
|
+
};
|
|
1414
|
+
(async () => {
|
|
1415
|
+
let response;
|
|
1416
|
+
try {
|
|
1417
|
+
const responsesModel = model;
|
|
1418
|
+
const body = {
|
|
1419
|
+
input: convertResponsesMessages(responsesModel, context, TOOL_CALL_ID_PROVIDERS),
|
|
1420
|
+
model: wireModelId(model),
|
|
1421
|
+
store: false,
|
|
1422
|
+
stream: true
|
|
1423
|
+
};
|
|
1424
|
+
const tools = getCurrentTools(context.messages);
|
|
1425
|
+
if (tools.length > 0) body.tools = convertResponsesTools(tools);
|
|
1426
|
+
if (request.toolChoice !== void 0) body.tool_choice = typeof request.toolChoice === "string" ? request.toolChoice : {
|
|
1427
|
+
name: request.toolChoice.function.name,
|
|
1428
|
+
type: "function"
|
|
1429
|
+
};
|
|
1430
|
+
if (options.maxTokens !== void 0) body.max_output_tokens = Math.max(options.maxTokens, 16);
|
|
1431
|
+
if (options.temperature !== void 0) body.temperature = options.temperature;
|
|
1432
|
+
const cacheKey = clampOpenAIPromptCacheKey(resolved.sessionAffinity ?? options.sessionId);
|
|
1433
|
+
if (cacheKey !== void 0) body.prompt_cache_key = cacheKey;
|
|
1434
|
+
if (model.reasoning === true) {
|
|
1435
|
+
body.include = ["reasoning.encrypted_content"];
|
|
1436
|
+
const effort = request.reasoningEffort;
|
|
1437
|
+
if (typeof effort === "string") body.reasoning = {
|
|
1438
|
+
effort: mappedEffort(model, effort),
|
|
1439
|
+
summary: "auto"
|
|
1440
|
+
};
|
|
1441
|
+
else if (model.thinkingLevelMap?.off !== null) body.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
|
1442
|
+
}
|
|
1443
|
+
const sampling = options.samplingParams;
|
|
1444
|
+
if (sampling !== void 0) Object.assign(body, sampling);
|
|
1445
|
+
const overridden = await options.onPayload?.(body, model);
|
|
1446
|
+
const payload = overridden === void 0 ? body : overridden;
|
|
1447
|
+
response = await sendUniversal(request, transport, payload, openAIVendorHeaders());
|
|
1448
|
+
await options.onResponse?.(responseInfo(response), model);
|
|
1449
|
+
attachCorrelation(output, correlationDetails(request, response, transport));
|
|
1450
|
+
recordWarnings(output, ignoredWorkersAIKnobs(request));
|
|
1451
|
+
await assertOk(response, {
|
|
1452
|
+
model: model.id,
|
|
1453
|
+
requestBodyValues: payload,
|
|
1454
|
+
url: transport.url
|
|
1455
|
+
});
|
|
1456
|
+
if (response.body === null) throw new Error("The model returned an empty response body.");
|
|
1457
|
+
stream.push({
|
|
1458
|
+
partial: output,
|
|
1459
|
+
type: "start"
|
|
1460
|
+
});
|
|
1461
|
+
await processResponsesStream(observe(sseJson(withIdleDeadline(response.body, request.streamIdleTimeoutMs))), output, stream, responsesModel);
|
|
1462
|
+
if (options.signal?.aborted) throw new Error("Request was aborted.");
|
|
1463
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") throw new Error(output.errorMessage ?? "The model reported an error stop reason.");
|
|
1464
|
+
if (observed.endedCleanly && !observed.sawTerminalEvent) throw new Error("The stream ended without a terminal response event.");
|
|
1465
|
+
stream.push({
|
|
1466
|
+
message: output,
|
|
1467
|
+
reason: output.stopReason,
|
|
1468
|
+
type: "done"
|
|
1469
|
+
});
|
|
1470
|
+
stream.end(output);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
if (response?.body !== null && response?.body?.locked === false) response.body.cancel().catch(() => {});
|
|
1473
|
+
for (const block of output.content) {
|
|
1474
|
+
delete block.index;
|
|
1475
|
+
delete block.partialJson;
|
|
1476
|
+
delete block.customInput;
|
|
1477
|
+
}
|
|
1478
|
+
failStream(stream, output, error, options.signal);
|
|
1479
|
+
}
|
|
1480
|
+
})();
|
|
1481
|
+
return stream;
|
|
1482
|
+
async function* observe(events) {
|
|
1483
|
+
for await (const event of events) {
|
|
1484
|
+
const type = event?.type;
|
|
1485
|
+
if (typeof type === "string" && TERMINAL_EVENTS.has(type)) observed.sawTerminalEvent = true;
|
|
1486
|
+
yield event;
|
|
1487
|
+
}
|
|
1488
|
+
observed.endedCleanly = true;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
//#endregion
|
|
1492
|
+
//#region src/models/pi-ai/models/fallback.ts
|
|
1493
|
+
/**
|
|
1494
|
+
* Client-side model fallback for pi-ai streams.
|
|
1495
|
+
*
|
|
1496
|
+
* A leg is abandoned when its stream terminates with an `error` event before
|
|
1497
|
+
* it produced any content; the next model is tried with the same context and
|
|
1498
|
+
* options. Once a leg has emitted content it is committed to, and a later
|
|
1499
|
+
* error propagates. `start` and the block starts that may follow it carry no
|
|
1500
|
+
* content, so they are held back until a leg proves itself.
|
|
1501
|
+
*/
|
|
1502
|
+
/** Diagnostic type under which abandoned legs are recorded. */
|
|
1503
|
+
const FALLBACK_DIAGNOSTIC = "cloudflare-fallback";
|
|
1504
|
+
/**
|
|
1505
|
+
* Events that announce output without carrying any. A leg that emits only
|
|
1506
|
+
* these and then fails has produced nothing, so they are held back with
|
|
1507
|
+
* `start` until the leg proves itself.
|
|
1508
|
+
*/
|
|
1509
|
+
const PREAMBLE_EVENTS = /* @__PURE__ */ new Set([
|
|
1510
|
+
"start",
|
|
1511
|
+
"text_start",
|
|
1512
|
+
"thinking_start",
|
|
1513
|
+
"toolcall_start"
|
|
1514
|
+
]);
|
|
1515
|
+
function isAbandonable(event) {
|
|
1516
|
+
return event.type === "error" && event.reason === "error";
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Runs legs in order, committing to the first one that produces output.
|
|
1520
|
+
*
|
|
1521
|
+
* The returned stream always ends. A leg that throws, a stream that rejects,
|
|
1522
|
+
* a leg that starts answering and then ends without a `done` or `error`
|
|
1523
|
+
* event, and a last leg that ends without producing anything all end it
|
|
1524
|
+
* with an `error` event, so a caller awaiting the result is never left
|
|
1525
|
+
* waiting.
|
|
1526
|
+
*/
|
|
1527
|
+
function streamWithFallback(legs, signal) {
|
|
1528
|
+
const first = legs[0];
|
|
1529
|
+
if (first === void 0) throw new Error("streamWithFallback needs at least one leg.");
|
|
1530
|
+
const outer = createAssistantMessageEventStream();
|
|
1531
|
+
const attempts = [];
|
|
1532
|
+
let ended = false;
|
|
1533
|
+
const end = (message) => {
|
|
1534
|
+
ended = true;
|
|
1535
|
+
outer.end(message);
|
|
1536
|
+
};
|
|
1537
|
+
const recordAttempts = (message) => {
|
|
1538
|
+
message.diagnostics = [...message.diagnostics ?? [], {
|
|
1539
|
+
details: jsonDetails({ attempts }),
|
|
1540
|
+
timestamp: Date.now(),
|
|
1541
|
+
type: FALLBACK_DIAGNOSTIC
|
|
1542
|
+
}];
|
|
1543
|
+
};
|
|
1544
|
+
(async () => {
|
|
1545
|
+
let current = first;
|
|
1546
|
+
let lastError;
|
|
1547
|
+
try {
|
|
1548
|
+
for (const [index, leg] of legs.entries()) {
|
|
1549
|
+
current = leg;
|
|
1550
|
+
const isLast = index === legs.length - 1;
|
|
1551
|
+
const inner = leg.start();
|
|
1552
|
+
let pending = [];
|
|
1553
|
+
let committed = false;
|
|
1554
|
+
let finished = false;
|
|
1555
|
+
for await (const event of inner) {
|
|
1556
|
+
if (!committed && PREAMBLE_EVENTS.has(event.type)) {
|
|
1557
|
+
pending.push(event);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (!committed) {
|
|
1561
|
+
if (event.type === "error" && isAbandonable(event) && !isLast) {
|
|
1562
|
+
attempts.push({
|
|
1563
|
+
errorMessage: event.error.errorMessage,
|
|
1564
|
+
model: leg.model.id
|
|
1565
|
+
});
|
|
1566
|
+
lastError = event.error;
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
committed = true;
|
|
1570
|
+
for (const held of pending) outer.push(held);
|
|
1571
|
+
pending = [];
|
|
1572
|
+
}
|
|
1573
|
+
if ((event.type === "done" || event.type === "error") && attempts.length > 0) recordAttempts(event.type === "done" ? event.message : event.error);
|
|
1574
|
+
outer.push(event);
|
|
1575
|
+
if (event.type === "done" || event.type === "error") finished = true;
|
|
1576
|
+
}
|
|
1577
|
+
if (finished) {
|
|
1578
|
+
end(await inner.result());
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (committed) throw new Error(`${leg.model.id} ended its stream without a done or error event.`);
|
|
1582
|
+
}
|
|
1583
|
+
if (lastError !== void 0) {
|
|
1584
|
+
recordAttempts(lastError);
|
|
1585
|
+
outer.push({
|
|
1586
|
+
error: lastError,
|
|
1587
|
+
reason: "error",
|
|
1588
|
+
type: "error"
|
|
1589
|
+
});
|
|
1590
|
+
end(lastError);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
throw new Error(`${current.model.id} ended its stream without output.`);
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
if (ended) return;
|
|
1596
|
+
const message = startMessage(current.model);
|
|
1597
|
+
if (attempts.length > 0) recordAttempts(message);
|
|
1598
|
+
ended = true;
|
|
1599
|
+
failStream(outer, message, error, signal);
|
|
1600
|
+
}
|
|
1601
|
+
})();
|
|
1602
|
+
return outer;
|
|
1603
|
+
}
|
|
1604
|
+
//#endregion
|
|
1605
|
+
//#region src/models/pi-ai/models/language.ts
|
|
1606
|
+
/**
|
|
1607
|
+
* The pi-ai language dispatcher: picks the wire from the model's own `api`
|
|
1608
|
+
* marker and runs the request through the transport, with client-side fallback
|
|
1609
|
+
* across models.
|
|
1610
|
+
*
|
|
1611
|
+
* The `api` is the model author's declaration, not a guess of ours: Workers AI
|
|
1612
|
+
* models carry {@link CLOUDFLARE_AI_API} and run on `env.AI.run`; every other
|
|
1613
|
+
* model carries whatever protocol its registry says it speaks, and goes to AI
|
|
1614
|
+
* Gateway's universal endpoint.
|
|
1615
|
+
*/
|
|
1616
|
+
/** A stream that fails immediately, for a model this provider cannot route. */
|
|
1617
|
+
function failedStream(request, error) {
|
|
1618
|
+
const stream = createAssistantMessageEventStream();
|
|
1619
|
+
failStream(stream, startMessage(request.model), error, request.options.signal);
|
|
1620
|
+
return stream;
|
|
1621
|
+
}
|
|
1622
|
+
function dispatchOne(request, transport) {
|
|
1623
|
+
switch (request.model.api) {
|
|
1624
|
+
case CLOUDFLARE_AI_API:
|
|
1625
|
+
case "openai-completions": return streamCompletions(request, transport);
|
|
1626
|
+
case "anthropic-messages": return streamAnthropic(request, transport);
|
|
1627
|
+
case "openai-responses": return streamResponses(request, transport);
|
|
1628
|
+
default: return failedStream(request, /* @__PURE__ */ new TypeError(`agents/models/pi-ai cannot route the "${request.model.api}" API. AI Gateway serves anthropic-messages, openai-responses and openai-completions models; Workers AI models use "${CLOUDFLARE_AI_API}".`));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* The options a fallback leg inherits from the chain: the transport bag, and
|
|
1633
|
+
* nothing that describes a model.
|
|
1634
|
+
*
|
|
1635
|
+
* `name`, `cost`, `contextWindow`, `maxTokens`, `reasoning` and `input` were
|
|
1636
|
+
* written about the model they were written on — a leg has its own, from pi's
|
|
1637
|
+
* registry — and inheriting them bills the leg's answer at the primary's
|
|
1638
|
+
* prices. The leg's own `fallback` is dropped for the same reason `ai()`
|
|
1639
|
+
* expands a chain once: a leg is already part of one.
|
|
1640
|
+
*/
|
|
1641
|
+
function legOptions(options) {
|
|
1642
|
+
if (options === void 0) return void 0;
|
|
1643
|
+
const { fallback: _fallback, name: _name, cost: _cost, contextWindow: _contextWindow, maxTokens: _maxTokens, reasoning: _reasoning, input: _input, ...rest } = options;
|
|
1644
|
+
return rest;
|
|
1645
|
+
}
|
|
1646
|
+
/** A leg may be given as a Workers AI id or as a model of any kind. */
|
|
1647
|
+
function legModel(leg, options) {
|
|
1648
|
+
if (typeof leg === "string") return leg.startsWith("@cf/") ? buildWorkersAIModel(leg, options) : buildGatewayModel(leg, options);
|
|
1649
|
+
const own = optionsOf(leg);
|
|
1650
|
+
const inherited = gatewayLayerOf(options);
|
|
1651
|
+
return withOptions(leg, mergeModelOptions(own, Object.keys(inherited).length > 0 ? inherited : void 0));
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Builds pi-ai's `stream` and `streamSimple` for one provider instance. Any
|
|
1655
|
+
* model reaches them — this provider's own, or one the caller built from their
|
|
1656
|
+
* registry and handed to `ai(model)` — and dispatches on its `api`.
|
|
1657
|
+
*/
|
|
1658
|
+
function createStreams(config) {
|
|
1659
|
+
const run = (model, context, options, simple) => {
|
|
1660
|
+
const callOptions = options ?? {};
|
|
1661
|
+
const primary = buildRequest(model, context, callOptions, simple, config);
|
|
1662
|
+
if (primary.fallback.length === 0) return dispatchOne(primary, config.transport);
|
|
1663
|
+
const shared = legOptions(optionsOf(model));
|
|
1664
|
+
return streamWithFallback([{
|
|
1665
|
+
model,
|
|
1666
|
+
start: () => dispatchOne(primary, config.transport)
|
|
1667
|
+
}, ...primary.fallback.map((leg) => {
|
|
1668
|
+
const legTarget = legModel(leg, shared);
|
|
1669
|
+
return {
|
|
1670
|
+
model: legTarget,
|
|
1671
|
+
start: () => dispatchOne(buildRequest(legTarget, context, callOptions, simple, config), config.transport)
|
|
1672
|
+
};
|
|
1673
|
+
})], callOptions.signal);
|
|
1674
|
+
};
|
|
1675
|
+
return {
|
|
1676
|
+
stream: (model, context, options) => run(model, context, options, false),
|
|
1677
|
+
streamSimple: (model, context, options) => run(model, context, options, true)
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
//#endregion
|
|
1681
|
+
//#region src/models/pi-ai/index.ts
|
|
1682
|
+
/**
|
|
1683
|
+
* `agents/models/pi-ai` — Cloudflare's AI as a pi-ai provider, with the same
|
|
1684
|
+
* shape as `agents/models/ai-sdk`.
|
|
1685
|
+
*
|
|
1686
|
+
* One rule decides everything here: **Workers AI is the catalog Cloudflare
|
|
1687
|
+
* keeps; every other vendor is pi-ai's.** So there are two call forms, and no
|
|
1688
|
+
* third:
|
|
1689
|
+
*
|
|
1690
|
+
* ```ts
|
|
1691
|
+
* import { createAI } from "agents/models/pi-ai";
|
|
1692
|
+
* import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all";
|
|
1693
|
+
*
|
|
1694
|
+
* const ai = createAI({ binding: env.AI, id: "prod" });
|
|
1695
|
+
*
|
|
1696
|
+
* ai("@cf/zai-org/glm-4.7-flash"); // Workers AI: ours
|
|
1697
|
+
* ai(getBuiltinModel("anthropic", "claude-opus-4-8")); // theirs, our transport
|
|
1698
|
+
* ai("anthropic/claude-opus-4-8"); // same, via pi's catalog
|
|
1699
|
+
* ```
|
|
1700
|
+
*
|
|
1701
|
+
* Workers AI runs on `env.AI.run` with Cloudflare's own compat layer. Every
|
|
1702
|
+
* other model goes to AI Gateway's universal endpoint, which forwards the
|
|
1703
|
+
* vendor's own request to the vendor with the key the gateway holds — so no
|
|
1704
|
+
* provider key, and no vendor wire format, lives in your Worker. The model's
|
|
1705
|
+
* metadata (`api`, `compat`, `thinkingLevelMap`, cost, limits) is whatever its
|
|
1706
|
+
* own registry says, and pi-ai's converters and stream processors do the rest.
|
|
1707
|
+
*
|
|
1708
|
+
* @beta Everything exported here is in beta: the surface may
|
|
1709
|
+
* change in a minor release while the design settles.
|
|
1710
|
+
*
|
|
1711
|
+
* @module
|
|
1712
|
+
*/
|
|
1713
|
+
/** The prefixes a bare vendor id is most often written with, for the error. */
|
|
1714
|
+
const VENDOR_HINTS = {
|
|
1715
|
+
anthropic: "anthropic",
|
|
1716
|
+
google: "google",
|
|
1717
|
+
openai: "openai"
|
|
1718
|
+
};
|
|
1719
|
+
/**
|
|
1720
|
+
* A string that is neither a Workers AI id nor a `<slug>/<id>` this provider
|
|
1721
|
+
* can resolve. There is no guessing here by design: a model we cannot name
|
|
1722
|
+
* exactly is the caller's to construct.
|
|
1723
|
+
*/
|
|
1724
|
+
function unroutableString(specifier) {
|
|
1725
|
+
const hint = VENDOR_HINTS[specifier.split("/")[0] ?? ""];
|
|
1726
|
+
return /* @__PURE__ */ new TypeError(`ai("${specifier}") is not a model this provider knows. Workers AI ids start with "@cf/". A third-party model comes from its own pi-ai registry: ai(getBuiltinModel(${hint === void 0 ? "\"<provider>\"" : `"${hint}"`}, "<model id>")) from "@earendil-works/pi-ai/providers/all", or any Model object you built yourself.`);
|
|
1727
|
+
}
|
|
1728
|
+
function buildModel(target, options) {
|
|
1729
|
+
for (const leg of options?.fallback ?? []) {
|
|
1730
|
+
if (typeof leg !== "string" || leg.startsWith("@cf/")) continue;
|
|
1731
|
+
if (!leg.includes("/")) throw unroutableString(leg);
|
|
1732
|
+
buildGatewayModel(leg, void 0);
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof target !== "string") return withOptions(target, options);
|
|
1735
|
+
if (target.startsWith("@cf/")) return buildWorkersAIModel(target, options);
|
|
1736
|
+
if (!target.includes("/")) throw unroutableString(target);
|
|
1737
|
+
return buildGatewayModel(target, options);
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Creates a pi-ai provider over Cloudflare's AI.
|
|
1741
|
+
*
|
|
1742
|
+
* The Workers AI binding is the only way in — Workers AI models go through
|
|
1743
|
+
* `env.AI.run`, vendor models through `env.AI.gateway(id).run`, and there is
|
|
1744
|
+
* no HTTP transport and no API token. Gateway options may be given flat
|
|
1745
|
+
* (`{ binding, id: "prod", cacheTtl: 60 }`) or nested under `gateway`;
|
|
1746
|
+
* per-model options and per-call pi-ai options (`headers`, `metadata`,
|
|
1747
|
+
* `sessionId`) override them in that order.
|
|
1748
|
+
*
|
|
1749
|
+
* ```ts
|
|
1750
|
+
* const ai = createAI({ binding: env.AI, id: "prod" });
|
|
1751
|
+
* ai("@cf/zai-org/glm-4.7-flash");
|
|
1752
|
+
* ai(anthropicModel, { fallback: ["@cf/zai-org/glm-4.7-flash"] });
|
|
1753
|
+
* new Agent({ streamFn: ai.streamFn, initialState: { model: ai(openaiModel) } });
|
|
1754
|
+
* ```
|
|
1755
|
+
*
|
|
1756
|
+
* @beta This surface is in beta and may change.
|
|
1757
|
+
*/
|
|
1758
|
+
function createAI(settings) {
|
|
1759
|
+
const transport = createTransport(settings);
|
|
1760
|
+
const streams = createStreams({
|
|
1761
|
+
providerGateway: settings,
|
|
1762
|
+
transport
|
|
1763
|
+
});
|
|
1764
|
+
const model = ((target, options) => buildModel(target, options));
|
|
1765
|
+
const provider = createProvider({
|
|
1766
|
+
api: streams,
|
|
1767
|
+
auth: { apiKey: {
|
|
1768
|
+
name: "Cloudflare (the AI binding)",
|
|
1769
|
+
resolve: async () => ({ auth: {} })
|
|
1770
|
+
} },
|
|
1771
|
+
id: CLOUDFLARE_PROVIDER_ID,
|
|
1772
|
+
models: [...workersAIModelIds().map((id) => buildWorkersAIModel(id, void 0)), ...gatewayModelIds().map((id) => buildGatewayModel(id, void 0))],
|
|
1773
|
+
name: "Cloudflare"
|
|
1774
|
+
});
|
|
1775
|
+
const stream = (target, context, options) => streams.stream(target, normalizeContext(context), options);
|
|
1776
|
+
const streamSimple = (target, context, options) => streams.streamSimple(target, normalizeContext(context), options);
|
|
1777
|
+
return Object.assign(model, {
|
|
1778
|
+
complete: (target, context, options) => stream(target, context, options).result(),
|
|
1779
|
+
completeSimple: (target, context, options) => streamSimple(target, context, options).result(),
|
|
1780
|
+
generateImages: (target, context, options) => generateImages({
|
|
1781
|
+
providerGateway: settings,
|
|
1782
|
+
transport
|
|
1783
|
+
}, target, context, options),
|
|
1784
|
+
images: (modelId, options) => buildImagesModel(modelId, options),
|
|
1785
|
+
model,
|
|
1786
|
+
provider,
|
|
1787
|
+
stream,
|
|
1788
|
+
streamFn: streamSimple,
|
|
1789
|
+
streamSimple
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
//#endregion
|
|
1793
|
+
export { CLOUDFLARE_AI_API, CLOUDFLARE_AI_IMAGES_API, CLOUDFLARE_DIAGNOSTIC, CLOUDFLARE_ERROR_DIAGNOSTIC, CLOUDFLARE_PROVIDER_ID, CloudflareAIError, FALLBACK_DIAGNOSTIC, GATEWAY_PROVIDER_NAMES, createAI };
|
|
1794
|
+
|
|
1795
|
+
//# sourceMappingURL=index.js.map
|