workers-ai-provider 3.2.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workers-ai-provider",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "Workers AI Provider for the vercel AI SDK",
5
5
  "keywords": [
6
6
  "ai",
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,112 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
291
305
  isBinding,
292
306
  });
293
307
 
308
+ const toGatewayOptions = (gateway: GatewayOptions | string | undefined): GatewayOptions | undefined =>
309
+ typeof gateway === "string" ? { id: gateway } : gateway;
310
+
311
+ const createDynamicRouteModel = (
312
+ modelId: TextGenerationModels,
313
+ settings: (WorkersAIChatSettings & DelegateCallOptions) = {},
314
+ ) => {
315
+ if (
316
+ settings.fallback ||
317
+ settings.transport === "gateway" ||
318
+ settings.resume === true ||
319
+ settings.onProgress ||
320
+ settings.onResumeExpired ||
321
+ settings.byok
322
+ ) {
323
+ throw new Error(
324
+ `"${modelId}" is an AI Gateway dynamic route. Dynamic routes use AI.run with ` +
325
+ "OpenAI-compatible chat-completions wire format; fallback, gateway transport, " +
326
+ "resume, BYOK, and resume callbacks must be configured on the dynamic route or " +
327
+ "gateway instead of per call.",
328
+ );
329
+ }
330
+
331
+ const gateway = {
332
+ ...(toGatewayOptions(settings.gateway) ?? options.gateway ?? { id: DEFAULT_GATEWAY_ID }),
333
+ };
334
+ if (settings.metadata) {
335
+ gateway.metadata = {
336
+ ...(gateway.metadata ?? {}),
337
+ ...settings.metadata,
338
+ };
339
+ }
340
+ if (settings.collectLog !== undefined) {
341
+ gateway.collectLog = settings.collectLog;
342
+ }
343
+ if (settings.cacheTtl !== undefined) {
344
+ gateway.cacheTtl = settings.cacheTtl;
345
+ }
346
+ if (settings.skipCache !== undefined) {
347
+ gateway.skipCache = settings.skipCache;
348
+ }
349
+
350
+ const chatSettings = {
351
+ ...settings,
352
+ gateway,
353
+ };
354
+ delete chatSettings.metadata;
355
+ delete chatSettings.collectLog;
356
+ delete chatSettings.cacheTtl;
357
+ delete chatSettings.skipCache;
358
+ delete chatSettings.resume;
359
+ delete chatSettings.fallback;
360
+ delete chatSettings.transport;
361
+ delete chatSettings.onDispatch;
362
+ delete chatSettings.onProgress;
363
+ delete chatSettings.onResumeExpired;
364
+ delete chatSettings.byok;
365
+
366
+ const plugin = options.providers?.find((p) => p.wireFormat === DYNAMIC_ROUTE_WIRE_FORMAT);
367
+ if (!plugin) {
368
+ if (options.providers?.length) {
369
+ throw new Error(
370
+ `"${modelId}" is an AI Gateway dynamic route. Dynamic routes return OpenAI-compatible ` +
371
+ "chat-completions wire format on the AI.run path, so configure the OpenAI " +
372
+ 'provider plugin: import { openai } from "workers-ai-provider/openai"; ' +
373
+ "createWorkersAI({ binding: env.AI, providers: [openai] }).",
374
+ );
375
+ }
376
+ return createChatModel(modelId, chatSettings);
377
+ }
378
+ const fetchImpl = (async (_input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
379
+ const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
380
+ delete body.model;
381
+
382
+ const runOptions: Record<string, unknown> = {
383
+ gateway,
384
+ returnRawResponse: true,
385
+ ...(settings.extraHeaders ? { extraHeaders: settings.extraHeaders } : {}),
386
+ ...(init?.signal ? { signal: init.signal } : {}),
387
+ };
388
+ const response = await (binding as unknown as {
389
+ run(
390
+ model: string,
391
+ inputs: Record<string, unknown>,
392
+ options: Record<string, unknown>,
393
+ ): Promise<Response>;
394
+ }).run(modelId, body, runOptions);
395
+ settings.onDispatch?.({
396
+ transport: "run",
397
+ resumeEnabled: false,
398
+ warnings: [],
399
+ status: response.status,
400
+ runId: response.headers.get("cf-aig-run-id"),
401
+ cfStep: response.headers.get("cf-aig-step"),
402
+ cacheStatus: response.headers.get("cf-aig-cache-status"),
403
+ logId: response.headers.get("cf-aig-log-id"),
404
+ });
405
+ return response;
406
+ }) as typeof globalThis.fetch;
407
+
408
+ return plugin.create({
409
+ modelId,
410
+ fetch: fetchImpl,
411
+ }) as unknown as WorkersAIChatLanguageModel;
412
+ };
413
+
294
414
  // Third-party catalog routing: when `providers` is configured, a non-`@cf/`
295
415
  // `"<provider>/<model>"` slug is dispatched through the gateway delegate
296
416
  // instead of being treated as a Workers AI model id. Built lazily so the
@@ -321,11 +441,14 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
321
441
  return delegate;
322
442
  };
323
443
 
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.
444
+ // Workers AI model ids are usually `@cf/...`, but AI Gateway dynamic routes
445
+ // use the `dynamic/<route>` namespace and must pass through to `AI.run`.
446
+ // Other non-`@` ids with a slash are treated as catalog slugs.
327
447
  const isGatewaySlug = (id: unknown): id is string =>
328
- typeof id === "string" && !id.startsWith("@") && id.includes("/");
448
+ typeof id === "string" &&
449
+ !id.startsWith("@") &&
450
+ !id.startsWith("dynamic/") &&
451
+ id.includes("/");
329
452
 
330
453
  // Settings is the union of both shapes here; the public `WorkersAI` interface
331
454
  // narrows it per call via `ModelSettings<M>`. We branch at runtime and cast to
@@ -334,6 +457,12 @@ export function createWorkersAI(options: WorkersAISettings): WorkersAI {
334
457
  modelId: TextGenerationModels,
335
458
  settings?: WorkersAIChatSettings | DelegateCallOptions,
336
459
  ): WorkersAIChatLanguageModel => {
460
+ if (typeof modelId === "string" && modelId.startsWith("dynamic/")) {
461
+ return createDynamicRouteModel(
462
+ modelId,
463
+ settings as (WorkersAIChatSettings & DelegateCallOptions) | undefined,
464
+ );
465
+ }
337
466
  if (isGatewaySlug(modelId)) {
338
467
  // The delegate returns a `LanguageModelV3` built by the configured plugin.
339
468
  // It's structurally compatible with the AI SDK consumers this provider is