agents 0.28.0 → 0.29.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 (121) hide show
  1. package/dist/{agent-routing-QDcchcQP.d.ts → agent-routing-CDbpVjYq.d.ts} +18 -8
  2. package/dist/agent-routing.d.ts +1 -1
  3. package/dist/agent-tool-types.d.ts +1 -1
  4. package/dist/{agent-tools-DHL2XI7u.d.ts → agent-tools-C8DH8Yii.d.ts} +2 -2
  5. package/dist/agent-tools.d.ts +1 -1
  6. package/dist/{ai-ahruGw7e.d.ts → ai-iXw0uOGj.d.ts} +3 -3
  7. package/dist/{ai-sdk-Bdsf6mSA.js → ai-sdk-DRPh2rRa.js} +218 -132
  8. package/dist/ai-sdk-DRPh2rRa.js.map +1 -0
  9. package/dist/browser/ai.d.ts +1 -1
  10. package/dist/browser/ai.js +2 -2
  11. package/dist/browser/index.d.ts +8 -8
  12. package/dist/browser/index.js +3 -3
  13. package/dist/browser/tools/ai-sdk.d.ts +2 -2
  14. package/dist/browser/tools/ai-sdk.js +1 -1
  15. package/dist/browser/tools/pi.d.ts +1 -1
  16. package/dist/browser/tools/pi.js +3 -3
  17. package/dist/browser/tools/pi.js.map +1 -1
  18. package/dist/browser/tools/tanstack-ai.d.ts +2 -2
  19. package/dist/browser/tools/tanstack-ai.js +1 -1
  20. package/dist/{browser-CEdNlsaD.js → browser-DccPw_wW.js} +130 -79
  21. package/dist/browser-DccPw_wW.js.map +1 -0
  22. package/dist/{browser-D30VlpcA.d.ts → browser-DlXhSAdf.d.ts} +41 -37
  23. package/dist/{browser-tool-4iomavs_.js → browser-tool-B1mhifin.js} +3 -3
  24. package/dist/{browser-tool-4iomavs_.js.map → browser-tool-B1mhifin.js.map} +1 -1
  25. package/dist/{browser-tool-vtrPOV8e.d.ts → browser-tool-Cxjk5sgG.d.ts} +2 -2
  26. package/dist/chat/index.d.ts +143 -71
  27. package/dist/chat/index.js +269 -110
  28. package/dist/chat/index.js.map +1 -1
  29. package/dist/chat-sdk/index.d.ts +1 -1
  30. package/dist/chat-sdk/index.js +1 -1
  31. package/dist/cli.js +33 -33
  32. package/dist/{client-BGIQKJhX.d.ts → client-C27CtRZr.d.ts} +2 -2
  33. package/dist/client.d.ts +1 -1
  34. package/dist/{connector-DEXzVoaY.d.ts → connector-20DJ6p4k.d.ts} +2 -2
  35. package/dist/{connector-CtZylXdk.js → connector-DAXBOIIi.js} +2 -2
  36. package/dist/{connector-CtZylXdk.js.map → connector-DAXBOIIi.js.map} +1 -1
  37. package/dist/experimental/channels/web/index.js +1 -1
  38. package/dist/{handler-stateless-VvrWSAVA.js → handler-stateless-Bly_nSH3.js} +12 -3
  39. package/dist/handler-stateless-Bly_nSH3.js.map +1 -0
  40. package/dist/harness/container/index.js +1 -1
  41. package/dist/harness/container/index.js.map +1 -1
  42. package/dist/harness/opencode/index.d.ts +182 -6
  43. package/dist/harness/opencode/index.js +354 -23
  44. package/dist/harness/opencode/index.js.map +1 -1
  45. package/dist/harness/pi/index.d.ts +13 -5
  46. package/dist/harness/pi/index.js +8 -214
  47. package/dist/harness/pi/index.js.map +1 -1
  48. package/dist/{images-Ce9SCu3X.js → images-xaDv2Fqe.js} +662 -152
  49. package/dist/images-xaDv2Fqe.js.map +1 -0
  50. package/dist/index-DCxk3OSJ.d.ts +378 -0
  51. package/dist/{index-kbSUIer3.d.ts → index-iv9FTXX9.d.ts} +80 -112
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +1 -1
  54. package/dist/{input-schema-D2QAtHZH.js → input-schema-B0Etuo-o.js} +2 -2
  55. package/dist/{input-schema-D2QAtHZH.js.map → input-schema-B0Etuo-o.js.map} +1 -1
  56. package/dist/lifecycle/index.js +1 -1
  57. package/dist/{lifecycle-DGorpAmA.js → lifecycle-ClT4DmF2.js} +11 -3
  58. package/dist/lifecycle-ClT4DmF2.js.map +1 -0
  59. package/dist/mcp/client/index.d.ts +1 -1
  60. package/dist/mcp/index.d.ts +1 -1
  61. package/dist/mcp/index.js +17 -3
  62. package/dist/mcp/index.js.map +1 -1
  63. package/dist/mcp/server/index.js +1 -1
  64. package/dist/models/ai-sdk/index.d.ts +31 -326
  65. package/dist/models/ai-sdk/index.js +1 -1
  66. package/dist/models/opencode/index.d.ts +25 -13
  67. package/dist/models/opencode/index.js +16 -11
  68. package/dist/models/opencode/index.js.map +1 -1
  69. package/dist/models/pi-ai/index.d.ts +17 -13
  70. package/dist/models/pi-ai/index.js +89 -36
  71. package/dist/models/pi-ai/index.js.map +1 -1
  72. package/dist/react.d.ts +2 -2
  73. package/dist/routing/index.d.ts +1 -1
  74. package/dist/serializable.d.ts +1 -1
  75. package/dist/{session-store-DoPY12xM.d.ts → session-store-_IK6dV-w.d.ts} +14 -2
  76. package/dist/{session-store-Do4xkDcf.js → session-store-yvwq1f9x.js} +55 -2
  77. package/dist/session-store-yvwq1f9x.js.map +1 -0
  78. package/dist/{settings-C45hDliX.js → settings-Dk2il2Hf.js} +10 -4
  79. package/dist/settings-Dk2il2Hf.js.map +1 -0
  80. package/dist/skills/index.d.ts +22 -28
  81. package/dist/skills/index.js +183 -143
  82. package/dist/skills/index.js.map +1 -1
  83. package/dist/{src-CQxJFv4l.js → src-6lr7MtM_.js} +3 -3
  84. package/dist/src-6lr7MtM_.js.map +1 -0
  85. package/dist/sub-routing.d.ts +1 -1
  86. package/dist/{tool-cQRkwSZZ.js → tool-DkT_DJBm.js} +2 -2
  87. package/dist/tool-DkT_DJBm.js.map +1 -0
  88. package/dist/transport-CPh7YGiw.d.ts +623 -0
  89. package/dist/voice/index.d.ts +1 -1
  90. package/dist/web/fetch/tools/pi.js +2 -2
  91. package/dist/web/fetch/tools/pi.js.map +1 -1
  92. package/dist/web/search/index.js +1 -1
  93. package/dist/web/search/tools/ai-sdk.js +2 -2
  94. package/dist/web/search/tools/pi.js +3 -3
  95. package/dist/web/search/tools/pi.js.map +1 -1
  96. package/dist/web/search/tools/tanstack-ai.js +2 -2
  97. package/dist/websockets/index.js +1 -1
  98. package/dist/{websockets-C3zy8YR0.js → websockets-CqxnntmE.js} +2 -2
  99. package/dist/{websockets-C3zy8YR0.js.map → websockets-CqxnntmE.js.map} +1 -1
  100. package/dist/workflow-types.d.ts +1 -1
  101. package/dist/workflows.d.ts +1 -1
  102. package/dist/workflows.js +1 -1
  103. package/docs/browse-the-web.md +1 -1
  104. package/docs/chat-agents.md +19 -18
  105. package/docs/harnesses/pi.md +47 -18
  106. package/docs/mcp-client.md +1 -1
  107. package/docs/mcp-servers.md +1 -1
  108. package/docs/models-pi-ai.md +70 -17
  109. package/docs/models.md +219 -152
  110. package/package.json +14 -14
  111. package/dist/ai-sdk-Bdsf6mSA.js.map +0 -1
  112. package/dist/browser-CEdNlsaD.js.map +0 -1
  113. package/dist/handler-stateless-VvrWSAVA.js.map +0 -1
  114. package/dist/images-Ce9SCu3X.js.map +0 -1
  115. package/dist/lifecycle-DGorpAmA.js.map +0 -1
  116. package/dist/session-store-Do4xkDcf.js.map +0 -1
  117. package/dist/settings-C45hDliX.js.map +0 -1
  118. package/dist/settings-NgGwF7Qt.d.ts +0 -331
  119. package/dist/src-CQxJFv4l.js.map +0 -1
  120. package/dist/tool-cQRkwSZZ.js.map +0 -1
  121. package/dist/transport-k2mFrtL8.d.ts +0 -200
