agents 0.28.1 → 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 (112) hide show
  1. package/dist/{agent-routing-BMvWTFEp.d.ts → agent-routing-CDbpVjYq.d.ts} +7 -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-ohoewW-B.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-C2iTAwUd.js → ai-sdk-DRPh2rRa.js} +185 -125
  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-Bp-JwL3p.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/harness/container/index.js +1 -1
  39. package/dist/harness/container/index.js.map +1 -1
  40. package/dist/harness/opencode/index.js +1 -1
  41. package/dist/harness/pi/index.d.ts +13 -5
  42. package/dist/harness/pi/index.js +8 -214
  43. package/dist/harness/pi/index.js.map +1 -1
  44. package/dist/{images-C4hiIexM.js → images-xaDv2Fqe.js} +636 -168
  45. package/dist/images-xaDv2Fqe.js.map +1 -0
  46. package/dist/index-DCxk3OSJ.d.ts +378 -0
  47. package/dist/{index-kbSUIer3.d.ts → index-iv9FTXX9.d.ts} +80 -112
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +1 -1
  50. package/dist/{input-schema-xBofjBKC.js → input-schema-B0Etuo-o.js} +2 -2
  51. package/dist/{input-schema-xBofjBKC.js.map → input-schema-B0Etuo-o.js.map} +1 -1
  52. package/dist/lifecycle/index.js +1 -1
  53. package/dist/{lifecycle-DGorpAmA.js → lifecycle-ClT4DmF2.js} +11 -3
  54. package/dist/lifecycle-ClT4DmF2.js.map +1 -0
  55. package/dist/mcp/client/index.d.ts +1 -1
  56. package/dist/mcp/index.d.ts +1 -1
  57. package/dist/mcp/index.js +1 -1
  58. package/dist/models/ai-sdk/index.d.ts +29 -352
  59. package/dist/models/ai-sdk/index.js +1 -1
  60. package/dist/models/opencode/index.d.ts +25 -13
  61. package/dist/models/opencode/index.js +16 -11
  62. package/dist/models/opencode/index.js.map +1 -1
  63. package/dist/models/pi-ai/index.d.ts +13 -14
  64. package/dist/models/pi-ai/index.js +85 -32
  65. package/dist/models/pi-ai/index.js.map +1 -1
  66. package/dist/react.d.ts +2 -2
  67. package/dist/routing/index.d.ts +1 -1
  68. package/dist/serializable.d.ts +1 -1
  69. package/dist/{session-store-DoPY12xM.d.ts → session-store-_IK6dV-w.d.ts} +14 -2
  70. package/dist/{session-store-Do4xkDcf.js → session-store-yvwq1f9x.js} +55 -2
  71. package/dist/session-store-yvwq1f9x.js.map +1 -0
  72. package/dist/{settings-BjAiPd6Z.js → settings-Dk2il2Hf.js} +4 -1
  73. package/dist/settings-Dk2il2Hf.js.map +1 -0
  74. package/dist/skills/index.d.ts +22 -28
  75. package/dist/skills/index.js +183 -143
  76. package/dist/skills/index.js.map +1 -1
  77. package/dist/{src-CQxJFv4l.js → src-6lr7MtM_.js} +3 -3
  78. package/dist/src-6lr7MtM_.js.map +1 -0
  79. package/dist/sub-routing.d.ts +1 -1
  80. package/dist/{tool-VF0qiO7q.js → tool-DkT_DJBm.js} +2 -2
  81. package/dist/tool-DkT_DJBm.js.map +1 -0
  82. package/dist/transport-CPh7YGiw.d.ts +623 -0
  83. package/dist/voice/index.d.ts +1 -1
  84. package/dist/web/fetch/tools/pi.js +2 -2
  85. package/dist/web/fetch/tools/pi.js.map +1 -1
  86. package/dist/web/search/index.js +1 -1
  87. package/dist/web/search/tools/ai-sdk.js +2 -2
  88. package/dist/web/search/tools/pi.js +3 -3
  89. package/dist/web/search/tools/pi.js.map +1 -1
  90. package/dist/web/search/tools/tanstack-ai.js +2 -2
  91. package/dist/websockets/index.js +1 -1
  92. package/dist/{websockets-C3zy8YR0.js → websockets-CqxnntmE.js} +2 -2
  93. package/dist/{websockets-C3zy8YR0.js.map → websockets-CqxnntmE.js.map} +1 -1
  94. package/dist/workflow-types.d.ts +1 -1
  95. package/dist/workflows.d.ts +1 -1
  96. package/dist/workflows.js +1 -1
  97. package/docs/browse-the-web.md +1 -1
  98. package/docs/chat-agents.md +19 -18
  99. package/docs/harnesses/pi.md +47 -18
  100. package/docs/models-pi-ai.md +62 -20
  101. package/docs/models.md +197 -172
  102. package/package.json +7 -7
  103. package/dist/ai-sdk-C2iTAwUd.js.map +0 -1
  104. package/dist/browser-CEdNlsaD.js.map +0 -1
  105. package/dist/images-C4hiIexM.js.map +0 -1
  106. package/dist/lifecycle-DGorpAmA.js.map +0 -1
  107. package/dist/session-store-Do4xkDcf.js.map +0 -1
  108. package/dist/settings-BjAiPd6Z.js.map +0 -1
  109. package/dist/settings-DnD__xuE.d.ts +0 -349
  110. package/dist/src-CQxJFv4l.js.map +0 -1
  111. package/dist/tool-VF0qiO7q.js.map +0 -1
  112. package/dist/transport-DCyarnwm.d.ts +0 -200
