Skip to content

AI SDK

Last updated View as MarkdownAgent setup

agents/models/ai-sdk is an AI SDK ↗︎ provider for Workers AI and AI Gateway. createAI returns a full ProviderV4, so its models work anywhere the AI SDK takes a model: generateText, streamText, tools, structured output, and Think.

The provider takes two kinds of model. Both go through the AI binding, or over HTTP with an account id and API token:

// A string runs on env.AI.run.
ai("@cf/zai-org/glm-4.7-flash"); // Workers AI
ai("anthropic/claude-opus-5.5"); // AI Gateway catalog
ai("dynamic/support"); // AI Gateway dynamic route

// A vendor's own model object goes to AI Gateway's provider-native endpoint.
ai(anthropic("claude-opus-5-5"));

A string needs no vendor package and no vendor key. A model object keeps the vendor's own request builder and parser.

createAI replaces workers-ai-provider and ai-gateway-provider. For AI Search, use ai-search-provider.

Install

The provider needs ai@^7 and @ai-sdk/provider@^4. Both are optional peer dependencies of agents:

npm i agents ai @ai-sdk/provider

Add a vendor's AI SDK provider only if you pass that vendor's model objects, for example @ai-sdk/anthropic or @ai-sdk/openai. Catalog ids do not need one.

Add the AI binding to your Wrangler configuration:

{
	"ai": {
		"binding": "AI",
	},
}
[ai]
binding = "AI"

Use an account id and API token

To use a gateway in another account, or to call from outside a Worker, pass accountId and apiToken instead of binding:

const ai = createAI({
	accountId: "<ACCOUNT_ID>",
	apiToken: env.CLOUDFLARE_API_TOKEN,
	gateway: "my-gateway",
});

The call forms and options are the same, and the gateway still defaults to default. A string goes to AI Gateway's REST API, which runs it as env.AI.run() does. A model object goes to the provider-native endpoint at gateway.ai.cloudflare.com, with the token in the cf-aig-authorization header. The token is sent to those two hosts only.

Create an API token with the Workers AI Read permission for strings and AI Gateway Run for model objects.

Call a Workers AI model

Pass a @cf/ id. Model ids autocomplete from @cloudflare/workers-types:

import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request, env) {
		const ai = createAI({ binding: env.AI });

		const { text } = await generateText({
			model: ai("@cf/zai-org/glm-4.7-flash"),
			prompt: "Say hello in three words",
		});

		return Response.json({ text });
	},
};
import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request: Request, env: Env) {
		const ai = createAI({ binding: env.AI });

		const { text } = await generateText({
			model: ai("@cf/zai-org/glm-4.7-flash"),
			prompt: "Say hello in three words",
		});

		return Response.json({ text });
	},
};

A string that is not a Workers AI id, a catalog id, or a dynamic route throws a TypeError that names the three forms.

Call an AI Gateway catalog model

Pass an <author>/<model> id from the model catalog. No vendor package or key is needed:

import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request, env) {
		const ai = createAI({ binding: env.AI, id: "my-gateway" });

		const { text } = await generateText({
			model: ai("anthropic/claude-opus-5.5"),
			prompt: "Say hello in three words",
		});

		return Response.json({ text });
	},
};
import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request: Request, env: Env) {
		const ai = createAI({ binding: env.AI, id: "my-gateway" });

		const { text } = await generateText({
			model: ai("anthropic/claude-opus-5.5"),
			prompt: "Say hello in three words",
		});

		return Response.json({ text });
	},
};

Use the catalog's spelling of the id, not the vendor's. AI Gateway looks the model up by its exact catalog name, so anthropic/claude-opus-5.5 works and anthropic/claude-opus-5-5 does not. Other examples are openai/gpt-5.5 and cloudflare/auto.

The request goes through env.AI.run() to AI Gateway. The provider sends it in the OpenAI chat completions format, and AI Gateway converts it to and from the format of the provider that serves the model. The gateway also handles:

  • Credentials. Unified Billing bills the model to your Cloudflare account. A key you store on the gateway is used instead when one is configured.
  • Providers. A catalog model can have several providers. AI Gateway picks one and retries across them.
  • Errors. An unknown model, an unpaid account, or an upstream failure throws a CloudflareAIError, the same type as a Workers AI failure.

Unified Billing requires authentication on the gateway. Without it, the request fails with error 2049.

reasoningEffort is sent as reasoning_effort. chatTemplateKwargs is a Workers AI option, so the provider drops it with a warning.

Call a dynamic route

