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.

WayCallAPI key?Billed toYou get back
Purpose methodenv.AI.chat({ messages })NoneYour AI creditsres.response
Run by idenv.AI.run("<id>", input)NoneYour AI creditsthe model's native shape
Bring your own keyenv.AI.openai.chat({ … })Your vendor keyThe 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.
The routing is server-side, your app just calls 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:

MethodPassRead 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.searchquery or { query, limit }res.items ([{ url, title, description }])
env.AI.browseurl 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)TaskPurpose method
llama-3.3-70bChat / generalenv.AI.chat
deepseek-r1Reasoning / analysisenv.AI.research
gpt-oss-120bCoding + tools (agentic)env.AI.code
deepseek-v4-flashCoding (fast, agentic)run("deepseek-v4-flash")
deepseek-v4-proCoding (long-horizon)run("deepseek-v4-pro")
glm-5.3-flashCoding (low-cost agentic)run("glm-5.3-flash")
clefTyped decisionsenv.AI.decide
clef-flashTyped decisions (faster)env.AI.decideFast
whisperSpeech → textenv.AI.transcribe
llama-3.2-visionImage → textenv.AI.vision
fluxText → imageenv.AI.image
bge-embedText embeddingsenv.AI.embed
melottsText → speechenv.AI.speak
m2m100Translationenv.AI.translate
bge-rerankerRerank (RAG)env.AI.rerank
distilbert-sentimentSentimentenv.AI.classify
resnet-50Image → class labelsenv.AI.classifyImage
p-image-editImage → imageenv.AI.editImage
wan-i2vImage → videoenv.AI.animateImage
veo-fastText → videoenv.AI.video
bria-video-rmbgVideo → videoenv.AI.editVideo
musicMusicenv.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 kindCallRead 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 Clauderun("anthropic/claude-sonnet-4.5", { messages, max_tokens })res.content[0].text
Google Geminirun("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.

Which provider models are available is up to Cloudflare. A model it doesn't serve key-free fails with a hint to the bring-your-own-key path (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).

MethodPassRead 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" },
});
Video can take one to two minutes per call (the request blocks until it finishes), so run it from a cron job or queue, not a user-facing request that would time out. 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.

MethodPassRead the result atCost
env.AI.searchquery or { query, limit } (1-10)res.items ([{ url, title, description }])1 cr / call
env.AI.browseurl or { url }res.text, res.title, res.finalUrl, res.status1 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 probability 0–1.
  • choice, pick one of your labelled options; returns the choice plus a probability per option and a confidence.
  • score, rate on a level scale; returns a probability-weighted score plus 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 (alias env.AI.gemini) → GOOGLE_API_KEY. Gemini generateContent shape (model + contents, not messages).
  • 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:

ModelInputOutput
llama-3.3-70b
Cloudrizz chat (Llama 70B)
60 cr / 1M500 cr / 1M
deepseek-v4-flash
Cloudrizz code (DeepSeek V4 Flash)
90 cr / 1M270 cr / 1M
deepseek-v4-pro
Cloudrizz code (DeepSeek V4 Pro)
270 cr / 1M800 cr / 1M
deepseek-r1
Cloudrizz research (DeepSeek-R1 32B)
100 cr / 1M1000 cr / 1M
gpt-oss-120b
Cloudrizz code (gpt-oss 120B)
70 cr / 1M150 cr / 1M
glm-5.3-flash
Cloudrizz code (GLM-5.3 Flash)
30 cr / 1M100 cr / 1M
clef
Cloudrizz decide (Clef)
50 cr / 1M0 cr / 1M
clef-flash
Cloudrizz decide (Clef flash)
20 cr / 1M0 cr / 1M
llama-3.2-vision
Cloudrizz vision (Llama 3.2 Vision)
10 cr / 1M140 cr / 1M
bge-embed
Cloudrizz embeddings (BGE large)
41 cr / 1M0 cr / 1M
m2m100
Cloudrizz translate (M2M100)
70 cr / 1M70 cr / 1M
bge-reranker
Cloudrizz rerank (BGE reranker)
1 cr / 1M0 cr / 1M
distilbert-sentiment
Cloudrizz classify (DistilBERT sentiment)
6 cr / 1M0 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 idInputOutput
openai/gpt-4.1-mini80 cr / 1M320 cr / 1M
openai/gpt-6-luna20 cr / 1M100 cr / 1M
openai/gpt-4.1400 cr / 1M1600 cr / 1M
openai/gpt-4.1-nano20 cr / 1M80 cr / 1M
openai/gpt-4o250 cr / 1M1000 cr / 1M
openai/gpt-4o-mini15 cr / 1M60 cr / 1M
openai/gpt-5250 cr / 1M2000 cr / 1M
openai/gpt-5-mini50 cr / 1M400 cr / 1M
openai/gpt-5-nano10 cr / 1M80 cr / 1M
openai/o4-mini220 cr / 1M880 cr / 1M
anthropic/claude-haiku-4.5200 cr / 1M1000 cr / 1M
anthropic/claude-sonnet-4.5600 cr / 1M3000 cr / 1M
anthropic/claude-opus-4.51000 cr / 1M5000 cr / 1M
google/gemini-2.5-flash60 cr / 1M500 cr / 1M
google/gemini-2.5-pro250 cr / 1M2000 cr / 1M
google/gemini-2.5-flash-lite20 cr / 1M80 cr / 1M
google/veo-3.1-fast0 cr / 1M0 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:

ModelPer unitTypical call
p-image-edit
Cloudrizz edit image (Pruna P-Image-Edit)
2 cr / image2 cr for 1 images
wan-i2v
Cloudrizz animate image (Wan 2.7 I2V)
20 cr / second100 cr for 5 seconds
veo-fast
Cloudrizz video (Veo 3.1 Fast)
16 cr / second80 cr for 5 seconds
bria-video-rmbg
Cloudrizz edit video (Bria RMBG 3.0)
5 cr / second25 cr for 5 seconds
music
Cloudrizz music (ElevenLabs Music v2)
0.5 cr / second15 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:

ModelPrice
whisper
Cloudrizz transcribe (Whisper v3 turbo)
1 cr / call
flux
Cloudrizz image (FLUX.1 schnell)
1 cr / call
melotts
Cloudrizz speech (MeloTTS)
1 cr / call
resnet-50
Cloudrizz 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.

Rates can change as upstream provider pricing moves; this table always reflects the current rate. A model with no explicit row (an uncatalogued @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:

codeHTTPMeansDo
byo_key_required400A 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_credits402The org's AI credits are exhausted.Buy a top-up pack or wait for the monthly refill.
rate_limited429Too many AI calls from this app in a short window.Back off and retry shortly.
upstream_error4xx / 5xxThe 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 });
}