package/docs/models.md CHANGED
@@ -1,39 +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 Cloudflare's.** A Workers AI id, `@cf/<author>/<model>`,
24
- autocompleted from `@cloudflare/workers-types`, or a
25
- [dynamic route](#dynamic-routes) on your gateway, `dynamic/<route>`. Any
26
- other string is a `TypeError` that tells you which provider package to
27
- install.
28
- 2. **`ai(model)` is any AI SDK v4 model**, routed through AI Gateway. Its
29
- provider builds the request, parses the response, and handles its own
30
- 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.
31
33
  3. **The gateway is an option, not a product.** Caching, logging, metadata,
32
34
  timeouts and retries are options on the call; the `default` gateway is
33
35
  created for you on first use.
34
- 4. **Unified billing means no vendor key.** Anthropic and OpenAI answer with a
35
- placeholder key today; anything else needs a key stored on the gateway
36
- (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).
37
38
  5. One factory: `createAI` — text, embeddings, images, speech, transcription
38
39
  and reranking all come off it, and it is a full `ProviderV4`.
39
40
 
@@ -41,16 +42,15 @@ we hand its request to AI Gateway and hand the answer straight back to it.
41
42
 
42
43
  Requirements: `ai@^7` and `@ai-sdk/provider@^4` must both be resolvable — they
43
44
  are optional peers of `agents`, so install them alongside it. Add a vendor
44
- provider only if you want that vendor:
45
+ provider only if you want to pass that vendor's model objects:
45
46
 
46
47
  ```sh
47
48
  pnpm add ai @ai-sdk/provider
48
- pnpm add @ai-sdk/anthropic # only if you call Anthropic
49
+ pnpm add @ai-sdk/anthropic # only for ai(anthropic("…"))
49
50
  ```
50
51
 
51
52
  ```ts
52
53
  import { createAI } from "agents/models/ai-sdk";
53
- import { createAnthropic } from "@ai-sdk/anthropic";
54
54
  import { generateText } from "ai";
55
55
 
56
56
  export default {
@@ -58,20 +58,17 @@ export default {
58
58
  // Inside a Worker: keyless, using the AI binding.
59
59
  const ai = createAI({ binding: env.AI, id: "default", cacheTtl: 60 });
60
60
 
61
- // The gateway holds the real key, so this one is a placeholder.
62
- const anthropic = createAnthropic({ apiKey: "cloudflare" });
63
-
64
61
  const workersAI = await generateText({
65
62
  model: ai("@cf/zai-org/glm-4.7-flash"),
66
63
  prompt: "Say hello in three words"
67
64
  });
68
65
 
69
- const vendor = await generateText({
70
- model: ai(anthropic("claude-opus-4-8")),
66
+ const claude = await generateText({
67
+ model: ai("anthropic/claude-opus-4.8"),
71
68
  prompt: "Say hello in three words"
72
69
  });
73
70
 
74
- return Response.json({ workersAI: workersAI.text, vendor: vendor.text });
71
+ return Response.json({ workersAI: workersAI.text, claude: claude.text });
75
72
  }
76
73
  };
77
74
  ```
@@ -84,12 +81,12 @@ Your `wrangler.jsonc` needs the binding:
84
81
  }
