workers-ai-provider 3.2.1 → 3.3.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/dist/openai.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ProviderPlugin } from "./gateway-delegate-BfaUTwDZ.mjs";
1
+ import { a as ProviderPlugin } from "./gateway-delegate-D_zkIp5r.mjs";
2
2
 
3
3
  //#region src/openai.d.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workers-ai-provider",
3
- "version": "3.2.1",
3
+ "version": "3.3.1",
4
4
  "description": "Workers AI Provider for the vercel AI SDK",
5
5
  "keywords": [
6
6
  "ai",
@@ -57,9 +57,10 @@
57
57
  "@ai-sdk/google": "^3.0.0",
58
58
  "@ai-sdk/openai": "^3.0.71",
59
59
  "@ai-sdk/provider": "^3.0.10",
60
- "@cloudflare/workers-types": "^4.20260613.1",
60
+ "@cloudflare/workers-types": "^4.20260628.1",
61
61
  "ai": "^6.0.204",
62
- "zod": "^4.4.3"
62
+ "zod": "^4.4.3",
63
+ "@cloudflare/gateway-core": "0.0.0"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "@ai-sdk/anthropic": "^3.0.0",
@@ -81,7 +82,7 @@
81
82
  },
82
83
  "authors": "Cloudflare Inc.",
