workers-ai-provider 3.2.0 → 3.3.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.
@@ -1,457 +1,15 @@
1
1
  /**
2
- * Registry of Cloudflare AI Gateway providers.
3
- *
4
- * One table drives both delegate surfaces:
5
- *
6
- * - **Slug delegate** (`wai("openai/gpt-5")`): `resolverKey` is the slug prefix
7
- * the user types. `runCatalog` providers dispatch through the resumable run
8
- * path (`env.AI.run`, unified billing, `cf-aig-run-id`); the rest go through
9
- * the gateway path (`env.AI.gateway().run`, BYOK, no resume). `wireFormat`
10
- * selects the built-in `@ai-sdk/*` parser; absent ⇒ the provider is reachable
11
- * only via the bring-your-own-provider wrapper (it isn't chat/completions
12
- * shaped, e.g. audio/image providers).
13
- * - **Bring-your-own-provider** (`createGatewayProvider`): `hostPattern` +
14
- * `transformEndpoint` map a wrapped provider's request URL to the gateway
15
- * `provider` id + endpoint path.
16
- *
17
- * Slugs mirror the AI Gateway provider directory
18
- * (developers.cloudflare.com/ai-gateway/usage/providers/); endpoint transforms
19
- * mirror `ai-gateway-provider`'s provider table. `runCatalog` / `billing` flags
20
- * follow the documented unified-billing list (OpenAI, Anthropic, Google AI
21
- * Studio, Google Vertex, xAI/Grok, Groq) and are otherwise conservative — the
22
- * e2e suite confirms them live, since resume is undocumented upstream.
2
+ * The AI Gateway provider registry now lives in `@cloudflare/gateway-core`
3
+ * (shared with the other Cloudflare AI Gateway packages). This module re-exports
4
+ * it so the existing `workers-ai-provider/src/gateway-providers` import path
5
+ * keeps working unchanged.
23
6
  */