85
82
  ```
86
83
 
87
- 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
88
85
  form:
89
86
 
90
87
  ```ts
91
88
  const result = streamText({
92
- model: ai(anthropic("claude-opus-4-8")),
89
+ model: ai("openai/gpt-5.5"),
93
90
  prompt: "Write a haiku about durable objects"
94
91
  });
95
92
  return result.toTextStreamResponse();
@@ -106,6 +103,30 @@ const { embeddings } = await embedMany({
106
103
  });
107
104
  ```
108
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
+
109
130
  ## Registry and default provider
110
131
 
111
132
  `createAI` returns a full `ProviderV4`, so it drops into both places the AI SDK
@@ -117,7 +138,7 @@ import { createProviderRegistry, generateText } from "ai";
117
138
  // A registry: `cloudflare:` names this provider.
118
139
  const registry = createProviderRegistry({ cloudflare: ai });
119
140
  await generateText({
120
- model: registry.languageModel("cloudflare:@cf/zai-org/glm-4.7-flash"),
141
+ model: registry.languageModel("cloudflare:anthropic/claude-opus-4.8"),
121
142
  prompt: "Hi"
122
143
  });
123
144
 
@@ -126,116 +147,113 @@ globalThis.AI_SDK_DEFAULT_PROVIDER = createAI({ binding: env.AI });
126
147
  await generateText({ model: "@cf/zai-org/glm-4.7-flash", prompt: "Hi" });
127
148
  ```
128
149
 
129
- The string rule does not change in either place: a specifier is a `@cf/` id, so
130
- a vendor model is still a model object (`ai(anthropic("claude-opus-4-8"))`) and
131
- cannot be reached by a bare string.
132
-
133
150
  Every modality is on the registry too (`registry.imageModel`,
134
151
  `registry.textEmbeddingModel`, and so on), because each `ProviderV4` method is
135
152
  implemented. The default provider is process-wide: set it once, where you build
136
153
  the Worker's `env`-dependent state.
137
154
 
138
- ## 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:
139
193
 
140
- An id is only useful if someone keeps its catalog current. We keep exactly one:
141
- Workers AI. Every other vendor already ships a maintained catalog inside its
142
- own `@ai-sdk/*` package — ids, wire format, thinking levels, tool shapes, error
143
- types — and that package is updated the day the vendor ships something. Copying
144
- any of it here would mean a second, slower copy that is wrong every time a
145
- 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
+ ```
146
200
 
147
- So `ai(model)` takes what the vendor built, clones it per call, swaps the
148
- `fetch` its provider would have used, and sends the very same bytes to AI
149
- Gateway. What comes back is the vendor's own response, parsed by the vendor's
150
- own code. Your model object is never mutated — two concurrent calls never see
151
- 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.
152
206
 
153
207
  ```ts
