workers-ai-provider
Version:
Workers AI Provider for the vercel AI SDK
144 lines (135 loc) • 5.22 kB
text/typescript
import { asText, buildGatewayEntry } from "@cloudflare/gateway-core";
import { WorkersAIGatewayError } from "./errors";
import { detectProviderByUrl, type GatewayProviderInfo } from "./gateway-providers";
/**
* Bring-your-own-provider: route any `@ai-sdk/*` provider's HTTP traffic through
* Cloudflare AI Gateway, without the catalog slug delegate. The provider keeps
* its own request/response shaping; this only swaps the transport.
*
* Use it for providers the slug delegate cannot auto-wire (bedrock, replicate,
* audio/image providers, anything provider-native), or when you want full control
* of the underlying `@ai-sdk` provider. This is the gateway path only — BYOK and
* caching are available, resume (`cf-aig-run-id`) is not.
*
* @example
* ```ts
* import { createOpenAI } from "@ai-sdk/openai";
* import { createGatewayFetch } from "workers-ai-provider/gateway";
*
* const openai = createOpenAI({
* apiKey: env.OPENAI_API_KEY, // forwarded when byok: true
* fetch: createGatewayFetch({ binding: env.AI, gateway: "my-gw", byok: true }),
* });
* const model = openai("gpt-5");
* ```
*/
export interface GatewayFetchConfig {
/** A Cloudflare AI binding (e.g. `env.AI`). */
binding: Ai;
/** Gateway id (or options). */
gateway: GatewayOptions | string;
/**
* Force a gateway provider id instead of detecting it from the request URL.
* Required when the wrapped provider's host is not in the registry.
*/
provider?: string;
/**
* Forward the upstream provider key (Authorization / x-api-key / …) instead of
* stripping it. Required for BYOK providers. Defaults to `false` (strip, so
* unified billing / the gateway's stored key applies).
*/
byok?: boolean;
/** Extra headers added to every gateway entry. */
extraHeaders?: Record<string, string>;
/** Gateway-path response caching (seconds). */
cacheTtl?: number;
/** Bypass gateway cache. */
skipCache?: boolean;
}
interface AiGatewayRunner {
run(body: unknown, options?: Record<string, unknown>): Promise<Response>;
}
/**
* A `fetch` that dispatches the wrapped provider's request through AI Gateway.
* Detects the gateway provider id from the request URL (or uses `config.provider`),
* strips the provider host to the endpoint path, and forwards the body verbatim.
*/
export function createGatewayFetch(config: GatewayFetchConfig): typeof globalThis.fetch {
if (!config?.binding) {
throw new WorkersAIGatewayError(
"gateway-error",
"createGatewayFetch requires a `binding` (e.g. { binding: env.AI }).",
);
}
const gatewayId = typeof config.gateway === "string" ? config.gateway : config.gateway?.id;
if (!gatewayId) {
throw new WorkersAIGatewayError(
"gateway-error",
'createGatewayFetch requires a `gateway` id (e.g. gateway: "my-gateway").',
);
}
return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const rawUrl = typeof input === "string" ? input : input.toString();
let info: GatewayProviderInfo | undefined;
if (config.provider) {
info = undefined; // explicit provider id; no registry lookup needed
} else {
info = detectProviderByUrl(rawUrl);
if (!info) {
throw new WorkersAIGatewayError(
"gateway-error",
`Could not detect a gateway provider from URL "${rawUrl}". ` +
'Pass `provider: "<gateway-provider-id>"` explicitly.',
{ recoverable: false },
);
}
}
const providerId = config.provider ?? (info as GatewayProviderInfo).gatewayProviderId;
const endpoint = info?.transformEndpoint
? info.transformEndpoint(rawUrl)
: rawUrl.replace(/^https?:\/\/[^/]+\//, "");
const body = JSON.parse(asText(init?.body)) as Record<string, unknown>;
const entry = buildGatewayEntry({
providerId,
endpoint,
initHeaders: init?.headers,
body,
...(!config.byok && info ? { stripAuthHeaders: info.authHeaders } : {}),
...(config.extraHeaders ? { extraHeaders: config.extraHeaders } : {}),
cache: { cacheTtl: config.cacheTtl, skipCache: config.skipCache },
});
const gw = (config.binding as unknown as { gateway(id: string): AiGatewayRunner }).gateway(
gatewayId,
);
const runOptions: Record<string, unknown> = {};
if (init?.signal) runOptions.signal = init.signal;
return gw.run([entry], runOptions);
}) as typeof globalThis.fetch;
}
/**
* Wrap an `@ai-sdk/*` provider factory so its traffic flows through AI Gateway.
* A thin convenience over {@link createGatewayFetch} — it injects the gateway
* `fetch` (and a placeholder `apiKey` unless you supply one for BYOK).
*
* @example
* ```ts
* import { createOpenAI } from "@ai-sdk/openai";
* import { createGatewayProvider } from "workers-ai-provider/gateway";
*
* const openai = createGatewayProvider(createOpenAI, {
* binding: env.AI,
* gateway: "my-gw",
* });
* const model = openai("gpt-5");
* ```
*/
export function createGatewayProvider<T>(
factory: (opts: { apiKey?: string; baseURL?: string; fetch: typeof globalThis.fetch }) => T,
config: GatewayFetchConfig & { apiKey?: string; baseURL?: string },
): T {
return factory({
apiKey: config.apiKey ?? "unused",
...(config.baseURL ? { baseURL: config.baseURL } : {}),
fetch: createGatewayFetch(config),
});
}