24
-
25
- /** Response wire format the slug delegate can parse with a built-in `@ai-sdk/*` provider. */
26
- export type WireFormat = "openai" | "anthropic" | "google";
27
-
28
- /** How a provider is billed + keyed when reached through the gateway. */
29
- export type Billing = "unified" | "byok";
30
-
31
- export interface GatewayProviderInfo {
32
- /**
33
- * Slug prefix the user types in `wai("<resolverKey>/<model>")`. For
34
- * `runCatalog` providers this is also the run-catalog author (so
35
- * `env.AI.run("<resolverKey>/<model>")` resolves).
36
- */
37
- resolverKey: string;
38
- /** Provider id for the gateway universal endpoint (`env.AI.gateway().run([{ provider }])`). */
39
- gatewayProviderId: string;
40
- /**
41
- * Built-in parser wire format. `openai` covers the whole OpenAI-compatible
42
- * long tail (deepseek, grok, groq, mistral, perplexity, …). Absent ⇒ reachable
43
- * only via the bring-your-own-provider wrapper (provider-native, non-chat, or a
44
- * gateway-path URL shape we don't reproduce reliably from the slug delegate).
45
- */
46
- wireFormat?: WireFormat;
47
- /**
48
- * Wire format the unified-billing **run path** (`env.AI.run`) emits for this
49
- * provider — which is NOT always the provider's native format. Cloudflare's
50
- * unified catalog normalizes most providers to OpenAI chat-completions (so
51
- * `google` is parsed with the `openai` plugin on the run path), but passes
52
- * **Anthropic through natively** (`content[].text`, native tool shape), so
53
- * anthropic must be parsed with the `anthropic` plugin. Defaults to `"openai"`
54
- * for run-catalog providers when omitted. Only meaningful when `runCatalog`.
55
- */
56
- runWireFormat?: WireFormat;
57
- /**
58
- * Base URL the wire-format builder should target so the request URL it
59
- * generates host-strips (via {@link transformEndpoint}) to the provider's
60
- * gateway-native endpoint. Omit to use the `@ai-sdk` provider's default (the
61
- * provider's own host — correct for `openai`/`anthropic`/`google`). Required
62
- * for OpenAI-wire providers that share the `openai` plugin but live on a
63
- * different host (deepseek, grok, groq, mistral, perplexity, …).
64
- */
65
- baseURL?: string;
66
- /** On the unified-billing resumable run catalog (`env.AI.run`, `cf-aig-run-id`). */
67
- runCatalog: boolean;
68
- /**
69
- * Whether the provider has a gateway path (`env.AI.gateway().run`). `false` ⇒
70
- * **run-path only**: the provider is on the unified run catalog but is not a
71
- * native gateway provider, so caching, server-side fallback, and
72
- * `transport: "gateway"` are unavailable and the delegate rejects them with a
73
- * clear error (rather than failing upstream). Defaults to `true`.
74
- */
75
- gatewayPath?: boolean;
76
- /** Billing model when reached through the gateway. */
77
- billing: Billing;
78
- /** Header(s) carrying the upstream provider key (stripped on the gateway path unless BYOK-forwarded). */
79
- authHeaders: string[];
80
- /** Host matcher for bring-your-own-provider URL detection. */
81
- hostPattern?: RegExp;
82
- /** Strip the provider host, leaving the gateway endpoint path (+ query). */
83
- transformEndpoint?: (url: string) => string;
84
- }
85
-
86
- /** Strip a leading `https://<host>/` prefix, leaving the endpoint path + query. */
87
- function hostStrip(pattern: RegExp): (url: string) => string {
88
- return (url: string) => url.replace(pattern, "");
89
- }
90
-
91
- const OPENAI_HOST = /^https:\/\/api\.openai\.com\//;
92
- const ANTHROPIC_HOST = /^https:\/\/api\.anthropic\.com\//;
93
- const GOOGLE_HOST = /^https:\/\/generativelanguage\.googleapis\.com\//;
94
- const VERTEX_HOST = /^https:\/\/(?:[a-z0-9-]+-)?aiplatform\.googleapis\.com\//;
95
- const XAI_HOST = /^https:\/\/api\.x\.ai\//;
96
- const GROQ_HOST = /^https:\/\/api\.groq\.com\/openai\/v1\//;
97
- const DEEPSEEK_HOST = /^https:\/\/api\.deepseek\.com\//;
98
- const MISTRAL_HOST = /^https:\/\/api\.mistral\.ai\//;
99
- const PERPLEXITY_HOST = /^https:\/\/api\.perplexity\.ai\//;
100
- const CEREBRAS_HOST = /^https:\/\/api\.cerebras\.ai\//;
101
- const OPENROUTER_HOST = /^https:\/\/openrouter\.ai\/api\//;
102
- const FIREWORKS_HOST = /^https:\/\/api\.fireworks\.ai\/inference\/v1\//;
103
- const COHERE_HOST = /^https:\/\/api\.cohere\.(?:com|ai)\//;
104
- const REPLICATE_HOST = /^https:\/\/api\.replicate\.com\//;
105
- const HUGGINGFACE_HOST = /^https:\/\/api-inference\.huggingface\.co\/models\//;
106
- const CARTESIA_HOST = /^https:\/\/api\.cartesia\.ai\//;
107
- const FAL_HOST = /^https:\/\/fal\.run\//;
108
- const IDEOGRAM_HOST = /^https:\/\/api\.ideogram\.ai\//;
109
- const DEEPGRAM_HOST = /^https:\/\/api\.deepgram\.com\//;
110
- const ELEVENLABS_HOST = /^https:\/\/api\.elevenlabs\.io\//;
111
- const GROK_KEY = "grok";
112
-
113
- // Bedrock's URL carries the AWS region, which the gateway endpoint preserves as
114
- // `bedrock-runtime/<region>/<rest>` (mirrors ai-gateway-provider).
115
- const BEDROCK_HOST = /^https:\/\/bedrock-runtime\.(?<region>[^.]+)\.amazonaws\.com\//;
116
- function bedrockTransform(url: string): string {
117
- const m = url.match(
118
- /^https:\/\/bedrock-runtime\.(?<region>[^.]+)\.amazonaws\.com\/(?<rest>.*)$/,
119
- );
120
- if (!m?.groups) return url;
121
- const { region, rest } = m.groups;
122
- if (!region || rest === undefined) return url;
123
- return `bedrock-runtime/${region}/${rest}`;
124
- }
125
-
126
- // Azure's URL carries the resource + deployment, so it needs a bespoke transform
127
- // (mirrors ai-gateway-provider). Only used for bring-your-own-provider detection.
128
- const AZURE_HOST =
129
- /^https:\/\/(?<resource>[^.]+)\.openai\.azure\.com\/openai\/deployments\/(?<deployment>[^/]+)\/(?<rest>.*)$/;
130
- function azureTransform(url: string): string {
131
- const m = url.match(AZURE_HOST);
132
- if (!m?.groups) return url;
133
- const { resource, deployment, rest } = m.groups;
134
- if (!resource || !deployment || !rest) return url;
135
- return `${resource}/${deployment}/${rest}`;
136
- }
137
-
138
- /**
139
- * The provider table. Order matters only for `detectProviderByUrl` (first match
140
- * wins); slugs are looked up by `resolverKey`.
141
- */
142
- export const GATEWAY_PROVIDERS: GatewayProviderInfo[] = [
143
- // ---- Unified-billing run-catalog providers (resumable run path) ----
144
- {
145
- resolverKey: "openai",
146
- gatewayProviderId: "openai",
147
- wireFormat: "openai",
148
- runCatalog: true,
149
- billing: "unified",
150
- authHeaders: ["authorization"],
151
- hostPattern: OPENAI_HOST,
152
- transformEndpoint: hostStrip(OPENAI_HOST),
153
- },
154
- {
155
- resolverKey: "anthropic",
156
- gatewayProviderId: "anthropic",
157
- wireFormat: "anthropic",
158
- // Unified billing passes Anthropic through natively (unlike google, which it
159
- // normalizes to openai-wire), so the run path also speaks Anthropic Messages.
160
- runWireFormat: "anthropic",
161
- runCatalog: true,
162
- billing: "unified",
163
- authHeaders: ["x-api-key", "authorization"],
164
- hostPattern: ANTHROPIC_HOST,
165
- transformEndpoint: hostStrip(ANTHROPIC_HOST),
166
- },
167
- {
168
- resolverKey: "google",
169
- gatewayProviderId: "google-ai-studio",
170
- // Gateway path hits Gemini's native endpoint (google-wire); the unified run
171
- // path, however, returns openai-wire — so runWireFormat defaults to "openai".
172
- wireFormat: "google",
173
- runCatalog: true,
174
- billing: "unified",
175
- authHeaders: ["x-goog-api-key", "authorization"],
176
- hostPattern: GOOGLE_HOST,
177
- transformEndpoint: hostStrip(GOOGLE_HOST),
178
- },
179
- {
180
- resolverKey: "xai",
181
- gatewayProviderId: GROK_KEY,
182
- wireFormat: "openai",
183
- // Targeted so a forced gateway-path request host-strips correctly (the run
184
- // path, the default for xai, ignores this).
185
- baseURL: "https://api.x.ai/v1",
186
- runCatalog: true,
187
- billing: "unified",
188
- authHeaders: ["authorization"],
189
- hostPattern: XAI_HOST,
190
- transformEndpoint: hostStrip(XAI_HOST),
191
- },
192
- {
193
- resolverKey: "groq",
194
- gatewayProviderId: "groq",
195
- wireFormat: "openai",
196
- // Groq's gateway-native endpoint strips `/openai/v1/`, so the builder must
197
- // target that base or a forced gateway request doubles the prefix.
198
- baseURL: "https://api.groq.com/openai/v1",
199
- runCatalog: true,
200
- billing: "unified",
201
- authHeaders: ["authorization"],
202
- hostPattern: GROQ_HOST,
203
- transformEndpoint: hostStrip(GROQ_HOST),
204
- },
205
- // Unified-catalog chat providers that are NOT in the native gateway directory:
206
- // they exist only on the resumable run path (env.AI.run, unified billing), so
207
- // there's no BYOK gateway path. Both return OpenAI chat-completions wire (so the
208
- // `openai` plugin parses them) and emit `cf-aig-run-id` on streams (resumable),
209
- // verified live against the default gateway. Forcing transport:"gateway" for
210
- // these errors upstream (no native provider) — that's expected.
211
- {
212
- // Alibaba Qwen, served via DashScope's OpenAI-compatible endpoint.
213
- resolverKey: "alibaba",
214
- gatewayProviderId: "alibaba",
215
- wireFormat: "openai",
216
- runCatalog: true,
217
- gatewayPath: false,
218
- billing: "unified",
219
- authHeaders: ["authorization"],
220
- },
221
- {
222
- // MiniMax (M-series). OpenAI-wire with extra fields (reasoning_content,
223
- // audio_content) the openai parser ignores; core choices[].delta.content is standard.
224
- resolverKey: "minimax",
225
- gatewayProviderId: "minimax",
226
- wireFormat: "openai",
227
- runCatalog: true,
228
- gatewayPath: false,
229
- billing: "unified",
230
- authHeaders: ["authorization"],
231
- },
232
- {
233
- resolverKey: "google-vertex",
234
- gatewayProviderId: "google-vertex-ai",
235
- // Vertex's URL carries project/location/publisher segments that the
236
- // `@ai-sdk/google` default (AI Studio) does not produce, so the slug
237
- // delegate can't shape it reliably — reach Vertex via createGatewayProvider.
238
- runCatalog: false,
239
- billing: "unified",
240
- authHeaders: ["authorization"],
241
- hostPattern: VERTEX_HOST,
242
- transformEndpoint: hostStrip(VERTEX_HOST),
243
- },
244
-
245
- // ---- OpenAI-compatible long tail (gateway path, BYOK) ----
246
- {
247
- resolverKey: "deepseek",
248
- gatewayProviderId: "deepseek",
249
- wireFormat: "openai",
250
- baseURL: "https://api.deepseek.com",
251
- runCatalog: false,
252
- billing: "byok",
253
- authHeaders: ["authorization"],
254
- hostPattern: DEEPSEEK_HOST,
255
- transformEndpoint: hostStrip(DEEPSEEK_HOST),
256
- },
257
- {
258
- resolverKey: "mistral",
259
- gatewayProviderId: "mistral",
260
- wireFormat: "openai",
261
- baseURL: "https://api.mistral.ai/v1",
262
- runCatalog: false,
263
- billing: "byok",
264
- authHeaders: ["authorization"],
265
- hostPattern: MISTRAL_HOST,
266
- transformEndpoint: hostStrip(MISTRAL_HOST),
267
- },
268
- {
269
- resolverKey: "perplexity",
270
- gatewayProviderId: "perplexity-ai",
271
- wireFormat: "openai",
272
- baseURL: "https://api.perplexity.ai",
273
- runCatalog: false,
274
- billing: "byok",
275
- authHeaders: ["authorization"],
276
- hostPattern: PERPLEXITY_HOST,
277
- transformEndpoint: hostStrip(PERPLEXITY_HOST),
278
- },
279
- {
280
- resolverKey: "cerebras",
281
- gatewayProviderId: "cerebras",
282
- wireFormat: "openai",
283
- baseURL: "https://api.cerebras.ai/v1",
284
- runCatalog: false,
285
- billing: "byok",
286
- authHeaders: ["authorization"],
287
- hostPattern: CEREBRAS_HOST,
288
- transformEndpoint: hostStrip(CEREBRAS_HOST),
289
- },
290
- {
291
- resolverKey: "openrouter",
292
- gatewayProviderId: "openrouter",
293
- wireFormat: "openai",
294
- baseURL: "https://openrouter.ai/api/v1",
295
- runCatalog: false,
296
- billing: "byok",
297
- authHeaders: ["authorization"],
298
- hostPattern: OPENROUTER_HOST,
299
- transformEndpoint: hostStrip(OPENROUTER_HOST),
300
- },
301
- {
302
- // Fireworks is OpenAI-compatible. Present on ai-gateway-provider (#409) but
303
- // not the current provider directory — treat as BYOK long-tail.
304
- resolverKey: "fireworks",
305
- gatewayProviderId: "fireworks",
306
- wireFormat: "openai",
307
- baseURL: "https://api.fireworks.ai/inference/v1",
308
- runCatalog: false,
309
- billing: "byok",
310
- authHeaders: ["authorization"],
311
- hostPattern: FIREWORKS_HOST,
312
- transformEndpoint: hostStrip(FIREWORKS_HOST),
313
- },
314
- // Providers whose gateway-path URL shape isn't reliably reproducible from the
315
- // shared openai builder (cohere's /compat surface, baseten's per-deployment
316
- // hosts, parallel, azure's resource/deployment path) are bring-your-own-provider
317
- // only — set your own @ai-sdk provider baseURL and route via createGatewayProvider.
318
- {
319
- resolverKey: "cohere",
320
- gatewayProviderId: "cohere",
321
- runCatalog: false,
322
- billing: "byok",
323
- authHeaders: ["authorization"],
324
- hostPattern: COHERE_HOST,
325
- transformEndpoint: hostStrip(COHERE_HOST),
326
- },
327
- {
328
- // Baseten serves per-deployment hosts, so there's no single detectable URL
329
- // shape — reach it with an explicit `provider` via createGatewayProvider.
330
- resolverKey: "baseten",
331
- gatewayProviderId: "baseten",
332
- runCatalog: false,
333
- billing: "byok",
334
- authHeaders: ["authorization"],
335
- },
336
- {
337
- resolverKey: "parallel",
338
- gatewayProviderId: "parallel",
339
- runCatalog: false,
340
- billing: "byok",
341
- authHeaders: ["authorization", "x-api-key"],
342
- },
343
- {
344
- resolverKey: "azure-openai",
345
- gatewayProviderId: "azure-openai",
346
- runCatalog: false,
347
- billing: "byok",
348
- authHeaders: ["api-key", "authorization"],
349
- hostPattern: AZURE_HOST,
350
- transformEndpoint: azureTransform,
351
- },
352
-
353
- // ---- Provider-native only: reachable via the bring-your-own-provider wrapper ----
354
- // (no `wireFormat` ⇒ not auto-wired by the slug delegate)
355
- {
356
- resolverKey: "aws-bedrock",
357
- gatewayProviderId: "aws-bedrock",
358
- runCatalog: false,
359
- billing: "byok",
360
- authHeaders: ["authorization"],
361
- hostPattern: BEDROCK_HOST,
362
- transformEndpoint: bedrockTransform,
363
- },
364
- {
365
- resolverKey: "huggingface",
366
- gatewayProviderId: "huggingface",
367
- runCatalog: false,
368
- billing: "byok",
369
- authHeaders: ["authorization"],
370
- hostPattern: HUGGINGFACE_HOST,
371
- transformEndpoint: hostStrip(HUGGINGFACE_HOST),
372
- },
373
- {
374
- resolverKey: "replicate",
375
- gatewayProviderId: "replicate",
376
- runCatalog: false,
377
- billing: "byok",
378
- authHeaders: ["authorization"],
379
- hostPattern: REPLICATE_HOST,
380
- transformEndpoint: hostStrip(REPLICATE_HOST),
381
- },
382
- {
383
- resolverKey: "fal",
384
- gatewayProviderId: "fal",
385
- runCatalog: false,
386
- billing: "byok",
387
- authHeaders: ["authorization"],
388
- hostPattern: FAL_HOST,
389
- transformEndpoint: hostStrip(FAL_HOST),
390
- },
391
- {
392
- resolverKey: "ideogram",
393
- gatewayProviderId: "ideogram",
394
- runCatalog: false,
395
- billing: "byok",
396
- authHeaders: ["authorization"],
397
- hostPattern: IDEOGRAM_HOST,
398
- transformEndpoint: hostStrip(IDEOGRAM_HOST),
399
- },
400
- {
401
- resolverKey: "cartesia",
402
- gatewayProviderId: "cartesia",
403
- runCatalog: false,
404
- billing: "byok",
405
- authHeaders: ["authorization", "x-api-key"],
406
- hostPattern: CARTESIA_HOST,
407
- transformEndpoint: hostStrip(CARTESIA_HOST),
408
- },
409
- {
410
- resolverKey: "deepgram",
411
- gatewayProviderId: "deepgram",
412
- runCatalog: false,
413
- billing: "byok",
414
- authHeaders: ["authorization", "token"],
415
- hostPattern: DEEPGRAM_HOST,
416
- transformEndpoint: hostStrip(DEEPGRAM_HOST),
417
- },
418
- {
419
- resolverKey: "elevenlabs",
420
- gatewayProviderId: "elevenlabs",
421
- runCatalog: false,
422
- billing: "byok",
423
- authHeaders: ["xi-api-key", "authorization"],
424
- hostPattern: ELEVENLABS_HOST,
425
- transformEndpoint: hostStrip(ELEVENLABS_HOST),
426
- },
427
- ];
428
-
429
- /** Aliases that map a friendly slug prefix to a canonical `resolverKey`. */
430
- const RESOLVER_ALIASES: Record<string, string> = {
431
- // xAI's run-catalog author is `xai`, but `grok` is the common name.
432
- grok: "xai",
433
- "google-ai-studio": "google",
434
- "google-vertex-ai": "google-vertex",
435
- bedrock: "aws-bedrock",
436
- azure: "azure-openai",
437
- };
438
-
439
- const BY_RESOLVER_KEY = new Map<string, GatewayProviderInfo>(
440
- GATEWAY_PROVIDERS.map((p) => [p.resolverKey, p]),
441
- );
442
-
443
- /** Look up a provider by the slug prefix the user typed (honoring aliases). */
444
- export function findProviderBySlug(resolverKey: string): GatewayProviderInfo | undefined {
445
- const canonical = RESOLVER_ALIASES[resolverKey] ?? resolverKey;
446
- return BY_RESOLVER_KEY.get(canonical);
447
- }
448
-
449
- /** Detect the gateway provider from a wrapped provider's request URL (BYOG). */
450
- export function detectProviderByUrl(url: string): GatewayProviderInfo | undefined {
451
- return GATEWAY_PROVIDERS.find((p) => p.hostPattern?.test(url));
452
- }
453
-
454
- /** All slug keys with a built-in parser (auto-wireable by the slug delegate). */
455
- export function wireableProviders(): GatewayProviderInfo[] {
456
- return GATEWAY_PROVIDERS.filter((p) => p.wireFormat !== undefined);
457
- }
7
+ export {
8
+ type Billing,
9
+ detectProviderByUrl,
10
+ findProviderBySlug,
11
+ GATEWAY_PROVIDERS,
12
+ type GatewayProviderInfo,
13
+ type WireFormat,
14
+ wireableProviders,
15
+ } from "@cloudflare/gateway-core";
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ import {
92
92
  type GatewayDelegate,
93
93
  type ProviderPlugin,
94
94
  type ResumeExpiredPolicy,
95
+ type WireFormat,
95
96
  } from "./gateway-delegate";