208
+ import { createAnthropic } from "@ai-sdk/anthropic";
154
209
  import { createOpenAI } from "@ai-sdk/openai";
155
210
 
211
+ const anthropic = createAnthropic({ apiKey: "cloudflare" }); // placeholder
156
212
  const openai = createOpenAI({ apiKey: "cloudflare" });
157
213
 
158
- ai(openai.responses("gpt-5-mini")); // OpenAI Responses API, routed
159
- 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
160
217
  ```
161
218
 
162
- Passing a third-party id as a string is an error, on purpose:
163
-
164
- ```ts
165
- ai("anthropic/claude-opus-4-8");
166
- // TypeError: "anthropic/claude-opus-4-8" is not a Workers AI model id.
167
- // Workers AI ids start with "@cf/" … install @ai-sdk/anthropic and pass the
168
- // model object: ai(anthropic("claude-opus-4-8")).
169
- ```
170
-
171
- A model whose provider hides its settings — one already wrapped in middleware,
172
- or hand-written — is an error too. Wrap first, then apply middleware:
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:
173
224
 
174
225
  ```ts
175
226
  wrapLanguageModel({ model: ai(anthropic("claude-opus-4-8")), middleware });
176
227
  ```
177
228
 
178
- ## The universal gateway request
229
+ ### Unified billing and BYOK
179
230
 
180
- A routed vendor model does not go down the Workers AI run path. It goes through
181
- AI Gateway's **universal request**, which carries the vendor's own body
182
- untouched:
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.
183
235
 
184
- ```ts
185
- env.AI.gateway(gatewayId).run(
186
- { provider: "anthropic", endpoint: "v1/messages", headers, query },
187
- { gateway: { cacheTtl, skipCache, metadata, … }, extraHeaders, signal }
188
- );
189
- ```
190
-
191
- - `provider` comes from the request URL's host (`api.anthropic.com` →
192
- `anthropic`, `api.openai.com` → `openai`, and so on for every gateway
193
- provider). A host the gateway has no provider for is a `TypeError` naming it.
194
- - `endpoint` is the path after the host (`v1/messages`, `v1/responses`).
195
- - `query` is the vendor's JSON body, verbatim — including its own model id, in
196
- the vendor's own spelling. `claude-opus-4.8` gets a 404 from Anthropic
197
- ("Did you mean claude-opus-4-8?"), because we do not rewrite ids.
198
- - The vendor's credential header (`authorization`, `x-api-key`,
199
- `x-goog-api-key`, `api-key`) is stripped; its protocol headers
200
- (`anthropic-version`, `anthropic-beta`, …) travel.
201
- - `stream: true` comes back as the vendor's own SSE. The gateway adds a small
202
- `"p"` padding field to each data event, which every vendor parser ignores.
203
-
204
- The universal request carries **JSON only**. A multipart body (a file upload,
205
- an audio transcription posted as `FormData`) cannot be expressed in it and
206
- raises a `TypeError` rather than being silently mangled.
207
-
208
- ## Unified billing and BYOK
209
-
210
- Whether a vendor answers without a key of yours is the gateway's business, not
211
- this package's:
212
-
213
- - **Unified billing** covers Anthropic and OpenAI today: a placeholder
214
- `apiKey` is enough, and Cloudflare bills you.
215
- - **Everything else needs BYOK** — a provider key stored on the gateway. Google
216
- AI Studio without one answers `403 "Method doesn't allow unregistered callers"`,
217
- and that error is surfaced to you exactly as Google wrote it, because Google's
218
- provider is the one that knows how to read it.
219
-
220
- `zdr` turns Zero Data Retention on (`true`) or off (`false`) for one request,
221
- whatever the gateway's own setting. It applies to a routed model on unified
222
- billing and is sent as `cf-aig-zdr`.
223
-
224
- To use a key you hold yourself rather than one stored on the gateway, give it
225
- to the vendor's provider and set `vendorKey: "request"`, which forwards the
226
- credential header instead of stripping it:
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:
227
238
 
228
239
  ```ts