package/docs/models.md CHANGED
@@ -1,37 +1,40 @@
1
1
  # Models (Experimental)
2
2
 
3
- `agents/models/ai-sdk` is Cloudflare's AI SDK provider. One rule decides
4
- everything in it:
5
-
6
- > **Workers AI is the one catalog we keep up with. Every other vendor brings
7
- > its own provider, and we route it.**
8
-
9
- So there are two call forms, and no third:
3
+ `agents/models/ai-sdk` is Cloudflare's AI SDK provider. It takes two call
4
+ forms, through the `AI` binding or
5
+ [an account id and API token](#account-id-and-api-token):
10
6
 
11
7
  ```ts
12
- ai("@cf/zai-org/glm-4.7-flash"); // Workers AI — our model, our compat layer
13
- ai(anthropic("claude-opus-4-8")); // the vendor's model, our transport
8
+ // A string runs on env.AI.run.
9
+ ai("@cf/zai-org/glm-4.7-flash"); // Workers AI
10
+ ai("anthropic/claude-opus-4.8"); // the AI Gateway catalog
11
+ ai("dynamic/support"); // a dynamic route on your gateway
12
+
13
+ // A vendor's own model goes to AI Gateway's provider-native path.
14
+ ai(anthropic("claude-opus-4-8"));
14
15
  ```
15
16
 
16
- No vendor ids, wire formats, thinking rules or model metadata live in this
17
- package, and `agents` takes no vendor package as a dependency — not even an
18
- optional one. You build the model with the vendor's own `@ai-sdk/*` provider;
19
- we hand its request to AI Gateway and hand the answer straight back to it.
17
+ A string needs no vendor package and no vendor key: AI Gateway's catalog names
18
+ the model, picks the provider, converts the request, and bills through Unified
19
+ Billing or a key stored on the gateway. A model object keeps the vendor's own
20
+ request and parser, so every vendor feature its package supports works.
20
21
 
21
22
  ## The canon, in five lines
22
23
 
23
- 1. **`ai(string)` is a Workers AI id.** `@cf/<author>/<model>`, autocompleted
24
- from `@cloudflare/workers-types`. Any other string is a `TypeError` that
25
- tells you which provider package to install.
26
- 2. **`ai(model)` is any AI SDK v4 model**, routed through AI Gateway. Its
27
- provider builds the request, parses the response, and handles its own
28
- errors; only the transport is ours.
24
+ 1. **A string is Cloudflare's.** `@cf/<author>/<model>` is Workers AI,
25
+ autocompleted from `@cloudflare/workers-types`; `<author>/<model>` is the
26
+ AI Gateway catalog, in the gateway's spelling; `dynamic/<route>` is a
27
+ [dynamic route](#dynamic-routes). All three run on `env.AI.run`, and any
28
+ other string is a `TypeError` naming the three forms.
29
+ 2. **`ai(model)` is any AI SDK v4 model**, sent to AI Gateway's
30
+ [provider-native path](#model-objects). Its provider builds the request,
31
+ parses the response, and handles its own errors; only the transport is
32
+ ours.
29
33
  3. **The gateway is an option, not a product.** Caching, logging, metadata,
30
34
  timeouts and retries are options on the call; the `default` gateway is
31
35
  created for you on first use.
32
- 4. **Unified billing means no vendor key.** Anthropic and OpenAI answer with a
33
- placeholder key today; anything else needs a key stored on the gateway
34
- (BYOK).
36
+ 4. **Unified billing means no vendor key.** The gateway holds the credential:
37
+ Cloudflare's, or a key you store on the gateway (BYOK).
35
38
  5. One factory: `createAI` — text, embeddings, images, speech, transcription
36
39
  and reranking all come off it, and it is a full `ProviderV4`.
37
40
 
@@ -39,16 +42,15 @@ we hand its request to AI Gateway and hand the answer straight back to it.
39
42
 
40
43
  Requirements: `ai@^7` and `@ai-sdk/provider@^4` must both be resolvable — they
41
44
  are optional peers of `agents`, so install them alongside it. Add a vendor
42
- provider only if you want that vendor:
45
+ provider only if you want to pass that vendor's model objects:
43
46
 
44
47
  ```sh
45
48
  pnpm add ai @ai-sdk/provider
46
- pnpm add @ai-sdk/anthropic # only if you call Anthropic
49
+ pnpm add @ai-sdk/anthropic # only for ai(anthropic("…"))
47
50
  ```
48
51
 
49
52
  ```ts
50
53
  import { createAI } from "agents/models/ai-sdk";
51
- import { createAnthropic } from "@ai-sdk/anthropic";
52
54
  import { generateText } from "ai";
53
55
 
54
56
  export default {
@@ -56,20 +58,17 @@ export default {
56
58
  // Inside a Worker: keyless, using the AI binding.
57
59
  const ai = createAI({ binding: env.AI, id: "default", cacheTtl: 60 });
58
60
 
59
- // The gateway holds the real key, so this one is a placeholder.
60
- const anthropic = createAnthropic({ apiKey: "cloudflare" });
61
-
62
61
  const workersAI = await generateText({
63
62
  model: ai("@cf/zai-org/glm-4.7-flash"),
64
63
  prompt: "Say hello in three words"
65
64
  });
66
65
 
67
- const vendor = await generateText({
68
- model: ai(anthropic("claude-opus-4-8")),
66
+ const claude = await generateText({
67
+ model: ai("anthropic/claude-opus-4.8"),
69
68
  prompt: "Say hello in three words"
70
69
  });
71
70
 
72
- return Response.json({ workersAI: workersAI.text, vendor: vendor.text });
71
+ return Response.json({ workersAI: workersAI.text, claude: claude.text });
73
72
  }
74
73
  };
75
74
  ```
@@ -82,12 +81,12 @@ Your `wrangler.jsonc` needs the binding:
82
81
  }
83
82
  ```
84
83
 
85
- Streaming, tools, and structured output are ordinary AI SDK calls, on either
84
+ Streaming, tools, and structured output are ordinary AI SDK calls, on every
86
85
  form:
87
86
 
88
87
  ```ts
89
88
  const result = streamText({
90
- model: ai(anthropic("claude-opus-4-8")),
89
+ model: ai("openai/gpt-5.5"),
91
90
  prompt: "Write a haiku about durable objects"
92
91
  });
93
92
  return result.toTextStreamResponse();
@@ -104,6 +103,30 @@ const { embeddings } = await embedMany({
104
103
  });
105
104
  ```
106
105
 
106
+ ### Account id and API token
107
+
108
+ To use a gateway in another account, or to call from outside a Worker, pass
109
+ `accountId` and `apiToken` instead of `binding`:
110
+
111
+ ```ts
112
+ const ai = createAI({
113
+ accountId: "27b146402af2103944379f33841b6234",
114
+ apiToken: env.CLOUDFLARE_API_TOKEN,
115
+ gateway: "project-gateway"
116
+ });
117
+ ```
118
+
119
+ The call forms and options are the same, and the gateway still defaults to
120
+ `default`. A string goes to AI Gateway's
121
+ [REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/)
122
+ (`api.cloudflare.com/client/v4/accounts/{accountId}/ai/...`), which runs it on
123
+ `/run` as `env.AI.run` does. A model object goes to the provider-native path,
124
+ `gateway.ai.cloudflare.com/v1/{accountId}/{gateway}/{provider}/...`, with the
125
+ token as `cf-aig-authorization`. The token is sent to those two hosts only.
126
+
127
+ Create the token with **Workers AI Read** for strings and **AI Gateway Run**
128
+ for model objects.
129
+
107
130
  ## Registry and default provider
108
131
 
109
132
  `createAI` returns a full `ProviderV4`, so it drops into both places the AI SDK
@@ -115,7 +138,7 @@ import { createProviderRegistry, generateText } from "ai";
115
138
  // A registry: `cloudflare:` names this provider.
116
139
  const registry = createProviderRegistry({ cloudflare: ai });
117
140
  await generateText({
118
- model: registry.languageModel("cloudflare:@cf/zai-org/glm-4.7-flash"),
141
+ model: registry.languageModel("cloudflare:anthropic/claude-opus-4.8"),
119
142
  prompt: "Hi"
120
143
  });
121
144
 
@@ -124,99 +147,134 @@ globalThis.AI_SDK_DEFAULT_PROVIDER = createAI({ binding: env.AI });
124
147
  await generateText({ model: "@cf/zai-org/glm-4.7-flash", prompt: "Hi" });
125
148
  ```
126
149
 
127
- The string rule does not change in either place: a specifier is a `@cf/` id, so
128
- a vendor model is still a model object (`ai(anthropic("claude-opus-4-8"))`) and
129
- cannot be reached by a bare string.
130
-
131
150
  Every modality is on the registry too (`registry.imageModel`,
132
151
  `registry.textEmbeddingModel`, and so on), because each `ProviderV4` method is
133
152
  implemented. The default provider is process-wide: set it once, where you build
134
153
  the Worker's `env`-dependent state.
135
154
 
136
- ## Why a model object and not an id
155
+ ## Catalog models
156
+
157
+ A catalog id is `<author>/<model>` in the
158
+ [AI Gateway catalog](https://developers.cloudflare.com/ai/models/)'s own
159
+ spelling — `anthropic/claude-opus-4.8`, `openai/gpt-5.5`, `cloudflare/auto`. The spelling is the gateway's, not the vendor's:
160
+ `anthropic/claude-opus-4.8`, not `claude-opus-4-8`.
161
+
162
+ The request goes through `env.AI.run(id, body, { gateway, openaiEndpoint })`
163
+ to AI Gateway's `/run`. The body is chat completions and the answer comes back
164
+ as chat completions, whichever provider served it: `/run` converts to and from
165
+ the provider's format. The gateway owns the rest:
166
+
167
+ - **Credentials.** Unified Billing bills the model to your Cloudflare account,
168
+ so there is no vendor key. A provider key stored on the gateway (BYOK) is
169
+ used instead when one is configured.
170
+ - **Providers.** A catalog model can have several providers behind it; `/run`
171
+ picks one and retries across them.
172
+ - **Errors.** A failure — an unknown model, an unpaid account, an upstream
173
+ error — comes back as a `CloudflareAIError`, the same type as a Workers AI
174
+ failure.
175
+
176
+ The request is built as strict OpenAI. `reasoningEffort` travels as
177
+ `reasoning_effort`, the output cap as `max_completion_tokens`, and
178
+ `chatTemplateKwargs`, which only Workers AI takes, is dropped with a warning.
179
+
180
+ A streamed answer from a model whose native format is Anthropic Messages (the
181
+ Claude models) comes back from `/run` in Anthropic events rather than chat
182
+ completions. This provider reads those events as chat completions (text,
183
+ reasoning, tool calls, finish reason and usage), so the stream looks the same
184
+ whatever model served it. `byokAlias` picks a stored key by alias; it is sent
185
+ as `cf-aig-byok-alias`, which `/run` reads.
186
+
187
+ ## Model objects
188
+
189
+ A vendor's own AI SDK model goes to AI Gateway's **provider-native path**,
190
+ unchanged. `ai(model)` clones the model per call and swaps the `fetch` its
191
+ provider would have used for one that sends the same request (method,
192
+ headers, body) through the binding:
137
193
 
138
- An id is only useful if someone keeps its catalog current. We keep exactly one:
139
- Workers AI. Every other vendor already ships a maintained catalog inside its
140
- own `@ai-sdk/*` package — ids, wire format, thinking levels, tool shapes, error
141
- types — and that package is updated the day the vendor ships something. Copying
142
- any of it here would mean a second, slower copy that is wrong every time a
143
- vendor moves.
194
+ ```ts
195
+ env.AI.fetch(
196
+ `https://workers-binding.ai/ai-gateway/gateways/${gateway}/anthropic/v1/messages`,
197
+ { method, headers, body } // the vendor's own, plus cf-aig-* gateway headers
198
+ );
199
+ ```
144
200
 
145
- So `ai(model)` takes what the vendor built, clones it per call, swaps the
146
- `fetch` its provider would have used, and sends the very same bytes to AI
147
- Gateway. What comes back is the vendor's own response, parsed by the vendor's
148
- own code. Your model object is never mutated — two concurrent calls never see
149
- each other's transport.
201
+ The provider slug and path come from the request URL's host (`api.anthropic.com`
202
+ is `anthropic`, `api.openai.com` is `openai`, and so on for every gateway
203
+ provider). A host the gateway has no provider for is a `TypeError` naming it.
204
+ What comes back is the vendor's own response, parsed by the vendor's own code,
205
+ streams included. Your model object is never mutated.
150
206
 
151
207
  ```ts
208
+ import { createAnthropic } from "@ai-sdk/anthropic";
152
209
  import { createOpenAI } from "@ai-sdk/openai";
153
210
 
211
+ const anthropic = createAnthropic({ apiKey: "cloudflare" }); // placeholder
154
212
  const openai = createOpenAI({ apiKey: "cloudflare" });
155
213
 
156
- ai(openai.responses("gpt-5-mini")); // OpenAI Responses API, routed
157
- ai(anthropic("claude-opus-4-8")); // Anthropic Messages API, routed
214
+ ai(anthropic("claude-opus-4-8")); // Anthropic Messages, the vendor's own id
215
+ ai(openai("gpt-5-mini")); // OpenAI Responses
216
+ ai.routed(openai.transcription("whisper-1")); // multipart bodies travel too
158
217
  ```
159
218
 
160
- Passing a third-party id as a string is an error, on purpose:
219
+ The vendor's id is the vendor's spelling (`claude-opus-4-8`), because its
220
+ package picks capabilities from it. Gateway options travel as `cf-aig-*`
221
+ headers. A model whose provider hides its settings (one already wrapped in
222
+ middleware, or hand-written) is a `TypeError`; wrap first, then apply
223
+ middleware:
161
224
 
162
225
  ```ts
163
- ai("anthropic/claude-opus-4-8");
164
- // TypeError: "anthropic/claude-opus-4-8" is not a Workers AI model id.
165
- // Workers AI ids start with "@cf/" … install @ai-sdk/anthropic and pass the
166
- // model object: ai(anthropic("claude-opus-4-8")).
226
+ wrapLanguageModel({ model: ai(anthropic("claude-opus-4-8")), middleware });
167
227
  ```
168
228
 
169
- A model whose provider hides its settings — one already wrapped in middleware,
170
- or hand-written — is an error too. Wrap first, then apply middleware:
229
+ ### Unified billing and BYOK
230
+
231
+ The vendor's credential header (`authorization`, `x-api-key`,
232
+ `x-goog-api-key`, `api-key`) is stripped by default, so the gateway supplies
233
+ the key: Unified Billing, or a key stored on the gateway. Unified Billing needs
234
+ authentication turned on for the gateway.
235
+
236
+ To use a key you hold yourself, give it to the vendor's provider and set
237
+ `vendorKey: "request"`, which forwards the credential instead of stripping it:
171
238
 
172
239
  ```ts
173
- wrapLanguageModel({ model: ai(anthropic("claude-opus-4-8")), middleware });
240
+ const anthropic = createAnthropic({ apiKey: env.ANTHROPIC_API_KEY });
241
+ const model = ai(anthropic("claude-opus-4-8"), { vendorKey: "request" });
174
242
  ```
175
243
 
176
- ## The universal gateway request
244
+ `vendorKey` belongs to the model it is set on. A fallback leg does not inherit
245
+ it, so a chain can mix a model on your key with one on Unified Billing.
246
+
247
+ `byokAlias` picks a stored key by alias when the gateway holds several for one
248
+ provider, and `zdr` turns Zero Data Retention on (`true`) or off (`false`) for
249
+ a Unified Billing request, whatever the gateway's own setting. On a model
250
+ object both travel as headers (`cf-aig-byok-alias`, `cf-aig-zdr`). `zdr`
251
+ applies to model objects only: `/run` does not read it.
252
+
253
+ A vendor's own error, such as a bad model id or a rejected parameter, stays the
254
+ vendor's, raised by its provider in its own error type. Only the gateway's own
255
+ failures, such as no such gateway or an unpaid account, become a
256
+ `CloudflareAIError`.
177
257
 
178
- A routed vendor model does not go down the Workers AI run path. It goes through
179
- AI Gateway's **universal request**, which carries the vendor's own body
180
- untouched:
258
+ ## Dynamic routes
259
+
260
+ An [AI Gateway dynamic route](https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/)
261
+ is called by name, as `dynamic/<route>`. The route configured on the gateway
262
+ picks the model (conditions, percentages, rate and budget limits, its own
263
+ fallbacks), and the call goes through `env.AI.run` like a Workers AI model:
181
264
 
182
265
  ```ts
183
- env.AI.gateway(gatewayId).run(
184
- { provider: "anthropic", endpoint: "v1/messages", headers, query },
185
- { gateway: { cacheTtl, skipCache, metadata, … }, extraHeaders, signal }
186
- );
266
+ const ai = createAI({ binding: env.AI, id: "prod" });
267
+
268
+ await generateText({ model: ai("dynamic/support"), prompt: "Hi" });
269
+ ai("@cf/zai-org/glm-4.7-flash", { fallback: ["dynamic/support"] });
187
270
  ```
188
271
 
189
- - `provider` comes from the request URL's host (`api.anthropic.com` →
190
- `anthropic`, `api.openai.com` → `openai`, and so on for every gateway
191
- provider). A host the gateway has no provider for is a `TypeError` naming it.
192
- - `endpoint` is the path after the host (`v1/messages`, `v1/responses`).
193
- - `query` is the vendor's JSON body, verbatim — including its own model id, in
194
- the vendor's own spelling. `claude-opus-4.8` gets a 404 from Anthropic
195
- ("Did you mean claude-opus-4-8?"), because we do not rewrite ids.
196
- - The vendor's credential header (`authorization`, `x-api-key`,
197
- `x-goog-api-key`, `api-key`) is stripped; its protocol headers
198
- (`anthropic-version`, `anthropic-beta`, …) travel.
199
- - `stream: true` comes back as the vendor's own SSE. The gateway adds a small
200
- `"p"` padding field to each data event, which every vendor parser ignores.
201
-
202
- The universal request carries **JSON only**. A multipart body (a file upload,
203
- an audio transcription posted as `FormData`) cannot be expressed in it and
204
- raises a `TypeError` rather than being silently mangled.
205
-
206
- ## Unified billing and BYOK
207
-
208
- Whether a vendor answers without a key of yours is the gateway's business, not
209
- this package's:
210
-
211
- - **Unified billing** covers Anthropic and OpenAI today: a placeholder
212
- `apiKey` is enough, and Cloudflare bills you.
213
- - **Everything else needs BYOK** — a provider key stored on the gateway. Google
214
- AI Studio without one answers `403 "Method doesn't allow unregistered callers"`,
215
- and that error is surfaced to you exactly as Google wrote it, because Google's
216
- provider is the one that knows how to read it.
217
-
218
- Only the gateway's _own_ failures — no such gateway, an unpaid account — become
219
- a `CloudflareAIError`. A vendor error stays the vendor's.
272
+ The route belongs to a gateway, so set the gateway `id` it was created on. It
273
+ answers in the OpenAI chat-completions shape whatever model served it, so the
274
+ request is built as strict OpenAI: `chatTemplateKwargs` is dropped with a
275
+ warning, because the route may not land on Workers AI. A dynamic route is a
276
+ language model only, and the modality methods refuse it, including as a
277
+ fallback leg.
220
278
 
221
279
  ## Other modalities
222
280
 
@@ -249,18 +307,6 @@ const { rerankedDocuments } = await rerank({
249
307
  });
250
308
  ```
251
309
 
252
- A vendor's own embedding, image, speech or transcription model can be routed
253
- too, with `ai.routed(model)`: the same `fetch` swap, the same gateway options,
254
- and `providerMetadata.cloudflare` on the result.
255
-
256
- `ai.routed` takes the gateway options, `headers` and `sessionAffinity` — and
257
- those only. `fallback` is not among them (a leg for one of those modalities
258
- would have to be another vendor's model, which this provider cannot build), and
259
- neither are `reasoningEffort` and `chatTemplateKwargs`, which are Workers AI
260
- settings; passing one is a compile error rather than a silent drop. Sent per
261
- call under `providerOptions.cloudflare` instead, the two Workers AI knobs come
262
- back in the result's `warnings`.
263
-
264
310
  Bytes stay bytes and base64 stays base64: some models answer with a raw body
265
311
  (`image/png`, `audio/mpeg`, `audio/wav`) and some with JSON (`{ image }`,
266
312
  `{ audio }`), the content type decides, and nothing is converted on the way
@@ -286,23 +332,23 @@ a `compatibility` warning.
286
332
  The run path generates one image per call, so `maxImagesPerCall` is 1 and
287
333
  `generateImage` fans a larger `n` out into parallel calls.
288
334
 
289
- `@cf/deepgram/nova-3` and `@cf/deepgram/flux` are not supported: they want the
290
- audio as an object body (`audio: { body, contentType }`) that the JSON run path
291
- cannot carry, and every documented encoding of it answers `400` live. They
292
- throw a `CloudflareAIError` before any request is made, pointing at
293
- `@cf/openai/whisper-large-v3-turbo`.
335
+ `@cf/deepgram/nova-3` takes its audio as a raw body (`audio: { body,
336
+ contentType }`), and the provider sends it as one. Of the transcription options
337
+ it takes `language`; the whisper-only ones come back in `warnings`.
338
+ `@cf/deepgram/flux` takes websocket connections only, so it throws a
339
+ `CloudflareAIError` before any request is made.
294
340
 
295
341
  ## Options
296
342
 
297
- Options can be set in three places, on both call forms. Precedence is
298
- per-call > per-model > provider.
343
+ Options can be set in three places. Precedence is per-call > per-model >
344
+ provider.
299
345
 
300
346
  ```ts
301
347
  // Provider-wide
302
348
  const ai = createAI({ binding: env.AI, id: "prod", metadata: { app: "demo" } });
303
349
 
304
350
  // Per model
305
- const model = ai(anthropic("claude-opus-4-8"), { cacheTtl: 3600 });
351
+ const model = ai("anthropic/claude-opus-4.8", { cacheTtl: 3600 });
306
352
 
307
353
  // Per call
308
354
  await generateText({
@@ -326,7 +372,10 @@ await generateText({
326
372
  | `eventId` | `string` | Correlates several requests into one gateway event. |
327
373
  | `requestTimeoutMs` | `number` | Upstream request timeout. |
328
374
  | `retries` | `{ maxAttempts, retryDelayMs, backoff }` | Server-side retry policy for the upstream provider call. |
329
- | `fallback` | `(WorkersAIModelId \| LanguageModelV4)[]` | Models to try, in order, if the primary fails before producing output. |
375
+ | `byokAlias` | `string` | A stored BYOK key, by alias. Catalog models and model objects. |
376
+ | `zdr` | `boolean` | Zero Data Retention on or off (Unified Billing). Model objects only. |
377
+ | `vendorKey` | `"gateway" \| "request"` | Model objects only: forward the vendor key on the request. Per model. |
378
+ | `fallback` | `(CloudflareLanguageModelId \| LanguageModelV4)[]` | Models to try, in order, if the primary fails before producing output. |
330
379
  | `headers` | `Record<string, string>` | Extra request headers, merged last. |
331
380
  | `sessionAffinity` | `string` | Pin same-key requests to one replica for prefix-cache hits. |
332
381
  | `reasoningEffort` | `"low" \| "medium" \| "high" \| null` | Workers AI only: explicit effort; `null` turns reasoning off. |
@@ -337,28 +386,25 @@ The flat gateway keys are sugar: `{ cacheTtl: 60 }` and
337
386
  layers above and below them (`metadata`, `retries` and `headers` merge field by
338
387
  field).
339
388
 
340
- `fallback` takes model objects only on the language forms. On the modality
389
+ `fallback` takes model objects only on language models. On the modality
341
390
  methods (`ai.embedding`, `ai.image`, `ai.speech`, `ai.transcription`,
342
391
  `ai.reranking`) a leg is a Workers AI id, because those run on the Workers AI
343
392
  run path, which resolves a leg by id and nothing else.
344
393
 
345
- `reasoningEffort` and `chatTemplateKwargs` are Workers AI settings. On a routed
346
- vendor model, reasoning is the vendor's own call option — use the AI SDK's
347
- unified `reasoning`, or that provider's `providerOptions`, and the vendor's
348
- rules apply because the vendor's code is what runs.
394
+ `reasoningEffort` maps onto `reasoning_effort` for every model, and the AI
395
+ SDK's unified `reasoning` call option maps onto it when it is not set.
396
+ `chatTemplateKwargs` is a Workers AI setting: a catalog model or a dynamic
397
+ route drops it with a warning.
349
398
 
350
399
  ### `providerOptions.cloudflare`
351
400
 
352
- Anything in the table above can be passed per call under the `cloudflare` key.
353
- It is read for gateway options and then **removed** before the call reaches a
354
- routed vendor model, so the vendor never sees a key it does not understand:
401
+ Anything in the table above can be passed per call under the `cloudflare` key:
355
402
 
356
403
  ```ts
357
404
  await generateText({
358
- model: ai(anthropic("claude-opus-4-8")),
405
+ model: ai("anthropic/claude-opus-4.8"),
359
406
  prompt: "hi",
360
407
  providerOptions: {
361
- anthropic: { sendReasoning: true },
362
408
  cloudflare: { metadata: { userId: "u1" }, skipCache: true, cacheTtl: 300 }
363
409
  }
364
410
  });
@@ -372,28 +418,55 @@ then fails before its first output part — an `error` part, an error finish, or
372
418
  a broken body — in which case the failed leg's preamble is dropped and the
373
419
  answering leg's stream is delivered in its place. Once a leg has produced
374
420
  output it is the answer, failures and all: by then the caller already has part
375
- of it. Legs may be Workers AI ids or model objects, in any mix:
421
+ of it. A leg is any model id, or any AI SDK language model:
376
422
 
377
423
  ```ts
378
424
  const model = ai(anthropic("claude-opus-4-8"), {
379
- fallback: [ai("@cf/zai-org/glm-4.7-flash")]
425
+ fallback: ["openai/gpt-5.5", "@cf/zai-org/glm-4.7-flash"]
380
426
  });
381
427
  ```
382
428
 
429
+ A model object in a chain is routed like `ai(model)`, through the
430
+ provider-native path, so a chain can mix catalog ids, Workers AI and a vendor's
431
+ own models.
432
+
383
433
  A leg travels with the chain's gateway options: the chain's `id`, `cacheTtl`
384
434
  and the rest win over a leg's own, `metadata` merges field by field, and
385
435
  everything else a leg was built with — its `headers`, its Workers AI knobs —
386
- stays the leg's. Legs written as strings are Workers AI ids, in a chain exactly
387
- as in `ai("…")`: a vendor id there is the same `TypeError`, whether it was
388
- written on the model or per call under `providerOptions.cloudflare`.
436
+ stays the leg's, and so does its `vendorKey`. A leg written as a string follows the same rule as `ai("…")`:
437
+ anything that is not one of the three id forms is the same `TypeError`,
438
+ whether it was written on the model or per call under
439
+ `providerOptions.cloudflare`.
389
440
 
390
441
  `providerMetadata.cloudflare.model` reports the model that actually answered.
391
442
  When every leg fails, the thrown `CloudflareAIError` carries an `attempts`
392
443
  array of `{ model, error }` and the last error as its `cause`.
393
444
 
394
- This is a client-side loop today. AI Gateway also accepts an **array** of
395
- universal requests as a server-side fallback chain; when we adopt it, the
396
- option shape will not change.
445
+ This loop runs across models. Within one catalog model, `/run` already retries
446
+ across that model's providers, and a [dynamic route](#dynamic-routes) runs its
447
+ own fallback on the gateway.
448
+
449
+ ## Stream resume
450
+
451
+ A streamed answer from AI Gateway's `/run` through the binding (a catalog
452
+ model or a dynamic route) carries `cf-aig-run-id`, and the gateway keeps every event of the run.
453
+ If the connection drops mid-stream, the provider reconnects through the
454
+ binding and replays the run from the last complete event, so the stream you
455
+ read continues with nothing lost or repeated. There is nothing to configure:
456
+
457
+ ```ts
458
+ const result = streamText({ model: ai("anthropic/claude-opus-4.8"), prompt });
459
+ // A dropped connection is resumed; result.textStream just keeps going.
460
+ ```
461
+
462
+ The run keeps going on the gateway after the connection is gone, so a resume
463
+ continues the same answer rather than starting a new one. It gives up, with a
464
+ `CloudflareAIError`, after five drops in one stream or once the gateway no
465
+ longer holds the run (a few minutes after it ends). Aborting the call never
466
+ resumes. The run id is in `providerMetadata.cloudflare.runId`.
467
+
468
+ Workers AI `@cf/` streams and model objects on the provider-native path have
469
+ no run id, so they are not resumed.
397
470
 
398
471
  ## How Workers AI stays OpenAI-shaped
399
472
 
@@ -418,20 +491,17 @@ is conformant needs no entry at all.
418
491
  ## Observability: `providerMetadata.cloudflare`
419
492
 
420
493
  Every result and every stream `finish` part carries typed gateway metadata, so
421
- you never have to walk response objects looking for a log id. On a routed
422
- vendor model it is merged alongside the vendor's own metadata block rather than
423
- replacing it:
494
+ you never have to walk response objects looking for a log id:
424
495
 
425
496
  ```ts
426
497
  const { providerMetadata } = await generateText({
427
- model: ai(anthropic("claude-opus-4-8")),
498
+ model: ai("anthropic/claude-opus-4.8"),
428
499
  prompt: "hi"
429
500
  });
430
501
 
431
502
  providerMetadata?.cloudflare;
432
503
  // {
433
- // model: "claude-opus-4-8",
434
- // provider: "anthropic",
504
+ // model: "anthropic/claude-opus-4.8",
435
505
  // gateway: "prod",
436
506
  // logId: "01M1KZWN069WWNPC18V05NKHSS",
437
507
  // eventId: "7bfd3660-9d0f-4bf7-bf9b-fa90b860456f",
@@ -440,14 +510,13 @@ providerMetadata?.cloudflare;
440
510
  // cacheStatus: "MISS",
441
511
  // step: 0
442
512
  // }
443
- providerMetadata?.anthropic; // still Anthropic's own
444
513
  ```
445
514
 
446
515
  | Field | Source header | Notes |
447
516
  | ------------- | --------------------- | ---------------------------------------------- |
448
517
  | `model` | — | The leg that answered, after any fallback. |
449
- | `gateway` | — | The gateway id the request went through. |
450
- | `provider` | — | The gateway provider slug, for a routed model. |
518
+ | `gateway` | — | The gateway id, when one handled the request. |
519
+ | `provider` | — | The gateway provider slug, for a model object. |
451
520
  | `logId` | `cf-aig-log-id` | Falls back to the binding's last log id. |
452
521
  | `eventId` | `cf-aig-event-id` | Correlates the requests of one gateway event. |
453
522
  | `requestId` | `cf-aig-request-id` | The gateway's id for this request. |
@@ -456,7 +525,7 @@ providerMetadata?.anthropic; // still Anthropic's own
456
525
  | `cacheStatus` | `cf-aig-cache-status` | `HIT` / `MISS`. |
457
526
  | `step` | `cf-aig-step` | Which step of a gateway route answered. |
458
527
 
459
- Every field except `model` and `gateway` is optional, and which ones arrive
528
+ Every field except `model` is optional, and which ones arrive
460
529
  depends on the path and the call. Read the fields you find rather than assuming
461
530
  any particular one is there. `response.headers` and `response.modelId` are
462
531
  populated too.
@@ -485,10 +554,8 @@ try {
485
554
  `code` is one of `"auth"`, `"rate-limit"`, `"not-found"`, `"bad-request"`,
486
555
  `"provider-error"`, `"gateway-error"`, `"unknown"`.
487
556
 
488
- On a routed vendor model this covers the gateway's own failures. A vendor's own
489
- error — a bad model id, a rejected parameter, a rate limit — is raised by that
490
- vendor's provider in its own error type, because it is the code that knows how
491
- to read it.
557
+ On a model object this covers the gateway's own failures; a vendor's own error
558
+ is raised by that vendor's provider in its own error type.
492
559
 
493
560
  ## Experimental
494
561
 
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "bin": {
13
13
  "agents": "./dist/cli.js"
14
14
  },
15
- "version": "0.28.0",
15
+ "version": "0.29.0",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "directory": "packages/agents",
@@ -42,14 +42,14 @@
42
42
  "@ai-sdk/provider": "^4.0.0",
43
43
  "@ai-sdk/react": "^4.0.0",
44
44
  "@ai-sdk/valibot": "3.0.6",
45
- "@cloudflare/codemode": "^0.5.3",
46
- "@earendil-works/pi-ai": "^1.0.0",
47
- "@earendil-works/pi-durable": "^1.0.0",
48
- "@earendil-works/pi-tui": "1.0.0",
49
- "@modelcontextprotocol/client": "2.0.0",
50
- "@modelcontextprotocol/conformance-v2": "npm:@modelcontextprotocol/conformance@0.2.0-alpha.10",
51
- "@modelcontextprotocol/sdk": "1.30.0",
52
- "@modelcontextprotocol/server": "2.0.0",
45
+ "@cloudflare/codemode": "^0.5.5",
46
+ "@earendil-works/pi-ai": "~1.3.0",
47
+ "@earendil-works/pi-durable": "~1.3.0",
48
+ "@earendil-works/pi-tui": "1.3.0",
49
+ "@modelcontextprotocol/client": "2.3.1",
50
+ "@modelcontextprotocol/conformance-v2": "npm:@modelcontextprotocol/conformance@0.2.0-alpha.12",
51
+ "@modelcontextprotocol/sdk": "1.32.1",
52
+ "@modelcontextprotocol/server": "2.3.1",
53
53
  "@opencode/plugin": "2.0.24",
54
54
  "@opencode/sdk": "2.0.24",
55
55
  "@tanstack/ai": "0.38.0",
@@ -71,11 +71,11 @@
71
71
  "@ai-sdk/provider": "^4.0.0",
72
72
  "@ai-sdk/react": "^3.0.0 || ^4.0.0",
73
73
  "@cloudflare/codemode": ">=0.5.0",
74
- "@earendil-works/pi-ai": "^1.0.0",
75
- "@earendil-works/pi-durable": "^1.0.0",
76
- "@modelcontextprotocol/client": "2.0.0",
77
- "@modelcontextprotocol/sdk": "1.30.0",
78
- "@modelcontextprotocol/server": "2.0.0",
74
+ "@earendil-works/pi-ai": "~1.3.0",
75
+ "@earendil-works/pi-durable": "~1.3.0",
76
+ "@modelcontextprotocol/client": "2.3.1",
77
+ "@modelcontextprotocol/sdk": "1.32.1",
78
+ "@modelcontextprotocol/server": "2.3.1",
79
79
  "@opencode/plugin": "^2.0.24",
80
80
  "@opencode/sdk": "^2.0.24",
81
81
  "@tanstack/ai": ">=0.10.2 <1.0.0",