96
97
 
97
98
  // ---------------------------------------------------------------------------
@@ -103,6 +104,7 @@ import {
103
104
  * configured. Every Cloudflare account has a `"default"` gateway.
104
105
  */
105
106
  const DEFAULT_GATEWAY_ID = "default";
107
+ const DYNAMIC_ROUTE_WIRE_FORMAT: WireFormat = "openai";
106
108
 
107
109
  export type WorkersAISettings = (
108
110
  | {
@@ -178,9 +180,17 @@ type IsCatalogSlug<M extends string> = string extends M
178
180
  ? false
179
181
  : M extends `@${string}`
180
182
  ? false
181
- : M extends `${string}/${string}`
182
- ? true
183
- : false;
183
+ : M extends `dynamic/${string}`
184
+ ? false
185
+ : M extends `${string}/${string}`
186
+ ? true
187
+ : false;
188
+
189
+ type IsDynamicRoute<M extends string> = string extends M
190
+ ? false
191
+ : M extends `dynamic/${string}`
192
+ ? true
193
+ : false;
184
194
 
185
195
  /**
186
196
  * Picks the per-model settings type from the (captured) literal model id:
@@ -189,7 +199,11 @@ type IsCatalogSlug<M extends string> = string extends M
189
199
  * options while `workersai("@cf/…", { … })` autocompletes chat settings.
190
200
  */
191
201
  type ModelSettings<M extends string> =
192
- IsCatalogSlug<M> extends true ? DelegateCallOptions : WorkersAIChatSettings;
202
+ IsCatalogSlug<M> extends true
203
+ ? DelegateCallOptions
204
+ : IsDynamicRoute<M> extends true
205
+ ? DelegateCallOptions | WorkersAIChatSettings
206
+ : WorkersAIChatSettings;
193
207
 
194
208
  export interface WorkersAI {
195
209
  <M extends string>(
@@ -291,6 +305,119 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
291
305
  isBinding,
292
306
  });
293
307
 
308
+ const toGatewayOptions = (
309
+ gateway: GatewayOptions | string | undefined,
310
+ ): GatewayOptions | undefined => (typeof gateway === "string" ? { id: gateway } : gateway);
311
+
312
+ const createDynamicRouteModel = (
313
+ modelId: TextGenerationModels,
314
+ settings: WorkersAIChatSettings & DelegateCallOptions = {},
315
+ ) => {
316
+ if (
317
+ settings.fallback ||
318
+ settings.transport === "gateway" ||
319
+ settings.resume === true ||
320
+ settings.onProgress ||
321
+ settings.onResumeExpired ||
322
+ settings.byok
323
+ ) {
324
+ throw new Error(
325
+ `"${modelId}" is an AI Gateway dynamic route. Dynamic routes use AI.run with ` +
326
+ "OpenAI-compatible chat-completions wire format; fallback, gateway transport, " +
327
+ "resume, BYOK, and resume callbacks must be configured on the dynamic route or " +
328
+ "gateway instead of per call.",
329
+ );
330
+ }
331
+
332
+ const gateway = {
333
+ ...(toGatewayOptions(settings.gateway) ??
334
+ options.gateway ?? { id: DEFAULT_GATEWAY_ID }),
335
+ };
336
+ if (settings.metadata) {
337
+ gateway.metadata = {
338
+ ...(gateway.metadata ?? {}),
339
+ ...settings.metadata,
340
+ };
341
+ }
342
+ if (settings.collectLog !== undefined) {
343
+ gateway.collectLog = settings.collectLog;
344
+ }
345
+ if (settings.cacheTtl !== undefined) {
346
+ gateway.cacheTtl = settings.cacheTtl;
347
+ }
348
+ if (settings.skipCache !== undefined) {
349
+ gateway.skipCache = settings.skipCache;
350
+ }
351
+
352
+ const chatSettings = {
353
+ ...settings,
354
+ gateway,
355
+ };
356
+ delete chatSettings.metadata;
357
+ delete chatSettings.collectLog;
358
+ delete chatSettings.cacheTtl;
359
+ delete chatSettings.skipCache;
360
+ delete chatSettings.resume;
361
+ delete chatSettings.fallback;
362
+ delete chatSettings.transport;
363
+ delete chatSettings.onDispatch;
364
+ delete chatSettings.onProgress;
365
+ delete chatSettings.onResumeExpired;
366
+ delete chatSettings.byok;
367
+
368
+ const plugin = options.providers?.find((p) => p.wireFormat === DYNAMIC_ROUTE_WIRE_FORMAT);
369
+ if (!plugin) {
370
+ if (options.providers?.length) {
371
+ throw new Error(
372
+ `"${modelId}" is an AI Gateway dynamic route. Dynamic routes return OpenAI-compatible ` +
373
+ "chat-completions wire format on the AI.run path, so configure the OpenAI " +
374
+ 'provider plugin: import { openai } from "workers-ai-provider/openai"; ' +
375
+ "createWorkersAI({ binding: env.AI, providers: [openai] }).",
376
+ );
377
+ }
378
+ return createChatModel(modelId, chatSettings);
379
+ }
380
+ const fetchImpl = (async (
381
+ _input: RequestInfo | URL,
382
+ init?: RequestInit,
383
+ ): Promise<Response> => {
384
+ const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
385
+ delete body.model;
386
+
387
+ const runOptions: Record<string, unknown> = {
388
+ gateway,
389
+ returnRawResponse: true,
390
+ ...(settings.extraHeaders ? { extraHeaders: settings.extraHeaders } : {}),
391
+ ...(init?.signal ? { signal: init.signal } : {}),
392
+ };
393
+ const response = await (
394
+ binding as unknown as {
395
+ run(
396
+ model: string,
397
+ inputs: Record<string, unknown>,
398
+ options: Record<string, unknown>,
399
+ ): Promise<Response>;
400
+ }
401
+ ).run(modelId, body, runOptions);
402
+ settings.onDispatch?.({
403
+ transport: "run",
404
+ resumeEnabled: false,
405
+ warnings: [],
406
+ status: response.status,
407
+ runId: response.headers.get("cf-aig-run-id"),
408
+ cfStep: response.headers.get("cf-aig-step"),
409
+ cacheStatus: response.headers.get("cf-aig-cache-status"),
410
+ logId: response.headers.get("cf-aig-log-id"),
411
+ });
412
+ return response;
413
+ }) as typeof globalThis.fetch;
414
+
415
+ return plugin.create({
416
+ modelId,
417
+ fetch: fetchImpl,
418
+ }) as unknown as WorkersAIChatLanguageModel;
419
+ };
420
+
294
421
  // Third-party catalog routing: when `providers` is configured, a non-`@cf/`
295
422
  // `"<provider>/<model>"` slug is dispatched through the gateway delegate
296
423
  // instead of being treated as a Workers AI model id. Built lazily so the
@@ -321,11 +448,14 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
321
448
  return delegate;
322
449
  };
323
450
 
324
- // Workers AI model ids are always `@cf/...`; gateway catalog slugs are
325
- // `"<provider>/<model>"`. Anything with a slash that is not `@`-prefixed is
326
- // treated as a catalog slug.
451
+ // Workers AI model ids are usually `@cf/...`, but AI Gateway dynamic routes
452
+ // use the `dynamic/<route>` namespace and must pass through to `AI.run`.
453
+ // Other non-`@` ids with a slash are treated as catalog slugs.
327
454
  const isGatewaySlug = (id: unknown): id is string =>
328
- typeof id === "string" && !id.startsWith("@") && id.includes("/");
455
+ typeof id === "string" &&
456
+ !id.startsWith("@") &&
457
+ !id.startsWith("dynamic/") &&
458
+ id.includes("/");
329
459
 
330
460
  // Settings is the union of both shapes here; the public `WorkersAI` interface
331
461
  // narrows it per call via `ModelSettings<M>`. We branch at runtime and cast to
@@ -334,6 +464,12 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
334
464
  modelId: TextGenerationModels,
335
465
  settings?: WorkersAIChatSettings | DelegateCallOptions,
336
466
  ): WorkersAIChatLanguageModel => {
467
+ if (typeof modelId === "string" && modelId.startsWith("dynamic/")) {
468
+ return createDynamicRouteModel(
469
+ modelId,
470
+ settings as (WorkersAIChatSettings & DelegateCallOptions) | undefined,
471
+ );
472
+ }
337
473
  if (isGatewaySlug(modelId)) {
338
474
  // The delegate returns a `LanguageModelV3` built by the configured plugin.
339
475
  // It's structurally compatible with the AI SDK consumers this provider is