83
84
  "scripts": {
84
- "build": "rm -rf dist && tsdown src/index.ts src/gateway-provider.ts src/openai.ts src/anthropic.ts src/google.ts --dts --sourcemap --format esm --target es2020",
85
+ "build": "rm -rf dist && tsdown --config tsdown.config.ts",
85
86
  "format": "oxfmt --write .",
86
87
  "test:ci": "vitest --watch=false",
87
88
  "test": "vitest",
@@ -1,13 +1,23 @@
1
- import type { LanguageModelV3 } from "@ai-sdk/provider";
1
+ import type { LanguageModelV3, LanguageModelV3CallOptions } from "@ai-sdk/provider";
2
+ import {
3
+ asText,
4
+ buildGatewayEntry,
5
+ createResumableStream,
6
+ GatewayDelegateError,
7
+ type GatewayCacheOptions,
8
+ type GatewayEntry,
9
+ type ResumeExpiredPolicy,
10
+ } from "@cloudflare/gateway-core";
2
11
  import { createClientFallbackModel } from "./client-fallback";
3
12
  import { findProviderBySlug, type GatewayProviderInfo, type WireFormat } from "./gateway-providers";
4
- import { createResumableStream, type ResumeExpiredPolicy } from "./resumable-stream";
5
13
 
6
14
  export {
7
15
  createResumableStream,
16
+ GatewayDelegateError,
17
+ type GatewayDelegateErrorKind,
8
18
  type ResumableStreamOptions,
9
19
  type ResumeExpiredPolicy,
10
- } from "./resumable-stream";
20
+ } from "@cloudflare/gateway-core";
11
21
  export {
12
22
  type FallbackAttempt,
13
23
  type GatewayErrorCode,
@@ -197,6 +207,20 @@ export interface DelegateCallOptions {
197
207
  * delegate strips provider auth headers so unified billing applies.
198
208
  */
199
209
  byok?: boolean;
210
+ /**
211
+ * Gateway path only: BYOK stored-key alias to authenticate with
212
+ * (`cf-aig-byok-alias`). Selects a non-`default` key you configured for the
213
+ * provider on the gateway. Independent of `byok` (which controls whether the
214
+ * caller's own provider auth header is forwarded vs. stripped).
215
+ */
216
+ byokAlias?: string;
217
+ /**
218
+ * Per-request Zero Data Retention override (`cf-aig-zdr`), Unified Billing
219
+ * only: `true` forces ZDR-capable upstreams, `false` disables it for this
220
+ * request. Overrides the gateway-level ZDR default. Applied on both transports
221
+ * (run path: passed through gateway options as a header; gateway path: entry header).
222
+ */
223
+ zdr?: boolean;
200
224
  /** Override the delegate's gateway for this model. */
201
225
  gateway?: GatewayOptions | string;
202
226
  /**
@@ -247,13 +271,18 @@ export function selectTransport(
247
271
  // Run-path-only providers (on the run catalog, but not native gateway
248
272
  // providers) have no gateway path at all — reject anything that would need it
249
273
  // here, with a clear message, rather than letting it fail upstream.
250
- if (runCatalog && !gatewayAvailable && (opts.transport === "gateway" || gatewayOnly)) {
251
- const what = opts.transport === "gateway" ? 'transport:"gateway"' : feature;
274
+ if (
275
+ runCatalog &&
276
+ !gatewayAvailable &&
277
+ (opts.transport === "gateway" || opts.byok || gatewayOnly)
278
+ ) {
279
+ const what =
280
+ opts.transport === "gateway" ? 'transport:"gateway"' : opts.byok ? "byok" : feature;
252
281
  throw new GatewayDelegateError(
253
282
  "config",
254
283
  `${what} is unavailable: this provider is on the unified run catalog but is not a ` +
255
284
  "native gateway provider, so it has no gateway path (no caching, server-side " +
256
- 'fallback, or transport:"gateway"). Use the default run path, or fallback.mode:"client".',
285
+ 'fallback, BYOK, or transport:"gateway"). Use the default run path, or fallback.mode:"client".',
257
286
  );
258
287
  }
259
288
 
@@ -277,6 +306,26 @@ export function selectTransport(
277
306
  return { transport: "gateway", resumeEnabled: false, warnings };
278
307
  }
279
308
 
309
+ // BYOK forwards the caller's own provider key, which only the gateway path
310
+ // supports — treat it like an explicit gateway transport (no resume-disabled
311
+ // warning; the caller opted into a gateway-only mode).
312
+ if (opts.byok) {
313
+ if (opts.transport === "run") {
314
+ throw new GatewayDelegateError(
315
+ "config",
316
+ 'transport:"run" cannot forward a BYOK key — BYOK is a gateway-path feature. ' +
317
+ 'Drop transport:"run" (or set transport:"gateway").',
318
+ );
319
+ }
320
+ if (resumeExplicitlyTrue) {
321
+ throw new GatewayDelegateError(
322
+ "config",
323
+ "byok cannot provide resume — cf-aig-run-id is only on the unified-billing run path.",
324
+ );
325
+ }
326
+ return { transport: "gateway", resumeEnabled: false, warnings };
327
+ }
328
+
280
329
  if (opts.transport === "run" && gatewayOnly) {
281
330
  throw new GatewayDelegateError(
282
331
  "config",
@@ -315,62 +364,14 @@ export function selectTransport(
315
364
  };
316
365
  }
317
366
 
318
- // ---------------------------------------------------------------------------
319
- // Errors
320
- // ---------------------------------------------------------------------------
321
-
322
- export type GatewayDelegateErrorKind = "config" | "dispatch" | "provider" | "resume-expired";
323
-
324
- export class GatewayDelegateError extends Error {
325
- readonly kind: GatewayDelegateErrorKind;
326
- override readonly cause?: unknown;
327
-
328
- constructor(kind: GatewayDelegateErrorKind, message: string, cause?: unknown) {
329
- super(message);
330
- this.name = "GatewayDelegateError";
331
- this.kind = kind;
332
- this.cause = cause;
333
- }
334
- }
335
-
336
367
  // ---------------------------------------------------------------------------
337
368
  // Dispatch internals
338
369
  // ---------------------------------------------------------------------------
339
370
 
340
- // Always stripped on the gateway path (transport-level headers the binding sets).
341
- const STRIP_HEADERS_BASE = new Set(["content-length", "host"]);
342
-
343
- interface GatewayEntry {
344
- provider: string;
345
- endpoint: string;
346
- headers: Record<string, string>;
347
- query: Record<string, unknown>;
348
- }
349
-
350
371
  interface AiGatewayRunner {
351
372
  run(body: unknown, options?: Record<string, unknown>): Promise<Response>;
352
373
  }
353
374
 
354
- function asText(body: BodyInit | null | undefined): string {
355
- if (typeof body === "string") return body;
356
- if (body instanceof Uint8Array) return new TextDecoder().decode(body);
357
- if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
358
- return "{}";
359
- }
360
-
361
- function headersToObject(h: HeadersInit | undefined): Record<string, string> {
362
- const out: Record<string, string> = {};
363
- if (!h) return out;
364
- if (h instanceof Headers) {
365
- for (const [k, v] of h) out[k] = v;
366
- } else if (Array.isArray(h)) {
367
- for (const [k, v] of h) out[k] = v;
368
- } else {
369
- Object.assign(out, h);
370
- }
371
- return out;
372
- }
373
-
374
375
  function normalizeGateway(gateway: GatewayOptions | string | undefined): {
375
376
  id: string;
376
377
  options: GatewayOptions;
@@ -517,8 +518,7 @@ export function createGatewayDelegate(config: GatewayDelegateConfig): GatewayDel
517
518
  return (slug, options = {}) => {
518
519
  // Client-side fallback: build a model per slug and wrap them so a failed
519
520
  // pre-stream dispatch falls through to the next, each on its own transport
520
- // (so resume is preserved per leg). Server-side fallback stays on the
521
- // gateway path inside makeGatewayFetch.
521
+ // (so resume is preserved per leg).
522
522
  if (options.fallback?.mode === "client") {
523
523
  const { fallback, ...rest } = options;
524
524
  const slugs = [slug, ...fallback.models];
@@ -528,6 +528,75 @@ export function createGatewayDelegate(config: GatewayDelegateConfig): GatewayDel
528
528
  });
529
529
  return createClientFallbackModel(legs);
530
530
  }
531
+
532
+ // Server-side fallback: all legs ship in one gateway run; `cf-aig-step`
533
+ // names the winner. Same-vendor legs are handled by makeGatewayFetch (it
534
+ // just swaps `model` in one body); cross-vendor legs need each leg's own
535
+ // builder to shape its native request, so route them through the
536
+ // capture/redispatch engine (ported from ai-gateway-provider).
537
+ if (options.fallback?.mode === "server") {
538
+ const slugs = [slug, ...options.fallback.models];
539
+ const resolved = slugs.map((s) => {
540
+ const parsed = parseSlug(s);
541
+ return { slug: s, modelId: parsed.modelId, info: resolveProvider(s, parsed) };
542
+ });
543
+ const crossVendor = resolved.some(
544
+ (r) => r.info.gatewayProviderId !== resolved[0]!.info.gatewayProviderId,
545
+ );
546
+ if (crossVendor) {
547
+ const resumeExplicitlyTrue = options.resume === true;
548
+ const effectiveOptions: DelegateCallOptions = {
549
+ ...options,
550
+ resume: options.resume ?? defaultResume,
551
+ onResumeExpired: options.onResumeExpired ?? config.onResumeExpired,
552
+ };
553
+ const primaryInfo = resolved[0]!.info;
554
+ // Forces the gateway path + disables resume (throws if resume:true).
555
+ const selection = selectTransport(
556
+ effectiveOptions,
557
+ resumeExplicitlyTrue,
558
+ primaryInfo.runCatalog,
559
+ primaryInfo.gatewayPath !== false,
560
+ );
561
+ for (const w of selection.warnings) console.warn(w);
562
+
563
+ const { id: gatewayId, options: gatewayOptions } = normalizeGateway(
564
+ options.gateway ?? config.gateway,
565
+ );
566
+
567
+ const legs: ServerFallbackLeg[] = resolved.map((r) => {
568
+ if (r.info.gatewayPath === false) {
569
+ throw new GatewayDelegateError(
570
+ "config",
571
+ `Server-side fallback cannot use "${r.slug}": it is on the unified ` +
572
+ "run catalog but has no native gateway path.",
573
+ );
574
+ }
575
+ const wire = r.info.wireFormat as WireFormat;
576
+ const plugin = plugins.get(wire);
577
+ if (!plugin) {
578
+ throw new GatewayDelegateError(
579
+ "config",
580
+ `No provider plugin for wire format "${wire}" (needed by "${r.slug}" ` +
581
+ `for server-side fallback). Registered: ` +
582
+ `${[...plugins.keys()].join(", ") || "<none>"}.`,
583
+ );
584
+ }
585
+ return { slug: r.slug, modelId: r.modelId, info: r.info, plugin };
586
+ });
587
+
588
+ return makeServerFallbackModel({
589
+ binding: config.binding,
590
+ gatewayId,
591
+ gatewayOptions,
592
+ legs,
593
+ opts: effectiveOptions,
594
+ selection,
595
+ callOptions: options,
596
+ });
597
+ }
598
+ }
599
+
531
600
  return buildOne(slug, options).model;
532
601
  };
533
602
  }
@@ -557,9 +626,32 @@ function mergeMetadata(
557
626
  return { ...base, ...override };
558
627
  }
559
628
 
560
- /** JSON-encode metadata for the `cf-aig-metadata` header (bigint → string). */
561
- function serializeMetadata(metadata: GatewayMetadata): string {
562
- return JSON.stringify(metadata, (_k, v) => (typeof v === "bigint" ? v.toString() : v));
629
+ /**
630
+ * Build the `cf-aig-*` request controls for a gateway-path entry. First-class
631
+ * call options (`cacheTtl`, `skipCache`, `collectLog`, `metadata`, `byokAlias`,
632
+ * `zdr`) are layered over the binding-style gateway options (`cacheKey`,
633
+ * `eventId`, `requestTimeoutMs`, `retries`) so the gateway path reaches parity
634
+ * with the run path, which forwards the whole `GatewayOptions` to `binding.run`.
635
+ */
636
+ function buildGatewayControls(
637
+ gatewayOptions: GatewayOptions,
638
+ opts: DelegateCallOptions,
639
+ ): GatewayCacheOptions {
640
+ const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
641
+ return {
642
+ cacheTtl: opts.cacheTtl,
643
+ skipCache: opts.skipCache,
644
+ ...(gatewayOptions.cacheKey !== undefined ? { cacheKey: gatewayOptions.cacheKey } : {}),
645
+ ...(metadata ? { metadata } : {}),
646
+ ...(opts.collectLog !== undefined ? { collectLog: opts.collectLog } : {}),
647
+ ...(gatewayOptions.eventId !== undefined ? { eventId: gatewayOptions.eventId } : {}),
648
+ ...(gatewayOptions.requestTimeoutMs !== undefined
649
+ ? { requestTimeoutMs: gatewayOptions.requestTimeoutMs }
650
+ : {}),
651
+ ...(gatewayOptions.retries ? { retries: gatewayOptions.retries } : {}),
652
+ ...(opts.byokAlias !== undefined ? { byokAlias: opts.byokAlias } : {}),
653
+ ...(opts.zdr !== undefined ? { zdr: opts.zdr } : {}),
654
+ };
563
655
  }
564
656
 
565
657
  function makeRunFetch(
@@ -582,10 +674,15 @@ function makeRunFetch(
582
674
  if (mergedMeta) mergedGateway.metadata = mergedMeta;
583
675
  if (opts.collectLog !== undefined) mergedGateway.collectLog = opts.collectLog;
584
676
 
677
+ // `zdr` is a Unified-Billing (run path) feature, but it's not a field on the
678
+ // binding's `GatewayOptions`, so pass it as a `cf-aig-zdr` extra header.
679
+ const extraHeaders: Record<string, string> = { ...opts.extraHeaders };
680
+ if (opts.zdr !== undefined) extraHeaders["cf-aig-zdr"] = String(opts.zdr);
681
+
585
682
  const runOptions = {
586
683
  gateway: mergedGateway,
587
684
  returnRawResponse: true,
588
- ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
685
+ ...(Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {}),
589
686
  ...(init?.signal ? { signal: init.signal } : {}),
590
687
  };
591
688
 
@@ -630,11 +727,6 @@ function makeGatewayFetch(
630
727
  selection: Selection,
631
728
  callOptions: DelegateCallOptions,
632
729
  ): typeof globalThis.fetch {
633
- // Strip the AI SDK's placeholder provider key unless BYOK forwards a real one;
634
- // unified billing / the gateway's stored key authenticates upstream otherwise.
635
- const strip = new Set(STRIP_HEADERS_BASE);
636
- if (!opts.byok) for (const h of info.authHeaders) strip.add(h.toLowerCase());
637
-
638
730
  return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
639
731
  const rawUrl = typeof input === "string" ? input : input.toString();
640
732
  // Host-strip to the provider's gateway-native endpoint. The registry
@@ -645,39 +737,33 @@ function makeGatewayFetch(
645
737
  : new URL(rawUrl).pathname.replace(/^\//, "") + (new URL(rawUrl).search || "");
646
738
  const body = JSON.parse(asText(init?.body)) as Record<string, unknown>;
647
739
 
648
- const headers: Record<string, string> = {};
649
- for (const [k, v] of Object.entries(headersToObject(init?.headers))) {
650
- if (!strip.has(k.toLowerCase())) headers[k] = v;
651
- }
652
- if (opts.extraHeaders) Object.assign(headers, opts.extraHeaders);
653
- // Best-effort gateway cache control (gateway-side config may still override).
654
- if (opts.cacheTtl !== undefined) headers["cf-aig-cache-ttl"] = String(opts.cacheTtl);
655
- if (opts.skipCache) headers["cf-aig-skip-cache"] = "true";
656
- // Gateway log controls (mirror the run path's typed gateway options).
657
- const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
658
- if (metadata) headers["cf-aig-metadata"] = serializeMetadata(metadata);
659
- if (opts.collectLog !== undefined) {
660
- headers["cf-aig-collect-log"] = String(opts.collectLog);
661
- }
662
-
663
- const primary: GatewayEntry = {
664
- provider: info.gatewayProviderId,
740
+ // Strip the AI SDK's placeholder provider key unless BYOK forwards a real
741
+ // one; unified billing / the gateway's stored key authenticates otherwise.
742
+ const primary: GatewayEntry = buildGatewayEntry({
743
+ providerId: info.gatewayProviderId,
665
744
  endpoint,
666
- headers,
667
- query: body,
668
- };
745
+ initHeaders: init?.headers,
746
+ body,
747
+ ...(opts.byok ? {} : { stripAuthHeaders: info.authHeaders }),
748
+ ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
749
+ cache: buildGatewayControls(gatewayOptions, opts),
750
+ });
669
751
  const entries: GatewayEntry[] = [primary];
670
752
 
753
+ // Same-vendor server fallback: every leg shares this provider + endpoint, so
754
+ // each is just the primary entry with `model` swapped. (Cross-vendor chains
755
+ // never reach here — the delegate routes them through makeServerFallbackModel,
756
+ // which captures each leg's native request; this stays a defensive guard.)
671
757
  if (opts.fallback?.mode === "server") {
672
758
  for (const fb of opts.fallback.models) {
673
759
  const fbParsed = parseSlug(fb);
674
760
  const fbInfo = resolveProvider(fb, fbParsed);
675
761
  if (fbInfo.gatewayProviderId !== info.gatewayProviderId) {
676
762
  throw new GatewayDelegateError(
677
- "config",
678
- `Cross-vendor server-side fallback (${info.gatewayProviderId} → ` +
679
- `${fbInfo.gatewayProviderId}) is not supported yet. Use fallback.mode:"client", ` +
680
- "or same-vendor fallback models.",
763
+ "dispatch",
764
+ `Internal: cross-vendor server fallback (${info.gatewayProviderId} → ` +
765
+ `${fbInfo.gatewayProviderId}) reached the same-vendor gateway fetch. ` +
766
+ "This should have been routed through makeServerFallbackModel.",
681
767
  );
682
768
  }
683
769
  entries.push({ ...primary, query: { ...body, model: fbParsed.modelId } });
@@ -694,3 +780,146 @@ function makeGatewayFetch(
694
780
  return resp;
695
781
  }) as typeof globalThis.fetch;
696
782
  }
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // Cross-vendor server-side fallback (ported from ai-gateway-provider)
786
+ // ---------------------------------------------------------------------------
787
+
788
+ /** Sentinel thrown by the capture fetch to stop a leg before it hits the network. */
789
+ class ServerFallbackCaptureStop extends Error {}
790
+
791
+ /** One leg of a server-side fallback chain (gateway path). */
792
+ interface ServerFallbackLeg {
793
+ slug: string;
794
+ modelId: string;
795
+ info: GatewayProviderInfo;
796
+ plugin: ProviderPlugin;
797
+ }
798
+
799
+ /**
800
+ * Cross-vendor server-side fallback via the gateway universal endpoint.
801
+ *
802
+ * Unlike same-vendor fallback (which just swaps `model` in one body), legs here
803
+ * are DIFFERENT providers with possibly different wire formats, so each leg's own
804
+ * `@ai-sdk` model must shape its native request. Mirrors ai-gateway-provider's
805
+ * `processModelRequest`: run each leg's builder with a capture `fetch` that
806
+ * sentinel-throws before the network, reshape each captured request into a
807
+ * `{ provider, endpoint, headers, query }` entry, dispatch all N as one
808
+ * `env.AI.gateway(id).run([...])`, then read `cf-aig-step` to find the leg the
809
+ * gateway actually served and feed the raw response back into that leg's model.
810
+ */
811
+ function makeServerFallbackModel(params: {
812
+ binding: Ai;
813
+ gatewayId: string;
814
+ gatewayOptions: GatewayOptions;
815
+ legs: ServerFallbackLeg[];
816
+ opts: DelegateCallOptions;
817
+ selection: Selection;
818
+ callOptions: DelegateCallOptions;
819
+ }): LanguageModelV3 {
820
+ const { binding, gatewayId, gatewayOptions, legs, opts, selection, callOptions } = params;
821
+ const first = legs[0]!;
822
+
823
+ // A throwaway model only used to read static metadata (provider/modelId/urls);
824
+ // its fetch is never invoked for those synchronous reads.
825
+ const refModel = first.plugin.create({
826
+ modelId: first.modelId,
827
+ fetch: (async () => {
828
+ throw new ServerFallbackCaptureStop();
829
+ }) as typeof globalThis.fetch,
830
+ ...(first.info.baseURL ? { baseURL: first.info.baseURL } : {}),
831
+ });
832
+
833
+ const cache = buildGatewayControls(gatewayOptions, opts);
834
+
835
+ const dispatch = async (
836
+ method: "doGenerate" | "doStream",
837
+ options: LanguageModelV3CallOptions,
838
+ ): Promise<unknown> => {
839
+ // 1) Capture each leg's native request without hitting the network.
840
+ const entries: GatewayEntry[] = [];
841
+ for (const leg of legs) {
842
+ let captured: { url: string; init?: RequestInit } | undefined;
843
+ const captureFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
844
+ captured = {
845
+ url: typeof input === "string" ? input : input.toString(),
846
+ init,
847
+ };
848
+ throw new ServerFallbackCaptureStop();
849
+ }) as typeof globalThis.fetch;
850
+
851
+ const model = leg.plugin.create({
852
+ modelId: leg.modelId,
853
+ fetch: captureFetch,
854
+ ...(leg.info.baseURL ? { baseURL: leg.info.baseURL } : {}),
855
+ });
856
+ try {
857
+ await (model[method] as (o: LanguageModelV3CallOptions) => Promise<unknown>)(
858
+ options,
859
+ );
860
+ } catch (e) {
861
+ if (!(e instanceof ServerFallbackCaptureStop)) throw e;
862
+ }
863
+ if (!captured) {
864
+ throw new GatewayDelegateError(
865
+ "dispatch",
866
+ `Server-side fallback leg "${leg.slug}" produced no request to dispatch.`,
867
+ );
868
+ }
869
+
870
+ const url = captured.url;
871
+ const endpoint = leg.info.transformEndpoint
872
+ ? leg.info.transformEndpoint(url)
873
+ : new URL(url).pathname.replace(/^\//, "") + (new URL(url).search || "");
874
+ const body = JSON.parse(asText(captured.init?.body)) as Record<string, unknown>;
875
+
876
+ entries.push(
877
+ buildGatewayEntry({
878
+ providerId: leg.info.gatewayProviderId,
879
+ endpoint,
880
+ initHeaders: captured.init?.headers,
881
+ body,
882
+ ...(opts.byok ? {} : { stripAuthHeaders: leg.info.authHeaders }),
883
+ ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
884
+ cache,
885
+ }),
886
+ );
887
+ }
888
+
889
+ // 2) One gateway run with all legs; `cf-aig-step` names the winner.
890
+ const gw = (binding as unknown as { gateway(id: string): AiGatewayRunner }).gateway(
891
+ gatewayId,
892
+ );
893
+ const abortSignal = (options as { abortSignal?: AbortSignal }).abortSignal;
894
+ const runOptions: Record<string, unknown> = {};
895
+ if (abortSignal) runOptions.signal = abortSignal;
896
+ const resp = await gw.run(entries, runOptions);
897
+ fireDispatch(resp, selection, callOptions);
898
+
899
+ // 3) Feed the raw winner response into its own model so its parser shapes
900
+ // the result for that leg's wire format.
901
+ const step = Number.parseInt(resp.headers.get("cf-aig-step") ?? "0", 10);
902
+ const winner = legs[step] ?? first;
903
+ const winnerModel = winner.plugin.create({
904
+ modelId: winner.modelId,
905
+ fetch: (async () => resp) as typeof globalThis.fetch,
906
+ ...(winner.info.baseURL ? { baseURL: winner.info.baseURL } : {}),
907
+ });
908
+ return (winnerModel[method] as (o: LanguageModelV3CallOptions) => Promise<unknown>)(
909
+ options,
910
+ );
911
+ };
912
+
913
+ return {
914
+ specificationVersion: "v3",
915
+ provider: refModel.provider,
916
+ modelId: refModel.modelId,
917
+ supportedUrls: refModel.supportedUrls,
918
+ doGenerate(options) {
919
+ return dispatch("doGenerate", options) as ReturnType<LanguageModelV3["doGenerate"]>;
920
+ },
921
+ doStream(options) {
922
+ return dispatch("doStream", options) as ReturnType<LanguageModelV3["doStream"]>;
923
+ },
924
+ } as LanguageModelV3;
925
+ }
@@ -1,3 +1,4 @@
1
+ import { asText, buildGatewayEntry } from "@cloudflare/gateway-core";
1
2
  import { WorkersAIGatewayError } from "./errors";
2
3
  import { detectProviderByUrl, type GatewayProviderInfo } from "./gateway-providers";
3
4
 
@@ -47,32 +48,10 @@ export interface GatewayFetchConfig {
47
48
  skipCache?: boolean;
48
49
  }
49
50
 
50
- const STRIP_HEADERS_BASE = new Set(["content-length", "host"]);
51
-
52
51
  interface AiGatewayRunner {
53
52
  run(body: unknown, options?: Record<string, unknown>): Promise<Response>;
54
53
  }
55
54
 
56
- function asText(body: BodyInit | null | undefined): string {
57
- if (typeof body === "string") return body;
58
- if (body instanceof Uint8Array) return new TextDecoder().decode(body);
59
- if (body instanceof ArrayBuffer) return new TextDecoder().decode(body);
60
- return "{}";
61
- }
62
-
63
- function headersToObject(h: HeadersInit | undefined): Record<string, string> {
64
- const out: Record<string, string> = {};
65
- if (!h) return out;
66
- if (h instanceof Headers) {
67
- for (const [k, v] of h) out[k] = v;
68
- } else if (Array.isArray(h)) {
69
- for (const [k, v] of h) out[k] = v;
70
- } else {
71
- Object.assign(out, h);
72
- }
73
- return out;
74
- }
75
-
76
55
  /**
77
56
  * A `fetch` that dispatches the wrapped provider's request through AI Gateway.
78
57
  * Detects the gateway provider id from the request URL (or uses `config.provider`),
@@ -117,18 +96,15 @@ export function createGatewayFetch(config: GatewayFetchConfig): typeof globalThi
117
96
  : rawUrl.replace(/^https?:\/\/[^/]+\//, "");
118
97
  const body = JSON.parse(asText(init?.body)) as Record<string, unknown>;
119
98
 
120
- const strip = new Set(STRIP_HEADERS_BASE);
121
- if (!config.byok && info) for (const h of info.authHeaders) strip.add(h.toLowerCase());
122
-
123
- const headers: Record<string, string> = {};
124
- for (const [k, v] of Object.entries(headersToObject(init?.headers))) {
125
- if (!strip.has(k.toLowerCase())) headers[k] = v;
126
- }
127
- if (config.extraHeaders) Object.assign(headers, config.extraHeaders);
128
- if (config.cacheTtl !== undefined) headers["cf-aig-cache-ttl"] = String(config.cacheTtl);
129
- if (config.skipCache) headers["cf-aig-skip-cache"] = "true";
130
-
131
- const entry = { provider: providerId, endpoint, headers, query: body };
99
+ const entry = buildGatewayEntry({
100
+ providerId,
101
+ endpoint,
102
+ initHeaders: init?.headers,
103
+ body,
104
+ ...(!config.byok && info ? { stripAuthHeaders: info.authHeaders } : {}),
105
+ ...(config.extraHeaders ? { extraHeaders: config.extraHeaders } : {}),
106
+ cache: { cacheTtl: config.cacheTtl, skipCache: config.skipCache },
107
+ });
132
108
  const gw = (config.binding as unknown as { gateway(id: string): AiGatewayRunner }).gateway(
133
109
  gatewayId,
134
110
  );