An AI Gateway dynamic route is named dynamic/<route>. The route you configured on the gateway picks the model, and can apply conditions, percentages, rate and budget limits, and its own fallbacks:

const ai = createAI({ binding: env.AI, id: "my-gateway" });

await generateText({ model: ai("dynamic/support"), prompt: "Hi" });

A route belongs to one gateway, so set the id of the gateway you created it on. The route answers in the chat completions format whatever model it picks. Like a catalog model, it drops chatTemplateKwargs with a warning. A dynamic route is a language model only. The embedding, image, speech, transcription, and reranking methods reject it.

Call a vendor's model object

Build the model with the vendor's own AI SDK provider, then pass it to ai(). AI Gateway holds the credential, so the vendor's apiKey is a placeholder:

import { createAnthropic } from "@ai-sdk/anthropic";
import { createOpenAI } from "@ai-sdk/openai";
import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request, env) {
		const ai = createAI({ binding: env.AI });
		const anthropic = createAnthropic({ apiKey: "cloudflare" });
		const openai = createOpenAI({ apiKey: "cloudflare" });

		const claude = await generateText({
			model: ai(anthropic("claude-opus-5-5")),
			prompt: "Say hello in three words",
		});

		const gpt = await generateText({
			model: ai(openai.responses("gpt-5-mini")),
			prompt: "Say hello in three words",
		});

		return Response.json({ claude: claude.text, gpt: gpt.text });
	},
};
import { createAnthropic } from "@ai-sdk/anthropic";
import { createOpenAI } from "@ai-sdk/openai";
import { createAI } from "agents/models/ai-sdk";
import { generateText } from "ai";

export default {
	async fetch(request: Request, env: Env) {
		const ai = createAI({ binding: env.AI });
		const anthropic = createAnthropic({ apiKey: "cloudflare" });
		const openai = createOpenAI({ apiKey: "cloudflare" });

		const claude = await generateText({
			model: ai(anthropic("claude-opus-5-5")),
			prompt: "Say hello in three words",
		});

		const gpt = await generateText({
			model: ai(openai.responses("gpt-5-mini")),
			prompt: "Say hello in three words",
		});

		return Response.json({ claude: claude.text, gpt: gpt.text });
	},
};

The vendor's package builds the request and parses the response. The provider replaces the model's fetch, and sends the same request, with the same method, headers, and body, through the binding to AI Gateway's provider-native endpoint for that vendor:

https://workers-binding.ai/ai-gateway/gateways/{gateway}/{provider}/{endpoint}

The provider takes the gateway provider from the request host. For example, api.anthropic.com is anthropic and api.openai.com is openai. A host that AI Gateway has no provider for throws a TypeError. Gateway options travel as cf-aig-* headers. Multipart bodies, such as file uploads for transcription, work.

Use the vendor's own model id, such as claude-opus-5-5. The vendor's package reads model capabilities from it.

Providers on Unified Billing answer with no key of yours. For any other provider, store a key on the gateway. Until you do, the vendor's own error comes back unchanged.

ai() clones the model for each call and never changes the object you passed. If you add middleware, wrap the model with ai() first:

wrapLanguageModel({ model: ai(anthropic("claude-opus-5-5")), middleware });

A model that is already wrapped in middleware hides the settings the provider needs, so ai() rejects it.

Unified billing and BYOK

By default, the provider removes the vendor's credential header (authorization, x-api-key, x-goog-api-key, or api-key) so AI Gateway supplies the key. To use a key you hold, give it to the vendor's provider and set vendorKey: "request" on the model:

const anthropic = createAnthropic({ apiKey: env.ANTHROPIC_API_KEY });
const model = ai(anthropic("claude-opus-5-5"), { vendorKey: "request" });

vendorKey belongs to the model you set it on. A fallback model does not inherit it, so one chain can mix a model on your key with a model on Unified Billing.

byokAlias picks a stored key by alias when the gateway holds several keys for one provider. The provider sends it as cf-aig-byok-alias for catalog models and model objects.

zdr turns Zero Data Retention on (true) or off (false) for one Unified Billing request, whatever the gateway's own setting. The provider sends it as cf-aig-zdr. It applies to model objects only, because catalog models and dynamic routes do not read it.

const model = ai(anthropic("claude-opus-5-5"), {
	byokAlias: "production",
	zdr: true,
});

Stream, call tools, and return structured output

Every kind of model works with every AI SDK call:

import { createAI } from "agents/models/ai-sdk";
import { generateText, isStepCount, Output, streamText, tool } from "ai";
import { z } from "zod";

