workers-ai-provider 3.2.1 → 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.
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-BAALxuBO.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.0",
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
  /**
@@ -315,62 +339,14 @@ export function selectTransport(
315
339
  };
316
340
  }
317
341
 
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
342
  // ---------------------------------------------------------------------------
337
343
  // Dispatch internals
338
344
  // ---------------------------------------------------------------------------
339
345
 
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
346
  interface AiGatewayRunner {
351
347
  run(body: unknown, options?: Record<string, unknown>): Promise<Response>;
352
348
  }
353
349
 
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
350
  function normalizeGateway(gateway: GatewayOptions | string | undefined): {
375
351
  id: string;
376
352
  options: GatewayOptions;
@@ -517,8 +493,7 @@ export function createGatewayDelegate(config: GatewayDelegateConfig): GatewayDel
517
493
  return (slug, options = {}) => {
518
494
  // Client-side fallback: build a model per slug and wrap them so a failed
519
495
  // 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.
496
+ // (so resume is preserved per leg).
522
497
  if (options.fallback?.mode === "client") {
523
498
  const { fallback, ...rest } = options;
524
499
  const slugs = [slug, ...fallback.models];
@@ -528,6 +503,75 @@ export function createGatewayDelegate(config: GatewayDelegateConfig): GatewayDel
528
503
  });
529
504
  return createClientFallbackModel(legs);
530
505
  }
506
+
507
+ // Server-side fallback: all legs ship in one gateway run; `cf-aig-step`
508
+ // names the winner. Same-vendor legs are handled by makeGatewayFetch (it
509
+ // just swaps `model` in one body); cross-vendor legs need each leg's own
510
+ // builder to shape its native request, so route them through the
511
+ // capture/redispatch engine (ported from ai-gateway-provider).
512
+ if (options.fallback?.mode === "server") {
513
+ const slugs = [slug, ...options.fallback.models];
514
+ const resolved = slugs.map((s) => {
515
+ const parsed = parseSlug(s);
516
+ return { slug: s, modelId: parsed.modelId, info: resolveProvider(s, parsed) };
517
+ });
518
+ const crossVendor = resolved.some(
519
+ (r) => r.info.gatewayProviderId !== resolved[0]!.info.gatewayProviderId,
520
+ );
521
+ if (crossVendor) {
522
+ const resumeExplicitlyTrue = options.resume === true;
523
+ const effectiveOptions: DelegateCallOptions = {
524
+ ...options,
525
+ resume: options.resume ?? defaultResume,
526
+ onResumeExpired: options.onResumeExpired ?? config.onResumeExpired,
527
+ };
528
+ const primaryInfo = resolved[0]!.info;
529
+ // Forces the gateway path + disables resume (throws if resume:true).
530
+ const selection = selectTransport(
531
+ effectiveOptions,
532
+ resumeExplicitlyTrue,
533
+ primaryInfo.runCatalog,
534
+ primaryInfo.gatewayPath !== false,
535
+ );
536
+ for (const w of selection.warnings) console.warn(w);
537
+
538
+ const { id: gatewayId, options: gatewayOptions } = normalizeGateway(
539
+ options.gateway ?? config.gateway,
540
+ );
541
+
542
+ const legs: ServerFallbackLeg[] = resolved.map((r) => {
543
+ if (r.info.gatewayPath === false) {
544
+ throw new GatewayDelegateError(
545
+ "config",
546
+ `Server-side fallback cannot use "${r.slug}": it is on the unified ` +
547
+ "run catalog but has no native gateway path.",
548
+ );
549
+ }
550
+ const wire = r.info.wireFormat as WireFormat;
551
+ const plugin = plugins.get(wire);
552
+ if (!plugin) {
553
+ throw new GatewayDelegateError(
554
+ "config",
555
+ `No provider plugin for wire format "${wire}" (needed by "${r.slug}" ` +
556
+ `for server-side fallback). Registered: ` +
557
+ `${[...plugins.keys()].join(", ") || "<none>"}.`,
558
+ );
559
+ }
560
+ return { slug: r.slug, modelId: r.modelId, info: r.info, plugin };
561
+ });
562
+
563
+ return makeServerFallbackModel({
564
+ binding: config.binding,
565
+ gatewayId,
566
+ gatewayOptions,
567
+ legs,
568
+ opts: effectiveOptions,
569
+ selection,
570
+ callOptions: options,
571
+ });
572
+ }
573
+ }
574
+
531
575
  return buildOne(slug, options).model;
532
576
  };
533
577
  }
@@ -557,9 +601,32 @@ function mergeMetadata(
557
601
  return { ...base, ...override };
558
602
  }
559
603
 
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));
604
+ /**
605
+ * Build the `cf-aig-*` request controls for a gateway-path entry. First-class
606
+ * call options (`cacheTtl`, `skipCache`, `collectLog`, `metadata`, `byokAlias`,
607
+ * `zdr`) are layered over the binding-style gateway options (`cacheKey`,
608
+ * `eventId`, `requestTimeoutMs`, `retries`) so the gateway path reaches parity
609
+ * with the run path, which forwards the whole `GatewayOptions` to `binding.run`.
610
+ */
611
+ function buildGatewayControls(
612
+ gatewayOptions: GatewayOptions,
613
+ opts: DelegateCallOptions,
614
+ ): GatewayCacheOptions {
615
+ const metadata = mergeMetadata(gatewayOptions.metadata, opts.metadata);
616
+ return {
617
+ cacheTtl: opts.cacheTtl,
618
+ skipCache: opts.skipCache,
619
+ ...(gatewayOptions.cacheKey !== undefined ? { cacheKey: gatewayOptions.cacheKey } : {}),
620
+ ...(metadata ? { metadata } : {}),
621
+ ...(opts.collectLog !== undefined ? { collectLog: opts.collectLog } : {}),
622
+ ...(gatewayOptions.eventId !== undefined ? { eventId: gatewayOptions.eventId } : {}),
623
+ ...(gatewayOptions.requestTimeoutMs !== undefined
624
+ ? { requestTimeoutMs: gatewayOptions.requestTimeoutMs }
625
+ : {}),
626
+ ...(gatewayOptions.retries ? { retries: gatewayOptions.retries } : {}),
627
+ ...(opts.byokAlias !== undefined ? { byokAlias: opts.byokAlias } : {}),
628
+ ...(opts.zdr !== undefined ? { zdr: opts.zdr } : {}),
629
+ };
563
630
  }
