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/README.md +28 -19
- package/dist/anthropic.d.mts +1 -1
- package/dist/{gateway-delegate-BfaUTwDZ.d.mts → gateway-delegate-D_zkIp5r.d.mts} +90 -15
- package/dist/{gateway-provider-1USFWm7c.mjs → gateway-provider-CQU-v2IO.mjs} +562 -170
- package/dist/gateway-provider-CQU-v2IO.mjs.map +1 -0
- package/dist/gateway-provider.mjs +1 -1
- package/dist/google.d.mts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +348 -284
- package/dist/index.mjs.map +1 -1
- package/dist/openai.d.mts +1 -1
- package/package.json +5 -4
- package/src/gateway-delegate.ts +318 -89
- package/src/gateway-provider.ts +10 -34
- package/src/gateway-providers.ts +13 -455
- package/src/index.ts +64 -20
- 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.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.
|
|
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
|
/**
|
|
@@ -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 (
|
|
251
|
-
|
|
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).
|
|
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
|
-
/**
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
"
|
|
678
|
-
`
|
|
679
|
-
`${fbInfo.gatewayProviderId})
|
|
680
|
-
"
|
|
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
|
+
}
|
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
|
);
|