229
240
  const anthropic = createAnthropic({ apiKey: env.ANTHROPIC_API_KEY });
230
241
  const model = ai(anthropic("claude-opus-4-8"), { vendorKey: "request" });
231
242
  ```
232
243
 
233
- `vendorKey` belongs to the model it is set on (`ai(model, …)` or
234
- `ai.routed(model, …)`). A fallback leg does not inherit it, so a chain can mix
235
- a model on your key with one on unified billing.
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.
236
246
 
237
- Only the gateway's _own_ failures — no such gateway, an unpaid account — become
238
- a `CloudflareAIError`. A vendor error stays the vendor's.
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`.
239
257
 
240
258
  ## Dynamic routes
241
259
 
@@ -289,18 +307,6 @@ const { rerankedDocuments } = await rerank({
289
307
  });
290
308
  ```
291
309
 
292
- A vendor's own embedding, image, speech or transcription model can be routed
293
- too, with `ai.routed(model)`: the same `fetch` swap, the same gateway options,
294
- and `providerMetadata.cloudflare` on the result.
295
-
296
- `ai.routed` takes the gateway options, `headers` and `sessionAffinity` — and
297
- those only. `fallback` is not among them (a leg for one of those modalities
298
- would have to be another vendor's model, which this provider cannot build), and
299
- neither are `reasoningEffort` and `chatTemplateKwargs`, which are Workers AI
300
- settings; passing one is a compile error rather than a silent drop. Sent per
301
- call under `providerOptions.cloudflare` instead, the two Workers AI knobs come
302
- back in the result's `warnings`.
303
-
304
310
  Bytes stay bytes and base64 stays base64: some models answer with a raw body
305
311
  (`image/png`, `audio/mpeg`, `audio/wav`) and some with JSON (`{ image }`,
306
312
  `{ audio }`), the content type decides, and nothing is converted on the way
@@ -326,23 +332,23 @@ a `compatibility` warning.
326
332
  The run path generates one image per call, so `maxImagesPerCall` is 1 and
327
333
  `generateImage` fans a larger `n` out into parallel calls.
328
334
 
329
- `@cf/deepgram/nova-3` and `@cf/deepgram/flux` are not supported: they want the
330
- audio as an object body (`audio: { body, contentType }`) that the JSON run path
331
- cannot carry, and every documented encoding of it answers `400` live. They
332
- throw a `CloudflareAIError` before any request is made, pointing at
333
- `@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.
334
340
 
335
341
  ## Options
336
342
 
337
- Options can be set in three places, on both call forms. Precedence is
338
- per-call > per-model > provider.
343
+ Options can be set in three places. Precedence is per-call > per-model >
344
+ provider.
339
345
 
340
346
  ```ts
341
347
  // Provider-wide
342
348
  const ai = createAI({ binding: env.AI, id: "prod", metadata: { app: "demo" } });
343
349
 
344
350
  // Per model
345
- const model = ai(anthropic("claude-opus-4-8"), { cacheTtl: 3600 });
351
+ const model = ai("anthropic/claude-opus-4.8", { cacheTtl: 3600 });
346
352
 
347
353
  // Per call
348
354
  await generateText({
@@ -366,9 +372,10 @@ await generateText({
366
372
  | `eventId` | `string` | Correlates several requests into one gateway event. |
367
373
  | `requestTimeoutMs` | `number` | Upstream request timeout. |
368
374
  | `retries` | `{ maxAttempts, retryDelayMs, backoff }` | Server-side retry policy for the upstream provider call. |
369
- | `zdr` | `boolean` | Routed models only: Zero Data Retention on or off (unified billing). |
370
- | `vendorKey` | `"gateway" \| "request"` | Routed models only: forward the vendor key on the request. Per model. |
371
- | `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. |
372
379
  | `headers` | `Record<string, string>` | Extra request headers, merged last. |