564
631
 
565
632
  function makeRunFetch(
@@ -582,10 +649,15 @@ function makeRunFetch(
582
649
  if (mergedMeta) mergedGateway.metadata = mergedMeta;
583
650
  if (opts.collectLog !== undefined) mergedGateway.collectLog = opts.collectLog;
584
651
 
652
+ // `zdr` is a Unified-Billing (run path) feature, but it's not a field on the
653
+ // binding's `GatewayOptions`, so pass it as a `cf-aig-zdr` extra header.
654
+ const extraHeaders: Record<string, string> = { ...opts.extraHeaders };
655
+ if (opts.zdr !== undefined) extraHeaders["cf-aig-zdr"] = String(opts.zdr);
656
+
585
657
  const runOptions = {
586
658
  gateway: mergedGateway,
587
659
  returnRawResponse: true,
588
- ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
660
+ ...(Object.keys(extraHeaders).length > 0 ? { extraHeaders } : {}),
589
661
  ...(init?.signal ? { signal: init.signal } : {}),
590
662
  };
591
663
 
@@ -630,11 +702,6 @@ function makeGatewayFetch(
630
702
  selection: Selection,
631
703
  callOptions: DelegateCallOptions,
632
704
  ): 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
705
  return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
639
706
  const rawUrl = typeof input === "string" ? input : input.toString();
640
707
  // Host-strip to the provider's gateway-native endpoint. The registry
@@ -645,39 +712,33 @@ function makeGatewayFetch(
645
712
  : new URL(rawUrl).pathname.replace(/^\//, "") + (new URL(rawUrl).search || "");
646
713
  const body = JSON.parse(asText(init?.body)) as Record<string, unknown>;
647
714
 
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,
715
+ // Strip the AI SDK's placeholder provider key unless BYOK forwards a real
716
+ // one; unified billing / the gateway's stored key authenticates otherwise.
717
+ const primary: GatewayEntry = buildGatewayEntry({
718
+ providerId: info.gatewayProviderId,
665
719
  endpoint,
666
- headers,
667
- query: body,
668
- };
720
+ initHeaders: init?.headers,
721
+ body,
722
+ ...(opts.byok ? {} : { stripAuthHeaders: info.authHeaders }),
723
+ ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
724
+ cache: buildGatewayControls(gatewayOptions, opts),
725
+ });
669
726
  const entries: GatewayEntry[] = [primary];
670
727
 
728
+ // Same-vendor server fallback: every leg shares this provider + endpoint, so
729
+ // each is just the primary entry with `model` swapped. (Cross-vendor chains
730
+ // never reach here — the delegate routes them through makeServerFallbackModel,
731
+ // which captures each leg's native request; this stays a defensive guard.)
671
732
  if (opts.fallback?.mode === "server") {
672
733
  for (const fb of opts.fallback.models) {
673
734
  const fbParsed = parseSlug(fb);
674
735
  const fbInfo = resolveProvider(fb, fbParsed);
675
736
  if (fbInfo.gatewayProviderId !== info.gatewayProviderId) {
676
737
  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.",
738
+ "dispatch",
739
+ `Internal: cross-vendor server fallback (${info.gatewayProviderId} → ` +
740
+ `${fbInfo.gatewayProviderId}) reached the same-vendor gateway fetch. ` +
741
+ "This should have been routed through makeServerFallbackModel.",
681
742
  );
682
743
  }
683
744
  entries.push({ ...primary, query: { ...body, model: fbParsed.modelId } });
@@ -694,3 +755,146 @@ function makeGatewayFetch(
694
755
  return resp;
695
756
  }) as typeof globalThis.fetch;
696
757
  }
758
+
759
+ // ---------------------------------------------------------------------------
760
+ // Cross-vendor server-side fallback (ported from ai-gateway-provider)
761
+ // ---------------------------------------------------------------------------
762
+
763
+ /** Sentinel thrown by the capture fetch to stop a leg before it hits the network. */
764
+ class ServerFallbackCaptureStop extends Error {}
765
+
766
+ /** One leg of a server-side fallback chain (gateway path). */
767
+ interface ServerFallbackLeg {
768
+ slug: string;
769
+ modelId: string;
770
+ info: GatewayProviderInfo;
771
+ plugin: ProviderPlugin;
772
+ }
773
+
774
+ /**
775
+ * Cross-vendor server-side fallback via the gateway universal endpoint.
776
+ *
777
+ * Unlike same-vendor fallback (which just swaps `model` in one body), legs here
778
+ * are DIFFERENT providers with possibly different wire formats, so each leg's own
779
+ * `@ai-sdk` model must shape its native request. Mirrors ai-gateway-provider's
780
+ * `processModelRequest`: run each leg's builder with a capture `fetch` that
781
+ * sentinel-throws before the network, reshape each captured request into a
782
+ * `{ provider, endpoint, headers, query }` entry, dispatch all N as one
783
+ * `env.AI.gateway(id).run([...])`, then read `cf-aig-step` to find the leg the
784
+ * gateway actually served and feed the raw response back into that leg's model.
785
+ */
786
+ function makeServerFallbackModel(params: {
787
+ binding: Ai;
788
+ gatewayId: string;
789
+ gatewayOptions: GatewayOptions;
790
+ legs: ServerFallbackLeg[];
791
+ opts: DelegateCallOptions;
792
+ selection: Selection;
793
+ callOptions: DelegateCallOptions;
794
+ }): LanguageModelV3 {
795
+ const { binding, gatewayId, gatewayOptions, legs, opts, selection, callOptions } = params;
796
+ const first = legs[0]!;
797
+
798
+ // A throwaway model only used to read static metadata (provider/modelId/urls);
799
+ // its fetch is never invoked for those synchronous reads.
800
+ const refModel = first.plugin.create({
801
+ modelId: first.modelId,
802
+ fetch: (async () => {
803
+ throw new ServerFallbackCaptureStop();
804
+ }) as typeof globalThis.fetch,
805
+ ...(first.info.baseURL ? { baseURL: first.info.baseURL } : {}),
806
+ });
807
+
808
+ const cache = buildGatewayControls(gatewayOptions, opts);
809
+
810
+ const dispatch = async (
811
+ method: "doGenerate" | "doStream",
812
+ options: LanguageModelV3CallOptions,
813
+ ): Promise<unknown> => {
814
+ // 1) Capture each leg's native request without hitting the network.
815
+ const entries: GatewayEntry[] = [];
816
+ for (const leg of legs) {
817
+ let captured: { url: string; init?: RequestInit } | undefined;
818
+ const captureFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
819
+ captured = {
820
+ url: typeof input === "string" ? input : input.toString(),
821
+ init,
822
+ };
823
+ throw new ServerFallbackCaptureStop();
824
+ }) as typeof globalThis.fetch;
825
+
826
+ const model = leg.plugin.create({
827
+ modelId: leg.modelId,
828
+ fetch: captureFetch,
829
+ ...(leg.info.baseURL ? { baseURL: leg.info.baseURL } : {}),
830
+ });
831
+ try {
832
+ await (model[method] as (o: LanguageModelV3CallOptions) => Promise<unknown>)(
833
+ options,
834
+ );
835
+ } catch (e) {
836
+ if (!(e instanceof ServerFallbackCaptureStop)) throw e;
837
+ }
838
+ if (!captured) {
839
+ throw new GatewayDelegateError(
840
+ "dispatch",
841
+ `Server-side fallback leg "${leg.slug}" produced no request to dispatch.`,
842
+ );
843
+ }
844
+
845
+ const url = captured.url;
846
+ const endpoint = leg.info.transformEndpoint
847
+ ? leg.info.transformEndpoint(url)
848
+ : new URL(url).pathname.replace(/^\//, "") + (new URL(url).search || "");
849
+ const body = JSON.parse(asText(captured.init?.body)) as Record<string, unknown>;
850
+
851
+ entries.push(
852
+ buildGatewayEntry({
853
+ providerId: leg.info.gatewayProviderId,
854
+ endpoint,
855
+ initHeaders: captured.init?.headers,
856
+ body,
857
+ ...(opts.byok ? {} : { stripAuthHeaders: leg.info.authHeaders }),
858
+ ...(opts.extraHeaders ? { extraHeaders: opts.extraHeaders } : {}),
859
+ cache,
860
+ }),
861
+ );
862
+ }
863
+
864
+ // 2) One gateway run with all legs; `cf-aig-step` names the winner.
865
+ const gw = (binding as unknown as { gateway(id: string): AiGatewayRunner }).gateway(
866
+ gatewayId,
867
+ );
868
+ const abortSignal = (options as { abortSignal?: AbortSignal }).abortSignal;
869
+ const runOptions: Record<string, unknown> = {};
870
+ if (abortSignal) runOptions.signal = abortSignal;
871
+ const resp = await gw.run(entries, runOptions);
872
+ fireDispatch(resp, selection, callOptions);
873
+
874
+ // 3) Feed the raw winner response into its own model so its parser shapes
875
+ // the result for that leg's wire format.
876
+ const step = Number.parseInt(resp.headers.get("cf-aig-step") ?? "0", 10);
877
+ const winner = legs[step] ?? first;
878
+ const winnerModel = winner.plugin.create({
879
+ modelId: winner.modelId,
880
+ fetch: (async () => resp) as typeof globalThis.fetch,
881
+ ...(winner.info.baseURL ? { baseURL: winner.info.baseURL } : {}),
882
+ });
883
+ return (winnerModel[method] as (o: LanguageModelV3CallOptions) => Promise<unknown>)(
884
+ options,
885
+ );
886
+ };
887
+
888
+ return {
889
+ specificationVersion: "v3",
890
+ provider: refModel.provider,
891
+ modelId: refModel.modelId,
892
+ supportedUrls: refModel.supportedUrls,
893
+ doGenerate(options) {
894
+ return dispatch("doGenerate", options) as ReturnType<LanguageModelV3["doGenerate"]>;
895
+ },
896
+ doStream(options) {
897
+ return dispatch("doStream", options) as ReturnType<LanguageModelV3["doStream"]>;
898
+ },
899
+ } as LanguageModelV3;
900
+ }
@@ -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
  );