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.
- package/README.md +24 -15
- package/dist/anthropic.d.mts +1 -1
- package/dist/{gateway-delegate-BfaUTwDZ.d.mts → gateway-delegate-BAALxuBO.d.mts} +75 -11
- package/dist/{gateway-provider-1USFWm7c.mjs → gateway-provider-BjPLZLob.mjs} +560 -168
- package/dist/gateway-provider-BjPLZLob.mjs.map +1 -0
- package/dist/gateway-provider.mjs +1 -1
- package/dist/google.d.mts +1 -1
- package/dist/index.d.mts +4 -3
- package/dist/index.mjs +388 -279
- package/dist/index.mjs.map +1 -1
- package/dist/openai.d.mts +1 -1
- package/package.json +5 -4
- package/src/gateway-delegate.ts +290 -86
- package/src/gateway-provider.ts +10 -34
- package/src/gateway-providers.ts +13 -455
- package/src/index.ts +144 -8
- package/src/resumable-stream.ts +9 -221
- package/src/streaming.ts +1 -41
- package/src/utils.ts +40 -125
- package/src/workersai-chat-language-model.ts +37 -16
- package/src/workersai-embedding-model.ts +22 -11
- package/src/workersai-error.ts +70 -0
- package/src/workersai-image-model.ts +19 -11
- package/src/workersai-reranking-model.ts +16 -5
- package/src/workersai-speech-model.ts +35 -13
- package/src/workersai-transcription-model.ts +15 -4
- package/dist/gateway-provider-1USFWm7c.mjs.map +0 -1
package/dist/openai.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workers-ai-provider",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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
|
|
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",
|
package/src/gateway-delegate.ts
CHANGED
|
@@ -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 "
|
|
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).
|
|
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
|
-
/**
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
678
|
-
`
|
|
679
|
-
`${fbInfo.gatewayProviderId})
|
|
680
|
-
"
|
|
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
|
+
}
|
package/src/gateway-provider.ts
CHANGED
|
@@ -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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
);
|