const getWeather = tool({
	description: "Get the current weather for a city.",
	inputSchema: z.object({ city: z.string() }),
	execute: ({ city }) => ({ city, conditions: "clear", temperatureC: 21 }),
});

export default {
	async fetch(request, env) {
		const ai = createAI({ binding: env.AI });
		const model = ai("@cf/zai-org/glm-4.7-flash");
		const { pathname } = new URL(request.url);

		if (pathname === "/stream") {
			const result = streamText({
				model,
				prompt: "Write a haiku about Durable Objects",
			});
			return result.toTextStreamResponse();
		}

		if (pathname === "/tools") {
			const { text } = await generateText({
				model,
				prompt: "What is the weather in Lisbon?",
				tools: { getWeather },
				stopWhen: isStepCount(3),
			});
			return Response.json({ text });
		}

		const { output } = await generateText({
			model,
			prompt: "One surprising fact about octopuses.",
			output: Output.object({ schema: z.object({ fact: z.string() }) }),
		});
		return Response.json(output);
	},
};
import { createAI } from "agents/models/ai-sdk";
import { generateText, isStepCount, Output, streamText, tool } from "ai";
import { z } from "zod";

const getWeather = tool({
	description: "Get the current weather for a city.",
	inputSchema: z.object({ city: z.string() }),
	execute: ({ city }) => ({ city, conditions: "clear", temperatureC: 21 }),
});

export default {
	async fetch(request: Request, env: Env) {
		const ai = createAI({ binding: env.AI });
		const model = ai("@cf/zai-org/glm-4.7-flash");
		const { pathname } = new URL(request.url);

		if (pathname === "/stream") {
			const result = streamText({
				model,
				prompt: "Write a haiku about Durable Objects",
			});
			return result.toTextStreamResponse();
		}

		if (pathname === "/tools") {
			const { text } = await generateText({
				model,
				prompt: "What is the weather in Lisbon?",
				tools: { getWeather },
				stopWhen: isStepCount(3),
			});
			return Response.json({ text });
		}

		const { output } = await generateText({
			model,
			prompt: "One surprising fact about octopuses.",
			output: Output.object({ schema: z.object({ fact: z.string() }) }),
		});
		return Response.json(output);
	},
};

Resume a dropped stream

Through the binding, a stream from a catalog model or a dynamic route resumes after a dropped connection. You do not need to configure anything:

const result = streamText({
	model: ai("anthropic/claude-opus-5.5"),
	prompt: "Write a haiku about Durable Objects",
});
// If the connection drops, result.textStream keeps going.

AI Gateway names each streamed run in the cf-aig-run-id header and keeps its events. If the connection drops mid-stream, the provider reconnects through the binding and replays the run from the last complete event. Your stream continues with nothing lost or repeated. The run keeps going on the gateway while the connection is down, so the resumed stream continues the same answer.

The provider gives up and throws a CloudflareAIError after five drops in one stream, or once the gateway no longer holds the run. The gateway keeps a run for a few minutes after it ends. Aborting the call never resumes it.

Workers AI @cf/ streams and model objects have no run id, so they do not resume. The run id is in providerMetadata.cloudflare.runId.

Use it in Think

Return a model from getModel():

import { Think } from "@cloudflare/think";
import { createAI } from "agents/models/ai-sdk";

export class Assistant extends Think {
	getModel() {
		return createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.7-code");
	}
}
import { Think } from "@cloudflare/think";
import { createAI } from "agents/models/ai-sdk";

export class Assistant extends Think<Env> {
	getModel() {
		return createAI({ binding: this.env.AI })("@cf/moonshotai/kimi-k2.7-code");
	}
}

Cache responses

AI Gateway can cache responses. Set cacheTtl in seconds, and optionally a cacheKey. Pass skipCache: true on a call to bypass the cache:

const ai = createAI({ binding: env.AI, id: "prod", cacheTtl: 60 });

// A longer TTL for one model.
const faq = ai("@cf/zai-org/glm-4.7-flash", {
	cacheTtl: 3600,
	cacheKey: "faq-v1",
});

// Skip the cache for one call.
await generateText({
	model: faq,
	prompt: "What changed today?",
	providerOptions: { cloudflare: { skipCache: true } },
});

Tag requests for logs and analytics

Attach metadata to the gateway log entry. Set it once on the provider and add to it per call. Metadata merges key by key:

const ai = createAI({ binding: env.AI, metadata: { app: "support-bot" } });

await generateText({
	model: ai(anthropic("claude-opus-5-5")),
	prompt: "Summarize this ticket",
	providerOptions: {
		cloudflare: { metadata: { userId: "u_123" }, eventId: "ticket-42" },
	},
});