373
380
  | `sessionAffinity` | `string` | Pin same-key requests to one replica for prefix-cache hits. |
374
381
  | `reasoningEffort` | `"low" \| "medium" \| "high" \| null` | Workers AI only: explicit effort; `null` turns reasoning off. |
@@ -379,28 +386,25 @@ The flat gateway keys are sugar: `{ cacheTtl: 60 }` and
379
386
  layers above and below them (`metadata`, `retries` and `headers` merge field by
380
387
  field).
381
388
 
382
- `fallback` takes model objects only on the language forms. On the modality
389
+ `fallback` takes model objects only on language models. On the modality
383
390
  methods (`ai.embedding`, `ai.image`, `ai.speech`, `ai.transcription`,
384
391
  `ai.reranking`) a leg is a Workers AI id, because those run on the Workers AI
385
392
  run path, which resolves a leg by id and nothing else.
386
393
 
387
- `reasoningEffort` and `chatTemplateKwargs` are Workers AI settings. On a routed
388
- vendor model, reasoning is the vendor's own call option — use the AI SDK's
389
- unified `reasoning`, or that provider's `providerOptions`, and the vendor's
390
- 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.
391
398
 
392
399
  ### `providerOptions.cloudflare`
393
400
 
394
- Anything in the table above can be passed per call under the `cloudflare` key.
395
- It is read for gateway options and then **removed** before the call reaches a
396
- 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:
397
402
 
398
403
  ```ts
399
404
  await generateText({
400
- model: ai(anthropic("claude-opus-4-8")),
405
+ model: ai("anthropic/claude-opus-4.8"),
401
406
  prompt: "hi",
402
407
  providerOptions: {
403
- anthropic: { sendReasoning: true },
404
408
  cloudflare: { metadata: { userId: "u1" }, skipCache: true, cacheTtl: 300 }
405
409
  }
406
410
  });
@@ -414,28 +418,55 @@ then fails before its first output part — an `error` part, an error finish, or
414
418
  a broken body — in which case the failed leg's preamble is dropped and the
415
419
  answering leg's stream is delivered in its place. Once a leg has produced
416
420
  output it is the answer, failures and all: by then the caller already has part
417
- 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:
418
422
 
419
423
  ```ts
420
424
  const model = ai(anthropic("claude-opus-4-8"), {
421
- fallback: [ai("@cf/zai-org/glm-4.7-flash")]
425
+ fallback: ["openai/gpt-5.5", "@cf/zai-org/glm-4.7-flash"]
422
426
  });
423
427
  ```
424
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
+
425
433
  A leg travels with the chain's gateway options: the chain's `id`, `cacheTtl`
426
434
  and the rest win over a leg's own, `metadata` merges field by field, and
427
435
  everything else a leg was built with — its `headers`, its Workers AI knobs —
428
- stays the leg's. Legs written as strings are Workers AI ids, in a chain exactly
429
- as in `ai("…")`: a vendor id there is the same `TypeError`, whether it was
430
- 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`.
431
440
 
432
441
  `providerMetadata.cloudflare.model` reports the model that actually answered.
433
442
  When every leg fails, the thrown `CloudflareAIError` carries an `attempts`
434
443
  array of `{ model, error }` and the last error as its `cause`.
435
444
 
436
- This is a client-side loop today. AI Gateway also accepts an **array** of
437
- universal requests as a server-side fallback chain; when we adopt it, the
438
- 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.
439
470
 
440
471
  ## How Workers AI stays OpenAI-shaped
441
472
 
@@ -460,20 +491,17 @@ is conformant needs no entry at all.
460
491
  ## Observability: `providerMetadata.cloudflare`
461
492
 
462
493
  Every result and every stream `finish` part carries typed gateway metadata, so
463
- you never have to walk response objects looking for a log id. On a routed
464
- vendor model it is merged alongside the vendor's own metadata block rather than
465
- replacing it:
494
+ you never have to walk response objects looking for a log id:
466
495
 
