AI
env.AI is the AI binding wired into every app. There are three ways to use it, all through env.AI: call an included model by purpose (env.AI.chat), run any model Cloudflare serves by its id with no key (env.AI.run, including OpenAI, Anthropic, and Google), or bring your own vendor key. It also reaches the live web, env.AI.search and env.AI.browse. This page spells out exactly what to pass and how to read every response, plus billing and errors, so there's no guessing.
The three ways to use AI
Everything below is one of these three. Pick by what you need: simplest default, a specific model with no key, or your own vendor account.
| Way | Call | API key? | Billed to | You get back |
|---|---|---|---|---|
| Purpose method | env.AI.chat({ messages }) | None | Your AI credits | res.response |
| Run by id | env.AI.run("<id>", input) | None | Your AI credits | the model's native shape |
| Bring your own key | env.AI.openai.chat({ … }) | Your vendor key | The vendor (not us) | the vendor's native shape |
- Purpose method,
env.AI.chat / research / code / vision / image / embed / …. The included Cloudflare-hosted models, picked for you per task. No key, metered to your AI credits, and we can upgrade the model behind a purpose without you changing code. - Run by id,
env.AI.run("<id>", input). Run a specific model Cloudflare serves, by its id, still no key and metered to your AI credits. That covers every Workers AI model ("@cf/…"), the included clean names, and OpenAI / Anthropic / Google models by full id (e.g."openai/gpt-6-luna"). - Bring your own key,
env.AI.openai / anthropic / google / jev. Set that vendor's key as an app env var and the call goes straight to the vendor on your account, Cloudrizz never sees the key and bills nothing.
env.AI.run("openai/gpt-6-luna", …) and Cloudrizz sends it to the right place. So every app can use any of this without a redeploy; env.AI is already attached.Quickstart
// api/ai.js — a minimal included-model call, no key
export default {
async fetch(request, env) {
const { question } = await request.json();
const res = await env.AI.chat({ messages: [{ role: "user", content: question }] });
return Response.json({ answer: res.response }); // res.response is the text
},
};Included models by purpose (env.AI.chat, …)
The simplest way to add AI: no key, nothing to attach, metered to your plan's AI credits (every plan includes a monthly grant; top up for more). Each method takes the input in the table below and returns a fixed shape, so you always know where the answer is.
// api/assistant.js
export default {
async fetch(request, env) {
const { question, ticket } = await request.json();
const reply = await env.AI.chat({ messages: [{ role: "user", content: question }] });
const analysis = await env.AI.research({ messages: [{ role: "user", content: "Analyse: " + question }] });
const vec = await env.AI.embed({ text: question });
const picture = await env.AI.image({ prompt: question });
// Typed decision — branchable output, no prose to parse:
const decision = await env.AI.decide({
state: ticket,
questions: {
urgent: { type: "noul", instructions: "Is this time-sensitive?" },
team: { type: "choice", instructions: "Which team?", criteria: { billing: "Payments", technical: "Bugs" } },
},
});
return Response.json({
reply: reply.response, // text
firstVector: vec.data[0], // embeddings
urgentProb: decision.answers.urgent.noul, // 0..1
team: decision.answers.team.choice, // "technical"
});
},
};Every purpose method, its input, and where to read the result:
| Method | Pass | Read the result at |
|---|---|---|
env.AI.chat / research / code | { messages } | res.response |
env.AI.decide / decideFast | { state, questions } | res.answers.<key> |
env.AI.transcribe | { audio } | res.text |
env.AI.vision | { image, prompt } | res.description |
env.AI.image | { prompt } | res.image (base64 PNG) |
env.AI.embed | { text } | res.data (array of vectors) |
env.AI.speak | { prompt } | res.audio (base64) |
env.AI.translate | { text, source_lang, target_lang } | res.translated_text |
env.AI.rerank | { query, contexts } | res.response ([{ id, score }]) |
env.AI.classify | { text } | [{ label, score }] |
env.AI.classifyImage | { image } | [{ label, score }] |
env.AI.search | query or { query, limit } | res.items ([{ url, title, description }]) |
env.AI.browse | url or { url } | res.text (+ res.title) |
code is an open-weight function-calling model for coding + agentic tool use. decideFast is the cheaper/faster typed-decision variant for high-volume routing. For embeddings + search, env.SEARCH embeds and indexes text for you (see Semantic search).Pin a specific included model instead of a purpose by passing its clean name to env.AI.run, e.g. env.AI.run("llama-3.3-70b", { messages }). The full catalogue:
| Model (clean name) | Task | Purpose method |
|---|---|---|
llama-3.3-70b | Chat / general | env.AI.chat |
deepseek-r1 | Reasoning / analysis | env.AI.research |
gpt-oss-120b | Coding + tools (agentic) | env.AI.code |
deepseek-v4-flash | Coding (fast, agentic) | run("deepseek-v4-flash") |
deepseek-v4-pro | Coding (long-horizon) | run("deepseek-v4-pro") |
glm-5.3-flash | Coding (low-cost agentic) | run("glm-5.3-flash") |
clef | Typed decisions | env.AI.decide |
clef-flash | Typed decisions (faster) | env.AI.decideFast |
whisper | Speech → text | env.AI.transcribe |
llama-3.2-vision | Image → text | env.AI.vision |
flux | Text → image | env.AI.image |
bge-embed | Text embeddings | env.AI.embed |
melotts | Text → speech | env.AI.speak |
m2m100 | Translation | env.AI.translate |
bge-reranker | Rerank (RAG) | env.AI.rerank |
distilbert-sentiment | Sentiment | env.AI.classify |
resnet-50 | Image → class labels | env.AI.classifyImage |
p-image-edit | Image → image | env.AI.editImage |
wan-i2v | Image → video | env.AI.animateImage |
veo-fast | Text → video | env.AI.video |
bria-video-rmbg | Video → video | env.AI.editVideo |
music | Music | env.AI.music |
Run any model by id (no key): gpt-6-luna, Claude, Gemini
env.AI.run("<id>", input) runs a specific model Cloudflare serves, with no API key, metered to your AI credits, exactly like the included models. That includes OpenAI, Anthropic, and Google models by their full provider id (openai/…, anthropic/…, google/…). Cloudrizz routes each to the right endpoint server-side, you just pass the provider's native request shape.
| Model kind | Call | Read the text at |
|---|---|---|
| OpenAI chat (gpt-4o, gpt-5, gpt-4.1…) | run("openai/gpt-5", { messages }) | res.choices[0].message.content |
| OpenAI Responses (gpt-6-luna) | run("openai/gpt-6-luna", { input, max_output_tokens }) | res.output[…].content[…].text |
| Anthropic Claude | run("anthropic/claude-sonnet-4.5", { messages, max_tokens }) | res.content[0].text |
| Google Gemini | run("google/gemini-2.5-pro", { messages }) | res.choices[0].message.content |
Using gpt-6-luna (an OpenAI Responses model) in a user app. Pass input (a string), an optional max_output_tokens, and an optional reasoning.effort (low / medium / high). The reply is OpenAI's Responses shape, the text lives under output:
// api/luna.js — gpt-6-luna, no key, billed to your AI credits
export default {
async fetch(request, env) {
const { question } = await request.json();
const res = await env.AI.run("openai/gpt-6-luna", {
input: question,
max_output_tokens: 512,
reasoning: { effort: "low" }, // optional: low | medium | high
});
// Responses shape — pull the assistant text out of output[]:
const text = res.output
?.find((o) => o.type === "message")
?.content?.find((c) => c.type === "output_text")?.text ?? "";
return Response.json({ answer: text, usage: res.usage });
},
};Chat-shaped models (most OpenAI + all Google) take messages and return the familiar chat-completion shape; Anthropic needs max_tokens and returns its own shape:
// OpenAI / Google chat — read choices[0].message.content
const gpt = await env.AI.run("openai/gpt-5", {
messages: [{ role: "user", content: question }],
});
const gptText = gpt.choices[0].message.content;
const gem = await env.AI.run("google/gemini-2.5-pro", {
messages: [{ role: "user", content: question }],
});
const gemText = gem.choices[0].message.content;
// Anthropic — max_tokens is required; read content[0].text
const claude = await env.AI.run("anthropic/claude-sonnet-4.5", {
messages: [{ role: "user", content: question }],
max_tokens: 1024,
});
const claudeText = claude.content[0].text;Verified available (call any by full id): OpenAI gpt-4.1 / gpt-4.1-mini / gpt-4.1-nano, gpt-4o / gpt-4o-mini, gpt-5 / gpt-5-mini / gpt-5-nano, o3, o4-mini, gpt-6-luna; Anthropic claude-haiku-4.5 / claude-sonnet-4.5 / claude-opus-4.5 / claude-sonnet-5 / claude-opus-5; Google gemini-2.5-flash / gemini-2.5-pro / gemini-2.5-flash-lite / gemini-3-flash. Other models Cloudflare serves work the same way, prefix with the provider name.
env.AI.openai / anthropic / google, below). The result is always the provider's native shape (not the { response } of env.AI.chat), use the table above to read it.Generative media (image, video, music)
env.AI also generates images, video, and music, one recommended model per task, no key. These are metered by output unit (per image, or per second of video/audio), so a longer clip costs more. They return JSON with a signed URL to the file (res.result.video / .audio / .image), not raw bytes; the models that edit an existing asset take a URL (host it in env.STORAGE and pass its link).
| Method | Pass | Read the result at |
|---|---|---|
env.AI.image | { prompt } | res.image (base64 PNG) |
env.AI.editImage | { prompt, images: [url] } | res.result.image (URL) |
env.AI.animateImage | { prompt, image: url } | res.result.video (URL) |
env.AI.video | { prompt } | res.result.video (URL) |
env.AI.editVideo | { video: url } | res.result.video (URL) |
env.AI.music | { prompt, music_length_ms } | res.result.audio (URL) |
// api/clip.js — a short video from a prompt (billed per second)
export default {
async fetch(request, env) {
const { prompt } = await request.json();
const res = await env.AI.video({ prompt });
// res.result.video is a signed URL (valid ~24h) — download + re-store it
// in env.STORAGE if you need it to persist.
return Response.json({ url: res.result.video });
},
};
// Music: duration is in MILLISECONDS
const song = await env.AI.music({ prompt: "calm lofi loop", music_length_ms: 15000 });
// song.result.audio → signed URL
// Text → image returns base64 (not a URL):
const pic = await env.AI.image({ prompt: "a red bicycle" });
return new Response(Uint8Array.from(atob(pic.image), (c) => c.charCodeAt(0)), {
headers: { "content-type": "image/png" },
});editVideo needs its provider enabled on the platform gateway, ask us if a media call returns "not available".Live web (search & browse)
Two methods give a model access to the live internet, so your app isn't stuck at a model's training cutoff. Both are included (no key, metered to your AI credits) and run on Cloudflare: env.AI.search returns ranked web results, env.AI.browse reads a page's text.
| Method | Pass | Read the result at | Cost |
|---|---|---|---|
env.AI.search | query or { query, limit } (1-10) | res.items ([{ url, title, description }]) | 1 cr / call |
env.AI.browse | url or { url } | res.text, res.title, res.finalUrl, res.status | 1 cr / call |
The classic pattern is search, then browse the best hits, then answer (a live-web RAG loop), all with no key:
// api/ask-web.js — answer a question from fresh web results
export default {
async fetch(request, env) {
const { question } = await request.json();
// 1) Search the live web (ranked results).
const { items } = await env.AI.search({ query: question, limit: 5 });
// 2) Read the top couple of pages for their actual text.
const pages = await Promise.all(
items.slice(0, 2).map((r) => env.AI.browse(r.url).catch(() => null)),
);
const context = pages
.filter(Boolean)
.map((p) => p.title + "\n" + p.text)
.join("\n\n---\n\n");
// 3) Answer grounded in what you just fetched, with citations.
const res = await env.AI.chat({
messages: [
{ role: "user", content:
"Using these web sources, answer: " + question +
"\n\nSources:\n" + context +
"\n\nCite the source URLs you used." },
],
});
return Response.json({ answer: res.response, sources: items.map((i) => i.url) });
},
};browse renders the page in a real browser, so JavaScript-heavy pages still yield text; it returns up to ~12k characters (longer pages are truncated). Private, loopback, and internal addresses are refused. For a known URL you can always browse it directly without a search first. search takes a limit of 1-10 (default 5).Streaming (for chat UIs)
For a live “typing” chat, pass stream: true (purpose methods) or use .stream(...) (BYO). You get a Response whose body is a Server-Sent-Events stream, pipe it straight to the browser and render tokens as they arrive.
// api/chat.js — stream an included-model reply to the browser
export default {
async fetch(request, env) {
const { messages } = await request.json();
const res = await env.AI.chat({ messages, stream: true });
return new Response(res.body, {
headers: { "content-type": "text/event-stream; charset=utf-8" },
});
},
};On the client, read it with EventSource (or fetch + a stream reader) and append each delta. With your own key, env.AI.openai.stream(...) / env.AI.anthropic.stream(...) / env.AI.google.stream(...) stream direct from the vendor.
Answer from your own data (RAG)
To make a model answer from your app's content, pair it with env.SEARCH. Retrieve the most relevant passages by meaning, put them in the prompt, then stream the answer, no key needed:
const { matches } = await env.SEARCH.query(question, { topK: 6 });
const context = matches.map((m) => m.text).join("\n---\n");
const res = await env.AI.chat({
stream: true,
messages: [{ role: "user", content: `Context:\n${context}\n\nQuestion: ${question}` }],
});
return new Response(res.body, { headers: { "content-type": "text/event-stream" } });Typed decisions (env.AI.decide, Jev)
When you want a decision your code can branch on (not prose), env.AI.decide (included, no key) and env.AI.jev (bring-your-own TypeSafe key) both take state plus typed questions and return typed answers with calibrated probabilities, in one call. No prompt-wrangling, no JSON parsing. Three question types:
noul, a yes/no judgment, returned as a probability0–1.choice, pick one of your labelled options; returns thechoiceplus a probability per option and aconfidence.score, rate on a level scale; returns a probability-weightedscoreplus the distribution.
// api/triage.js — included, no key:
const d = await env.AI.decide({
state: message,
questions: {
is_urgent: { type: "noul", instructions: "Does this convey urgency?" },
department: { type: "choice", instructions: "Which team?", criteria: { billing: "Payments", technical: "Bugs" } },
frustration:{ type: "score", instructions: "How frustrated?", criteria: ["Calm", "Frustrated", "Very angry"] },
},
});
// d.answers.is_urgent.noul → 0.95
// d.answers.department.choice → "billing" (+ .probabilities, .confidence)
// d.answers.frustration.score → 1.05 (+ .probabilities, .confidence)
// Same interface, bring-your-own TypeSafe key (set TYPESAFE_API_KEY):
const j = await env.AI.jev({ state: message, questions: { is_urgent: { type: "noul", instructions: "Urgent?" } } });env.AI.decide runs on Cloudflare's native decision model (included, no key). env.AI.jev / env.AI.typesafe.systemOne is the bring-your-own TypeSafe alternative. Decisions are a single call, so there's no streaming.Bring your own provider (OpenAI, Anthropic, Gemini)
Want to call a vendor on your own account (your billing, your key)? Set the provider's key as an app env var (via manage_app_env, action: set) and that provider's calls go straight to the vendor, Cloudrizz never sees the key and bills nothing. Without the key these throw byo_key_required. Note this is a separate path from env.AI.run("openai/…") above (which is key-free on your AI credits), use BYO when you specifically want it billed by the vendor, or for a model Cloudflare doesn't serve.
env.AI.openai→OPENAI_API_KEY. OpenAI Chat Completions shape.env.AI.anthropic→ANTHROPIC_API_KEY. Anthropic Messages shape.env.AI.google(aliasenv.AI.gemini) →GOOGLE_API_KEY. GeminigenerateContentshape (model+contents, notmessages).env.AI.jev/env.AI.typesafe→TYPESAFE_API_KEY. Typed decisions.
// api/ask.js — requires OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY
const openai = await env.AI.openai.chat({ model: "gpt-5", messages: [{ role: "user", content: q }] });
const claude = await env.AI.anthropic.chat({ model: "claude-sonnet-4-6", max_tokens: 1024, messages: [{ role: "user", content: q }] });
const gemini = await env.AI.google.chat({ model: "gemini-2.5-flash", contents: [{ role: "user", parts: [{ text: q }] }] });Billing & credits
Every included and run-by-id call (purpose methods, env.AI.run on @cf/… ids, and the key-free provider models) is metered to your org's AI credits (1 credit = 1¢). Each plan includes a monthly grant; buy top-up packs for more. Text models bill by token, media by output unit. Bring-your-own-key calls bill you nothing on Cloudrizz, the vendor bills your account directly.
- When credits hit zero, included / run-by-id calls fail with
out_of_credits(HTTP 402). Buy a top-up pack, wait for the monthly refill, or switch that call to a bring-your-own-key model. - Check the balance and usage in your dashboard under AI credits.
- A model without an explicit price bills a small flat per-call amount (never free), models you're likely to use are priced by token.
See Limits & quotas for the per-plan grants.
What each model costs
Exact rates, kept in sync with what we bill. 1 credit = 1¢ (so 100 credits = $1). Text models bill per token, split into input (the prompt + context you send) and output (what the model writes back), quoted here per 1M tokens. As a feel for it, a short chat turn is typically a few hundred to a few thousand tokens, so a llama-3.3-70b reply costs a tiny fraction of a credit. There's a per-model floor (most calls round up to at least 1 credit).
Included models (by purpose / clean name). What env.AI.chat, research, code, decide and friends run, and what env.AI.run("<clean-name>") pins:
| Model | Input | Output |
|---|---|---|
llama-3.3-70bCloudrizz chat (Llama 70B) | 60 cr / 1M | 500 cr / 1M |
deepseek-v4-flashCloudrizz code (DeepSeek V4 Flash) | 90 cr / 1M | 270 cr / 1M |
deepseek-v4-proCloudrizz code (DeepSeek V4 Pro) | 270 cr / 1M | 800 cr / 1M |
deepseek-r1Cloudrizz research (DeepSeek-R1 32B) | 100 cr / 1M | 1000 cr / 1M |
gpt-oss-120bCloudrizz code (gpt-oss 120B) | 70 cr / 1M | 150 cr / 1M |
glm-5.3-flashCloudrizz code (GLM-5.3 Flash) | 30 cr / 1M | 100 cr / 1M |
clefCloudrizz decide (Clef) | 50 cr / 1M | 0 cr / 1M |
clef-flashCloudrizz decide (Clef flash) | 20 cr / 1M | 0 cr / 1M |
llama-3.2-visionCloudrizz vision (Llama 3.2 Vision) | 10 cr / 1M | 140 cr / 1M |
bge-embedCloudrizz embeddings (BGE large) | 41 cr / 1M | 0 cr / 1M |
m2m100Cloudrizz translate (M2M100) | 70 cr / 1M | 70 cr / 1M |
bge-rerankerCloudrizz rerank (BGE reranker) | 1 cr / 1M | 0 cr / 1M |
distilbert-sentimentCloudrizz classify (DistilBERT sentiment) | 6 cr / 1M | 0 cr / 1M |
Provider models via Cloudflare (key-free). Called by full id with env.AI.run("openai/…") etc., billed to your credits by token just like the included models:
| Model id | Input | Output |
|---|---|---|
openai/gpt-4.1-mini | 80 cr / 1M | 320 cr / 1M |
openai/gpt-6-luna | 20 cr / 1M | 100 cr / 1M |
openai/gpt-4.1 | 400 cr / 1M | 1600 cr / 1M |
openai/gpt-4.1-nano | 20 cr / 1M | 80 cr / 1M |
openai/gpt-4o | 250 cr / 1M | 1000 cr / 1M |
openai/gpt-4o-mini | 15 cr / 1M | 60 cr / 1M |
openai/gpt-5 | 250 cr / 1M | 2000 cr / 1M |
openai/gpt-5-mini | 50 cr / 1M | 400 cr / 1M |
openai/gpt-5-nano | 10 cr / 1M | 80 cr / 1M |
openai/o4-mini | 220 cr / 1M | 880 cr / 1M |
anthropic/claude-haiku-4.5 | 200 cr / 1M | 1000 cr / 1M |
anthropic/claude-sonnet-4.5 | 600 cr / 1M | 3000 cr / 1M |
anthropic/claude-opus-4.5 | 1000 cr / 1M | 5000 cr / 1M |
google/gemini-2.5-flash | 60 cr / 1M | 500 cr / 1M |
google/gemini-2.5-pro | 250 cr / 1M | 2000 cr / 1M |
google/gemini-2.5-flash-lite | 20 cr / 1M | 80 cr / 1M |
google/veo-3.1-fast | 0 cr / 1M | 0 cr / 1M |
Generative media. Billed per output unit (an image, or a second of video / audio), so a longer clip costs more. The “typical call” column is the default length:
| Model | Per unit | Typical call |
|---|---|---|
p-image-editCloudrizz edit image (Pruna P-Image-Edit) | 2 cr / image | 2 cr for 1 images |
wan-i2vCloudrizz animate image (Wan 2.7 I2V) | 20 cr / second | 100 cr for 5 seconds |
veo-fastCloudrizz video (Veo 3.1 Fast) | 16 cr / second | 80 cr for 5 seconds |
bria-video-rmbgCloudrizz edit video (Bria RMBG 3.0) | 5 cr / second | 25 cr for 5 seconds |
musicCloudrizz music (ElevenLabs Music v2) | 0.5 cr / second | 15 cr for 30 seconds |
Flat per-call models. A few models don't report token usage, so they bill a flat amount per call regardless of size:
| Model | Price |
|---|---|
whisperCloudrizz transcribe (Whisper v3 turbo) | 1 cr / call |
fluxCloudrizz image (FLUX.1 schnell) | 1 cr / call |
melottsCloudrizz speech (MeloTTS) | 1 cr / call |
resnet-50Cloudrizz image classify (ResNet-50) | 1 cr / call |
Web tools. env.AI.search and env.AI.browse (the Live web section above) each bill a flat 1 credit per call.
@cf/… id) bills the flat 5-credit per-call rate so it's never free. Bring-your-own-key calls cost nothing on Cloudrizz, the vendor bills your account at their rates.Errors
A failed call throws an Error with status and code fields. Wrap calls in try / catch and branch on e.code:
| code | HTTP | Means | Do |
|---|---|---|---|
byo_key_required | 400 | A bring-your-own-key method was called without its key (or a model Cloudflare won't serve key-free). | Set the vendor key, or use an included model / run-by-id. |
out_of_credits | 402 | The org's AI credits are exhausted. | Buy a top-up pack or wait for the monthly refill. |
rate_limited | 429 | Too many AI calls from this app in a short window. | Back off and retry shortly. |
upstream_error | 4xx / 5xx | The model/provider rejected the call (bad input, unavailable model, …). The message carries the provider's reason. | Fix the request, or try another model. |
try {
const res = await env.AI.run("openai/gpt-6-luna", { input: question, max_output_tokens: 512 });
// …
} catch (e) {
if (e.code === "out_of_credits") return Response.json({ error: "No AI credits left" }, { status: 402 });
if (e.code === "byo_key_required") return Response.json({ error: "Set an API key for this model" }, { status: 400 });
return Response.json({ error: e.message }, { status: e.status || 500 });
}