eventId groups several requests into one gateway event, such as all the model calls in one agent turn.

Fall back to another model

fallback lists models to try in order if the primary model fails before it produces any output. A fallback model can be a Workers AI id, a catalog id, a dynamic route, or a model object:

const model = ai(anthropic("claude-opus-5-5"), {
	fallback: ["openai/gpt-5.5", "dynamic/support", "@cf/zai-org/glm-4.7-flash"],
});

A model object in the list goes to the provider-native endpoint, like ai(model).

A request that the vendor rejects falls back. So does a stream that fails before its first output part. Once a model has produced output, its answer is final, errors included. providerMetadata.cloudflare.model names the model that answered.

Each fallback model uses the chain's gateway options. The chain's id, cacheTtl, and the other gateway options win over a fallback model's own, and metadata merges. The headers, Workers AI options, and vendorKey a fallback model was built with stay its own.

Fallback runs in your Worker and moves across models. Within one catalog model, AI Gateway already retries across that model's providers, and a dynamic route runs its own fallback on the gateway.

Set options

Set gateway options on the provider, on a model, or per call under providerOptions.cloudflare. A per-call option wins over a per-model option, which wins over the provider's:

// Provider
const ai = createAI({ binding: env.AI, id: "prod", metadata: { app: "demo" } });

// Model
const model = ai("anthropic/claude-opus-5.5", { cacheTtl: 3600 });

// Call
await generateText({
	model,
	prompt: "hi",
	providerOptions: {
		cloudflare: { skipCache: true, metadata: { userId: "u1" } },
	},
});

The provider removes providerOptions.cloudflare before the call reaches a vendor's model, so the vendor never sees it.

Gateway options can be given flat or nested: { cacheTtl: 60 } and { gateway: { cacheTtl: 60 } } are the same.

Option Type Description
binding Ai The AI binding. Provider only.
accountId string The account for an account id and API token. Provider only.
apiToken string A Cloudflare API token for that account. Provider only.
id string Gateway id. Defaults to "default", which is created the first time you use it.
gateway string | GatewayOptions The gateway id, or the gateway options nested.
skipCache boolean Bypass the gateway cache.
cacheTtl number Cache lifetime in seconds.
cacheKey string Cache key to use instead of the one derived from the request.
collectLog boolean Whether the gateway stores the request and response in its log.
metadata Record<string, string | number | boolean | null> Values attached to the gateway log entry.
eventId string Groups several requests into one gateway event.
requestTimeoutMs number Timeout for the upstream request.
retries { maxAttempts, retryDelayMs, backoff } Gateway retry policy for the upstream call. maxAttempts is 1 to 5.
byokAlias string A stored BYOK key, by alias. Catalog models and model objects.
zdr boolean Zero Data Retention on or off for a Unified Billing request. Model objects only.
vendorKey "gateway" | "request" "request" forwards the vendor key on the request. Model objects only, per model.
fallback (CloudflareLanguageModelId | LanguageModelV4)[] Models to try in order. Model and call only.
headers Record<string, string> Extra request headers, merged last. Model and call only.
sessionAffinity string Workers AI only. Sends requests with the same key to one replica for prefix caching.
reasoningEffort "low" | "medium" | "high" | null Sent as reasoning_effort. null turns reasoning off on models that allow it.
chatTemplateKwargs Record<string, unknown> Workers AI only. Sent as chat_template_kwargs, for example { enable_thinking: false }.

reasoningEffort applies to Workers AI models, catalog models, and dynamic routes. When it is not set, the AI SDK's reasoning call option maps onto it. For a model object, ask for reasoning with the AI SDK's reasoning call option or the vendor's own providerOptions. The provider ignores reasoningEffort and chatTemplateKwargs on a model object and adds a warning.

Read gateway metadata

Every result, and every stream's finish part, carries providerMetadata.cloudflare. For a vendor's model it sits next to the vendor's own metadata:

const { providerMetadata } = await generateText({
	model: ai(anthropic("claude-opus-5-5")),
	prompt: "hi",
});

providerMetadata?.cloudflare;
// {
//   model: "claude-opus-5-5",
//   provider: "anthropic",
//   gateway: "default",
//   logId: "01M1KZWN069WWNPC18V05NKHSS",
//   cacheStatus: "MISS",
//   ...
// }
Field Description
model The model that answered, after any fallback.
gateway The gateway id, when a gateway handled the request.
provider The gateway provider, for a model object.
logId The gateway log entry, from cf-aig-log-id.
eventId The gateway event, from cf-aig-event-id.
requestId The gateway request id, from cf-aig-request-id.
traceId The trace this request belongs to, from cf-aig-trace-id.
runId The streamed run, from cf-aig-run-id, for a catalog model or a dynamic route.
cacheStatus HIT or MISS, from cf-aig-cache-status.
step The step of a gateway route that answered, from cf-aig-step.

