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.
Files changed (170) hide show
  1. package/dist/{agent-routing-B2XLNMxq.d.ts → agent-routing-0DR0Tk2v.d.ts} +205 -132
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-routing.js.map +1 -1
  4. package/dist/agent-tool-types.d.ts +28 -22
  5. package/dist/agent-tool-types.js +23 -1
  6. package/dist/agent-tool-types.js.map +1 -1
  7. package/dist/{agent-tools-y7zLfw4Q.js → agent-tools-CdJC-8Dc.js} +72 -9
  8. package/dist/agent-tools-CdJC-8Dc.js.map +1 -0
  9. package/dist/{agent-tools-Dj5Mzlw7.d.ts → agent-tools-YrATZJu2.d.ts} +34 -20
  10. package/dist/agent-tools.d.ts +20 -18
  11. package/dist/agent-tools.js.map +1 -1
  12. package/dist/ai-fm7xx1BK.js +312 -0
  13. package/dist/ai-fm7xx1BK.js.map +1 -0
  14. package/dist/base64-redaction-C1Uu29t5.js +59 -0
  15. package/dist/base64-redaction-C1Uu29t5.js.map +1 -0
  16. package/dist/browser/ai-sdk.d.ts +90 -0
  17. package/dist/browser/ai-sdk.js +51 -0
  18. package/dist/browser/ai-sdk.js.map +1 -0
  19. package/dist/browser/ai.d.ts +4 -4
  20. package/dist/browser/ai.js +1 -347
  21. package/dist/browser/index.d.ts +81 -79
  22. package/dist/browser/index.js +361 -2
  23. package/dist/browser/index.js.map +1 -0
  24. package/dist/browser/tanstack-ai.d.ts +96 -1
  25. package/dist/browser/tanstack-ai.js +62 -2
  26. package/dist/browser/tanstack-ai.js.map +1 -1
  27. package/dist/browser-DE3d-RLk.d.ts +275 -0
  28. package/dist/browser-tool-CQ10e6Kj.js +480 -0
  29. package/dist/browser-tool-CQ10e6Kj.js.map +1 -0
  30. package/dist/browser-tool-DuNdlwZ9.d.ts +73 -0
  31. package/dist/capability-B4WbF81e.js.map +1 -1
  32. package/dist/{capability-runner-BUBa6Ake.d.ts → capability-runner-DBAnGr6L.d.ts} +7 -2
  33. package/dist/chat/index.d.ts +340 -234
  34. package/dist/chat/index.js +592 -66
  35. package/dist/chat/index.js.map +1 -1
  36. package/dist/chat/react.d.ts +42 -2
  37. package/dist/chat/react.js +191 -35
  38. package/dist/chat/react.js.map +1 -1
  39. package/dist/chat/transport.d.ts +1 -1
  40. package/dist/chat/transport.js +1 -1
  41. package/dist/chat-sdk/index.d.ts +17 -3
  42. package/dist/chat-sdk/index.js +28 -2
  43. package/dist/chat-sdk/index.js.map +1 -1
  44. package/dist/{client-C9DAjZIY.js → client-4OlMIJx-.js} +102 -29
  45. package/dist/client-4OlMIJx-.js.map +1 -0
  46. package/dist/{client-BxdkCuVy.d.ts → client-je3koD9Z.d.ts} +45 -10
  47. package/dist/client.d.ts +7 -5
  48. package/dist/client.js +2 -2
  49. package/dist/{connector-CptFKzRh.js → connector-58XDGvc3.js} +251 -120
  50. package/dist/connector-58XDGvc3.js.map +1 -0
  51. package/dist/{connector-CkQD4MK3.d.ts → connector-Cl-VqPmA.d.ts} +244 -146
  52. package/dist/context/index.d.ts +29 -0
  53. package/dist/context/index.js +64 -17
  54. package/dist/context/index.js.map +1 -1
  55. package/dist/current-agent-DhoDkSnH.js.map +1 -1
  56. package/dist/{current-agent-C4w86kcN.d.ts → current-agent-vAk_5JMt.d.ts} +4 -4
  57. package/dist/{durable-object-lifecycle-ei7M7lqB.d.ts → durable-object-lifecycle-HhgOMWq9.d.ts} +15 -7
  58. package/dist/harness/pi/index.d.ts +344 -0
  59. package/dist/harness/pi/index.js +873 -0
  60. package/dist/harness/pi/index.js.map +1 -0
  61. package/dist/images-iiTDSyRu.js +1631 -0
  62. package/dist/images-iiTDSyRu.js.map +1 -0
  63. package/dist/{index-Dlgtd3Mj.d.ts → index-BreL0YrQ.d.ts} +2 -2
  64. package/dist/index-kbSUIer3.d.ts +343 -0
  65. package/dist/index.d.ts +46 -44
  66. package/dist/index.js +1 -1
  67. package/dist/internal_context.d.ts +1 -1
  68. package/dist/lifecycle/index.d.ts +3 -3
  69. package/dist/lifecycle/index.js +1 -1
  70. package/dist/{lifecycle-Mm_jQh7r.js → lifecycle-CFu6OV03.js} +39 -10
  71. package/dist/{lifecycle-Mm_jQh7r.js.map → lifecycle-CFu6OV03.js.map} +1 -1
  72. package/dist/mcp/client/index.d.ts +20 -20
  73. package/dist/mcp/client/x402.js +21 -19
  74. package/dist/mcp/client/x402.js.map +1 -1
  75. package/dist/mcp/index.d.ts +35 -35
  76. package/dist/mcp/index.js +1 -1
  77. package/dist/{message-builder-BymO4N_D.js → message-builder-DAl9AhHH.js} +89 -4
  78. package/dist/message-builder-DAl9AhHH.js.map +1 -0
  79. package/dist/models/ai-sdk/index.d.ts +353 -0
  80. package/dist/models/ai-sdk/index.js +2443 -0
  81. package/dist/models/ai-sdk/index.js.map +1 -0
  82. package/dist/models/pi-ai/index.d.ts +313 -0
  83. package/dist/models/pi-ai/index.js +1795 -0
  84. package/dist/models/pi-ai/index.js.map +1 -0
  85. package/dist/observability/ai/index.js +20 -4
  86. package/dist/observability/ai/index.js.map +1 -1
  87. package/dist/{protocol-B0nh6KNf.js → protocol-kHsr-f0X.js} +26 -12
  88. package/dist/{protocol-B0nh6KNf.js.map → protocol-kHsr-f0X.js.map} +1 -1
  89. package/dist/queue/index.d.ts +3 -3
  90. package/dist/queue/index.js +1 -1
  91. package/dist/{queue-BC9F5cUo.js → queue-BB-zfIYF.js} +2 -2
  92. package/dist/{queue-BC9F5cUo.js.map → queue-BB-zfIYF.js.map} +1 -1
  93. package/dist/react.d.ts +6 -6
  94. package/dist/react.js +160 -64
  95. package/dist/react.js.map +1 -1
  96. package/dist/{retries-D9B2UCq3.d.ts → retries-CV6AkXmQ.d.ts} +18 -9
  97. package/dist/retries.d.ts +11 -9
  98. package/dist/retries.js +11 -1
  99. package/dist/retries.js.map +1 -1
  100. package/dist/routing/index.d.ts +2 -2
  101. package/dist/routing/index.js +1 -1
  102. package/dist/{scheduler-D_KHqxBa.js → scheduler-DZ3xQfeK.js} +2 -2
  103. package/dist/{scheduler-D_KHqxBa.js.map → scheduler-DZ3xQfeK.js.map} +1 -1
  104. package/dist/{scheduler-BGq6M5Kd.d.ts → scheduler-RdACNXDP.d.ts} +3 -3
  105. package/dist/schedules/index.d.ts +1 -1
  106. package/dist/schedules/index.js +1 -1
  107. package/dist/serializable.d.ts +1 -1
  108. package/dist/sessions/index.d.ts +70 -3
  109. package/dist/sessions/index.js +276 -67
  110. package/dist/sessions/index.js.map +1 -1
  111. package/dist/skills/index.d.ts +29 -314
  112. package/dist/socket-address.d.ts +16 -0
  113. package/dist/socket-address.js +19 -0
  114. package/dist/socket-address.js.map +1 -0
  115. package/dist/{src-BNU3ZiJM.js → src-BZeu-SCD.js} +423 -115
  116. package/dist/src-BZeu-SCD.js.map +1 -0
  117. package/dist/state/index.d.ts +1 -1
  118. package/dist/state/index.js +1 -1
  119. package/dist/streams/index.d.ts +1 -1
  120. package/dist/streams/index.js +1 -1
  121. package/dist/{streams-BcE8JP2X.d.ts → streams-Cmkj9QfW.d.ts} +4 -2
  122. package/dist/{streams-CCPRV6dt.js → streams-DW-UCOUc.js} +9 -2
  123. package/dist/streams-DW-UCOUc.js.map +1 -0
  124. package/dist/sub-routing.d.ts +12 -12
  125. package/dist/tasks/index.d.ts +1 -1
  126. package/dist/tasks/index.js +1 -1
  127. package/dist/{tasks-D4nLqVSI.js → tasks-68DESxf0.js} +2 -2
  128. package/dist/{tasks-D4nLqVSI.js.map → tasks-68DESxf0.js.map} +1 -1
  129. package/dist/{tasks-V2lCT8ZV.d.ts → tasks-9YxYtByh.d.ts} +2 -2
  130. package/dist/transport-PfFnt-Ld.d.ts +525 -0
  131. package/dist/{types-DCD6BNZH.d.ts → types-BA4anrjI.d.ts} +2 -2
  132. package/dist/voice/index.d.ts +2 -2
  133. package/dist/websockets/index.d.ts +23 -6
  134. package/dist/websockets/index.js +1 -1
  135. package/dist/{websockets-D7IzWbZb.js → websockets-Ci-LZFHH.js} +45 -20
  136. package/dist/{websockets-D7IzWbZb.js.map → websockets-Ci-LZFHH.js.map} +1 -1
  137. package/dist/wire-types-CnMt6_HR.js.map +1 -1
  138. package/dist/wire-types-CrjKvDyi.d.ts +300 -0
  139. package/dist/workflow-types.d.ts +25 -25
  140. package/dist/workflows.d.ts +21 -21
  141. package/dist/workflows.js +1 -1
  142. package/dist/{ws-chat-transport-UNRIS2xl.d.ts → ws-chat-transport-BbT1A5Lc.d.ts} +51 -2
  143. package/dist/{ws-chat-transport-rWwta645.js → ws-chat-transport-cqMKq3IY.js} +143 -16
  144. package/dist/ws-chat-transport-cqMKq3IY.js.map +1 -0
  145. package/docs/agent-tools.md +29 -4
  146. package/docs/browse-the-web.md +87 -5
  147. package/docs/chat-agents.md +124 -16
  148. package/docs/context.md +27 -0
  149. package/docs/durable-execution.md +2 -0
  150. package/docs/harnesses/pi.md +173 -0
  151. package/docs/human-in-the-loop.md +4 -0
  152. package/docs/index.md +3 -0
  153. package/docs/lifecycle.md +20 -4
  154. package/docs/models-pi-ai.md +325 -0
  155. package/docs/models.md +504 -0
  156. package/docs/resumable-streaming.md +2 -0
  157. package/docs/routing.md +13 -13
  158. package/docs/sessions.md +36 -1
  159. package/docs/state.md +1 -0
  160. package/docs/sub-agents.md +9 -0
  161. package/package.json +40 -2
  162. package/dist/agent-tools-y7zLfw4Q.js.map +0 -1
  163. package/dist/browser/ai.js.map +0 -1
  164. package/dist/client-C9DAjZIY.js.map +0 -1
  165. package/dist/client-tools-aIBO0Fk7.d.ts +0 -53
  166. package/dist/connector-CptFKzRh.js.map +0 -1
  167. package/dist/message-builder-BymO4N_D.js.map +0 -1
  168. package/dist/src-BNU3ZiJM.js.map +0 -1
  169. package/dist/streams-CCPRV6dt.js.map +0 -1
  170. 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