467
496
  ```ts
468
497
  const { providerMetadata } = await generateText({
469
- model: ai(anthropic("claude-opus-4-8")),
498
+ model: ai("anthropic/claude-opus-4.8"),
470
499
  prompt: "hi"
471
500
  });
472
501
 
473
502
  providerMetadata?.cloudflare;
474
503
  // {
475
- // model: "claude-opus-4-8",
476
- // provider: "anthropic",
504
+ // model: "anthropic/claude-opus-4.8",
477
505
  // gateway: "prod",
478
506
  // logId: "01M1KZWN069WWNPC18V05NKHSS",
479
507
  // eventId: "7bfd3660-9d0f-4bf7-bf9b-fa90b860456f",
@@ -482,14 +510,13 @@ providerMetadata?.cloudflare;
482
510
  // cacheStatus: "MISS",
483
511
  // step: 0
484
512
  // }
485
- providerMetadata?.anthropic; // still Anthropic's own
486
513
  ```
487
514
 
488
515
  | Field | Source header | Notes |
489
516
  | ------------- | --------------------- | ---------------------------------------------- |
490
517
  | `model` | — | The leg that answered, after any fallback. |
491
- | `gateway` | — | The gateway id the request went through. |
492
- | `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. |
493
520
  | `logId` | `cf-aig-log-id` | Falls back to the binding's last log id. |
494
521
  | `eventId` | `cf-aig-event-id` | Correlates the requests of one gateway event. |
495
522
  | `requestId` | `cf-aig-request-id` | The gateway's id for this request. |
@@ -498,7 +525,7 @@ providerMetadata?.anthropic; // still Anthropic's own
498
525
  | `cacheStatus` | `cf-aig-cache-status` | `HIT` / `MISS`. |
499
526
  | `step` | `cf-aig-step` | Which step of a gateway route answered. |
500
527
 
501
- Every field except `model` and `gateway` is optional, and which ones arrive
528
+ Every field except `model` is optional, and which ones arrive
502
529
  depends on the path and the call. Read the fields you find rather than assuming
503
530
  any particular one is there. `response.headers` and `response.modelId` are
504
531
  populated too.
@@ -527,10 +554,8 @@ try {
527
554
  `code` is one of `"auth"`, `"rate-limit"`, `"not-found"`, `"bad-request"`,
528
555
  `"provider-error"`, `"gateway-error"`, `"unknown"`.
529
556
 
530
- On a routed vendor model this covers the gateway's own failures. A vendor's own
531
- error — a bad model id, a rejected parameter, a rate limit — is raised by that
532
- vendor's provider in its own error type, because it is the code that knows how
533
- 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.
534
559
 
535
560
  ## Experimental
536
561
 
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "bin": {
13
13
  "agents": "./dist/cli.js"
14
14
  },
15
- "version": "0.28.1",
15
+ "version": "0.29.0",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "directory": "packages/agents",
@@ -42,10 +42,10 @@
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.4",
46
- "@earendil-works/pi-ai": "^1.0.0",
47
- "@earendil-works/pi-durable": "^1.0.0",
48
- "@earendil-works/pi-tui": "1.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
49
  "@modelcontextprotocol/client": "2.3.1",
50
50
  "@modelcontextprotocol/conformance-v2": "npm:@modelcontextprotocol/conformance@0.2.0-alpha.12",
51
51
  "@modelcontextprotocol/sdk": "1.32.1",
@@ -71,8 +71,8 @@
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",
74
+ "@earendil-works/pi-ai": "~1.3.0",
75
+ "@earendil-works/pi-durable": "~1.3.0",
76
76
  "@modelcontextprotocol/client": "2.3.1",
77
77
  "@modelcontextprotocol/sdk": "1.32.1",
78
78
  "@modelcontextprotocol/server": "2.3.1",