Only model is always present. Check for the other fields before you use them.

Handle errors

The provider throws CloudflareAIError, which extends the AI SDK's APICallError, for Workers AI failures, AI Gateway failures, failed stream resumes, and a fallback chain where every model failed. The AI SDK's retry loop reads its isRetryable flag:

import { CloudflareAIError } from "agents/models/ai-sdk";

try {
	await generateText({ model: ai("@cf/zai-org/glm-4.7-flash"), prompt: "hi" });
} catch (error) {
	if (error instanceof CloudflareAIError) {
		console.log(error.status, error.code, error.logId, error.model);
		// error.attempts lists every fallback model that failed.
	}
}

code is one of "auth", "rate-limit", "not-found", "bad-request", "provider-error", "gateway-error", or "unknown".

For a model object, CloudflareAIError covers the gateway's own failures, such as a missing gateway or an unpaid account. The vendor's own errors, such as an unknown model id or a rate limit, come from the vendor's provider in its own error type.

Use other modalities

Embeddings, images, speech, transcription, and reranking use Workers AI models from the same provider. Each method also has its ProviderV4 name, such as ai.embeddingModel():

import {
	embedMany,
	generateImage,
	generateSpeech,
	rerank,
	transcribe,
} from "ai";

const { embeddings } = await embedMany({
	model: ai.embedding("@cf/baai/bge-base-en-v1.5"),
	values: ["hello", "world"],
});

const { image } = await generateImage({
	model: ai.image("@cf/black-forest-labs/flux-1-schnell"),
	prompt: "A cyberpunk lizard",
});

const { audio } = await generateSpeech({
	model: ai.speech("@cf/deepgram/aura-1"),
	text: "Hello from Cloudflare.",
});

const { text } = await transcribe({
	model: ai.transcription("@cf/openai/whisper-large-v3-turbo"),
	audio: audio.uint8Array,
});

const { rerankedDocuments } = await rerank({
	model: ai.reranking("@cf/baai/bge-reranker-base"),
	documents: ["a cyberpunk lizard", "a rainy Tuesday"],
	query: "Which one is cooler?",
});

Model-specific settings go under providerOptions.cloudflare:

Modality Call options used providerOptions.cloudflare
Image prompt, n, size, seed, files, mask steps, guidance, negativePrompt, strength
Speech text, voice (as speaker), outputFormat (as encoding), language (MeloTTS only) None
Transcription audio, mediaType language, task, initialPrompt, prefix, vadFilter
Reranking query, documents, topN (as top_k) None

An option a model does not take is listed in the result's warnings instead of being sent. For example, flux-1-schnell takes only a prompt and steps, and @cf/deepgram/nova-3 takes language but not the Whisper-only transcription options. Image generation makes one image per request, so generateImage with a larger n sends parallel requests.

These methods take Workers AI @cf/ ids only, including in fallback. To route a vendor's own embedding, image, speech, or transcription model through AI Gateway, pass it to ai.routed(model). It goes to the provider-native endpoint, like ai(model). ai.routed() takes the gateway options, headers, sessionAffinity, and vendorKey.

Use a provider registry

createAI returns a ProviderV4, so it works with createProviderRegistry and as the AI SDK's global default provider:

import { createProviderRegistry, generateText } from "ai";

const registry = createProviderRegistry({ cloudflare: ai });
await generateText({
	model: registry.languageModel("cloudflare:anthropic/claude-opus-5.5"),
	prompt: "Hi",
});

// Bare strings now resolve through this provider.
globalThis.AI_SDK_DEFAULT_PROVIDER = createAI({ binding: env.AI });
await generateText({ model: "@cf/zai-org/glm-4.7-flash", prompt: "Hi" });

A string can be a Workers AI id, a catalog id, or a dynamic route.

Limitations

  • @cf/deepgram/flux takes websocket connections only, so it throws a CloudflareAIError before any request is sent. Use @cf/deepgram/nova-3 or @cf/openai/whisper-large-v3-turbo for transcription.
  • chatTemplateKwargs and sessionAffinity apply to Workers AI models only.
  • zdr applies to model objects only.
  • Workers AI streams and model object streams do not resume after a dropped connection.

Was this helpful?