AI SDK
Last updated Oct 26, 2026
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.
Beta
This provider is in beta. Its API may change in a minor release.
Install
The provider needs ai@^7 and @ai-sdk/provider@^4. Both are optional peer dependencies of agents:
npmyarnpnpmbun
npm i agents ai @ai-sdk/provider
yarn add agents ai @ai-sdk/provider
pnpm add agents ai @ai-sdk/provider
bun add 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.
