跳到內容

Workers AI

查證日期
驗證環境wrangler@4.118.0·compatibility_date: 2026-07-24

⚠️ AI binding 永遠連到 production,本機開發也會計費。本章的推論探針無法在沒有帳號的環境執行,已逐一標明。

第 29 章的結論是 Containers 沒有 GPU。那 Cloudflare 平台上的 GPU 在哪裡?答案是:只在 Workers AI 裡,而且你碰不到 GPU 本身 —— 你碰到的是一個 env.AI.run()

這一章的重點不是「怎麼呼叫模型」(那三行就講完了),而是怎麼在一個模型目錄每幾週就變一次的平台上,寫出不會突然壞掉的程式碼


34.1 型別會騙你(本章最重要的一節)

Section titled “34.1 型別會騙你(本章最重要的一節)”

先講結論:

worker-configuration.d.ts 裡的 AiModels 是一份靜態快照。它包含已經被宣告下架的 model ID,也可能缺少最新的旗艦模型。TypeScript 會替死掉的 model ID 提供完整的自動完成,然後在 production 讓你炸掉。

Cloudflare 在 2026-05-08 的 changelog 宣告,18 個模型將於 2026-05-30 下架

「We are refreshing the Workers AI model catalog to make room for newer releases. Please update your apps to remove references to the models listed below before the deprecation date.」

我把那 18 個 ID 拿去比對本機 wrangler types 產生的 AiModels(共 91 個 model ID):

已宣告下架的 ID仍在生成的型別裡?
@cf/meta/llama-3-8b-instruct
@cf/meta/llama-2-7b-chat-fp16
@cf/meta/llama-2-7b-chat-int8
@cf/mistral/mistral-7b-instruct-v0.1
@cf/google/gemma-3-12b-it
@cf/microsoft/phi-2
@cf/meta/llama-3.1-8b-instruct不在

反過來,官方模型目錄頁置頂的四個旗艦模型:

旗艦 ID在生成的型別裡?
@cf/zai-org/glm-4.7-flash
@cf/openai/gpt-oss-120b
@cf/meta/llama-4-scout-17b-16e-instruct
@cf/moonshotai/kimi-k2.7-code不在(只有 k2.5 與 k2.6)

kimi-k2.5 本身就在下架清單上(會被自動 alias 到更貴的 k2.6)。

所以型別檔同時包含死掉的 ID、又缺少最新的 ID。兩個方向都不可靠。

同樣的 ID 還印在這些頁面上:

  • /workers-ai/get-started/workers-wrangler/(主要 quickstart)—— @cf/meta/llama-3.1-8b-instruct
  • /workers-ai/get-started/rest-api/ —— 同一個
  • /workers-ai/features/json-mode/ —— 支援清單 9 個 ID 裡有 4 個已宣告下架
  • /workers-ai/features/prompt-caching/ —— 範例用 @cf/moonshotai/kimi-k2.5

不要照抄官方範例的 model ID。 這句話在本系列裡出現過很多次,但這一章是最嚴重的一次 —— 因為錯誤不會在編譯期出現,也不會在本機出現,只會在 production 的某一次請求上出現。

另外要誠實說明一件事:官方沒有 /workers-ai/deprecations/ 這樣的頁面(404),下架資訊只存在於那一篇 changelog。而且沒有任何一頁確認 2026-05-30 的下架真的執行了 —— 那些模型的個別頁面現在仍然打得開,只在表格裡有一列 Deprecated: 5/30/2026下架之後打那個 ID 會得到什麼,文件完全沒說。

對策:把模型清單當成需要監控的相依項

Section titled “對策:把模型清單當成需要監控的相依項”

範例專案的 /audit 路由就是為此存在的:

const MODELS = {
chat: "@cf/meta/llama-4-scout-17b-16e-instruct",
cheap: "@cf/zai-org/glm-4.7-flash",
embed: "@cf/google/embeddinggemma-300m",
guard: "@cf/meta/llama-guard-3-8b",
} as const;
const live = await env.AI.models({ per_page: 500 });
const liveNames = new Set(live.map((m) => m.name));
return {
liveModelCount: live.length,
inUse: Object.fromEntries(
Object.entries(MODELS).map(([k, id]) => [k, { id, live: liveNames.has(id) }]),
),
deprecatedStillLive: ANNOUNCED_DEPRECATED.filter((id) => liveNames.has(id)),
};

env.AI.models() 是唯一的真相來源。 型別是快照,文件是快照,這個 API 是即時的。

三個實務建議:

  1. 把所有 model ID 集中在一個 MODELS 常數裡。 分散在二十個檔案裡的字面值,是無法審計的。
  2. 在 CI 跑 /audit,任何 live: false 就讓 build 失敗。這是第 40 章的內容,但這一章就該埋下去。
  3. 每個用途至少準備兩個 model ID(主要 + 備援),這樣下架時只是改一個常數。

{ "ai": { "binding": "AI" } }

schema 有三個欄位:binding(必填)、stagingremote

實測型別定義,run()6 個 overload,關鍵在於哪個選項放哪一格:

// stream 在 INPUTS(第 2 個參數)
run(model, inputs & { stream: true }, options?): Promise<ReadableStream>
// queueRequest / websocket / returnRawResponse 在 OPTIONS(第 3 個參數)
run(model, { requests: [...] }, options & { queueRequest: true }): Promise<AiAsyncBatchResponse>
run(model, inputs, options & { websocket: true }): Promise<Response>
run(model, inputs, options & { returnRawResponse: true }): Promise<Response>
// ✅ 正確
await env.AI.run(model, { messages, stream: true });
// ❌ 錯誤:會走到「一般」那個 overload,回傳完整物件而不是 stream
await env.AI.run(model, { messages }, { stream: true });

第二種寫法不會編譯失敗AiOptions 沒有 additionalProperties 限制),只是靜默地不串流。

type AiOptions = {
queueRequest?: boolean;
websocket?: boolean;
tags?: string[]; // ← 文件較少提到
gateway?: GatewayOptions;
returnRawResponse?: boolean;
prefix?: string;
extraHeaders?: object;
signal?: AbortSignal;
};

tags 的規則寫在型別註解裡:只能含字母、數字與 : - . / @,每個標籤最多 50 字元,每個請求最多 5 個,重複的會被移除。它們會出現在 dashboard 上供分組檢視 —— 對多租戶產品來說,tags: ["tenant:acme"] 是零成本的歸因。

signalAbortSignal,代表你可以對長時間的生成設超時 —— 對第 29 章那條「CPU time 是計費單位」的意識來說很重要。

實測型別註解:

/** @deprecated Use the standalone `ai_search_namespaces` or `ai_search` Workers bindings instead. */
aiSearch(): AiSearchNamespace;
/** @deprecated AutoRAG has been replaced by AI Search. */
autorag(autoragId: string): AutoRAG;

AutoRAG 已經被 AI Search 取代,而且不再掛在 env.AI 上,改成獨立的 binding。第 36 章會處理它。如果你在 2025 年的教學裡看到 env.AI.autorag(...),那已經過時了。

await env.AI.run("@cf/openai/gpt-oss-120b", {
instructions: "You are a concise assistant.",
input: "What is a V8 isolate?",
});

官方模型頁說它「支援三種 API 格式」:Responses API(input)、Workers AI runmessages / prompt / input 皆可,會自動判斷)、Chat Completions(messages)。

所以 messages 不會被拒絕,只是 instructions + input 是文件示範的預設寫法。如果你寫了一個泛用的呼叫包裝函式,這個模型是唯一需要特判的。


34.3 env.AI.run() 現在也跑第三方模型

Section titled “34.3 env.AI.run() 現在也跑第三方模型”

這是 2026 年最大的架構變化。2026-04-16 的 blog:

「Starting today, you can call third-party models using the same AI.run() binding you already use for Workers AI.」

await env.AI.run(
"openai/gpt-4.1-mini",
{ messages: [{ role: "user", content: "hi" }] },
{ gateway: { id: "default" } }, // ← 第三方模型必填
);

第三方模型一定要帶 gateway

「Third-party models require an AI Gateway and use Unified Billing. Cloudflare manages the provider credentials and deducts credits from your account.」

id: "default" 會在第一次通過認證的請求時自動建立一個 gateway。

前綴是什麼計費
@cf/...Workers AI 自有模型Neurons
@cf/deepgram/...@cf/leonardo/...夥伴模型,仍跑在 Workers AI 上Neurons
openai/...google/...真正的第三方 APIAI Gateway Unified Billing(credits)

Unified Billing 的關鍵細節:

「A 5% fee is applied to all credits purchased through Unified Billing. For example, a $100 credit purchase will result in a $105 charge.」

而且:

「Workers AI models(@cf/ 開頭的)經由 AI Gateway 路由時不會走 Unified Billing 計費,它們仍然按 Workers AI 定價計費。」

好處是你不需要自備 OpenAI API key —— Cloudflare 管憑證。代價是 5% 手續費與預付 credit。

型別上,第三方 model ID 走的是那個 fallback overload,註解寫得很清楚:

// Names that aren't in `AiModelList` — e.g. third-party gateway models
// like `"google/nano-banana"` — still hit this overload.
run<Model extends string>(model: Model extends keyof AiModelList ? never : Model,
inputs: Record<string, unknown>, options?: AiOptions): Promise<Record<string, unknown>>;

代價是第三方模型完全沒有型別檢查 —— inputsRecord<string, unknown>,回傳也是。


Neurons:每日 10,000 免費(UTC 00:00 重置),超出 $0.011 / 1,000 Neurons(Free 與 Paid 方案皆可用 Workers AI)。

兩個必須分開處理的錯誤碼(兩者都是 HTTP 429):

意義該怎麼辦
3036「Account limited」—— 每日免費 Neuron 額度用盡不要重試。 今天不會好
3040「Out of capacity」——「No more data centers to forward the request to」應該重試,帶退避

⚠️ 官方的 errors 頁是一張沒有任何說明文字的表格。 沒有重試建議、沒有退避策略、沒有 3040 的處理指引、也沒有連到 Batch API。

唯一沾邊的是 Batch API 頁那句「guarantees request fulfillment even during capacity constraints」,但兩頁之間沒有互相連結。

所以下面這個策略是本章自己的:

async function runWithRetry(env, model, inputs, attempts = 4) {
let lastError;
for (let i = 0; i < attempts; i++) {
try {
return await env.AI.run(model, inputs);
} catch (e) {
const code = e.code;
if (code === 3036) throw e; // 免費額度用盡 -> 今天別試了
if (code !== 3040 && code !== undefined) throw e;
lastError = e;
await new Promise((r) => setTimeout(r, 250 * 2 ** i)); // 250/500/1000/2000ms
}
}
throw lastError;
}

3036 與 3040 都是 429,如果你只看 HTTP 狀態碼就一律重試,會在額度用盡時空轉四次。 必須看 code

2026-03 重新設計成 pull-based。提交與輪詢用的是同一個 env.AI.run()

// 提交 —— queueRequest 在第三個參數
const { request_id } = await env.AI.run(
"@cf/baai/bge-m3",
{ requests: [{ text: "a" }, { text: "b" }] },
{ queueRequest: true },
);
// 輪詢 —— request_id 在第二個參數(inputs)裡
const status = await env.AI.run("@cf/baai/bge-m3", { request_id });
// 處理中回傳 status "queued" 或 "running";完成時回 200 加上依索引對應的結果

payload 上限 10 MB。

文件沒有給輪詢間隔建議、沒有給結果保留期、也沒有列出完整的終態集合。 實務上把 request_id 存進 D1 或 KV,用 Cron Trigger(第 20 章)或 Workflow 的 step.sleep(第 21 章)去輪詢,而不是在請求路徑上 busy-wait。

「Prefix caching only works when a request routes to the same model instance that holds the cached tensors. To maximize cache hit rates, send the x-session-affinity header with a unique identifier for your session or agent.」

await env.AI.run(model, { messages },
{ extraHeaders: { "x-session-affinity": `ses_${conversationId}` } });

省下來的錢是實質的 —— 以 kimi-k2.7-code 為例,快取過的輸入是 $0.19/M 對比一般輸入 $0.95/M,五倍差距。

任何有長 system prompt 的多輪對話都應該設這個 header。


實測 wrangler dev,即使我的設定裡沒有寫 "remote": true

env.AI AI remote
⎔ Establishing remote connection...

wrangler 自動把它標成 remote。沒有憑證時直接失敗:

✘ ERROR Failed to start the remote proxy session. Error reloading remote server:
In a non-interactive environment, it's necessary to set a CLOUDFLARE_API_TOKEN
environment variable for wrangler to work.

官方 quickstart 講得很直白:

「Using Workers AI always accesses your Cloudflare account in order to run AI models and will incur usage charges even in local development.」

bindings-per-env 矩陣上,AI 是 local simulation ❌ / remote binding ✅

⚠️ /workers-ai/configuration/bindings/ 這一頁仍然只寫 {"ai": {"binding": "AI"}},沒有 remote、也沒有任何費用警告 —— 與 Workers 的 local-development 頁不一致。

實測 "remote": falsedeploy --dry-run 的 binding 表格會少掉 remote 標記(不報錯),但按照文件那是無效組態。顯式寫上 "remote": true,讓「這會花錢」這件事在設定檔裡看得見。

實務後果:Workers AI 是本系列到目前為止唯一一個「本機開發直接產生帳單」的 primitive。 開發時請務必:

  • 用最便宜的模型(glm-4.7-flash 的 $0.06/M 對比 kimi-k2.7-code 的 $0.95/M)
  • tags: ["env:dev"] 讓 dashboard 上分得出來
  • 把回應快取進 KV,同一個 prompt 不要重複打

兩個需求:(a) 使用者提交短網址時掃描目標頁面有沒有惡意內容;(b) 產生一句話摘要放進 OG 描述。

// src/ai.ts —— 所有 model ID 集中在這裡(34.1)
export const MODELS = {
guard: "@cf/meta/llama-guard-3-8b", // 專門的安全分類模型
cheap: "@cf/zai-org/glm-4.7-flash", // 摘要用便宜的
embed: "@cf/google/embeddinggemma-300m",
} as const;
export const FALLBACK = {
cheap: "@cf/meta/llama-4-scout-17b-16e-instruct",
} as const;

掃描:走 Queue,不要走請求路徑

Section titled “掃描:走 Queue,不要走請求路徑”
app.post("/api/links", async (c) => {
const { url } = await c.req.json<{ url: string }>();
const slug = await createLink(c.env, url, { status: "pending" });
// 建立立刻回,掃描非同步做(第 19 章)
await c.env.SCAN_QUEUE.send({ slug, url });
return c.json({ slug, status: "pending" }, 202);
});
async queue(batch: MessageBatch<ScanJob>, env: Env) {
for (const msg of batch.messages) {
try {
// 第 30 章:用 Browser Run 的 markdown quick action 取得純文字
const res = await env.BROWSER.quickAction("markdown", {
url: msg.body.url,
rejectResourceTypes: ["image", "media", "font"], // 第 30 章的成本槓桿
});
const { result: markdown } = await res.json<{ result: string }>();
const verdict = await runWithRetry(env, MODELS.guard, {
messages: [{ role: "user", content: markdown.slice(0, 4000) }],
});
const summary = await runWithRetry(env, MODELS.cheap, {
messages: [
{ role: "system", content: "Summarise this page in one sentence." },
{ role: "user", content: markdown.slice(0, 4000) },
],
max_tokens: 60,
});
await env.DB.prepare(
"update links set status = ?1, summary = ?2 where slug = ?3",
).bind(isSafe(verdict) ? "active" : "blocked", extractText(summary), msg.body.slug).run();
msg.ack();
} catch (e) {
if ((e as { code?: number }).code === 3036) {
// 免費額度用盡 -> 明天再說,別佔用重試次數
msg.retry({ delaySeconds: 3600 });
} else {
msg.retry();
}
continue;
}
}
}

四個設計點:

  1. 掃描絕不放在請求路徑上。 推論延遲是數百毫秒到數秒,而且會失敗。
  2. llama-guard-3-8b 是專門的安全分類模型,比拿一個通用 chat 模型問「這安全嗎」更可靠也更便宜。
  3. 3036 用長延遲重試(一小時後),而不是立刻重試 —— 這是 34.4 那條「3036 重試無意義」在 Queue 語境下的正確表達。
  4. 摘要用便宜模型。 一句話的摘要不需要旗艦模型,glm-4.7-flash 的價格是十五分之一。
const key = `scan:${await sha256(url)}`;
const cached = await env.CACHE.get(key, "json");
if (cached) return cached;
// ... 掃描 ...
await env.CACHE.put(key, JSON.stringify(result), { expirationTtl: 86400 * 7 });

推論是本系列所有 primitive 裡單次成本最高的操作。第 8 章的 KV 在這裡的投報率比在任何其他章節都高。


#結論影響
1生成的 AiModels91 個 model ID,其中至少 6 個已在 2026-05-30 的下架清單上型別會替死掉的 ID 提供自動完成
2官方目錄置頂的旗艦 @cf/moonshotai/kimi-k2.7-code 不在生成的型別裡(只有 k2.5、k2.6)型別兩個方向都不可靠
3kimi-k2.5 本身在下架清單上,會被自動 alias 到更貴的 k2.6不改程式碼帳單會漲
4Cloudflare 自家 quickstart、REST quickstart、JSON mode 支援清單、prompt caching 範例都還印著已宣告下架的 ID不要照抄官方範例
5沒有 /workers-ai/deprecations/ 頁面;下架資訊只在一篇 changelog
6沒有任何一頁確認下架真的執行了,也沒說打到已移除 ID 會拿到什麼文件缺口
7env.AI.models() 是唯一的即時真相來源範例的 /audit 應該進 CI
8stream: true 在 inputs(第 2 參數)queueRequest/websocket/returnRawResponse 在 options(第 3 參數)放錯位置不會編譯失敗,只會靜默失效
9AiOptionstags(最多 5 個、各 50 字元、限定字元集)與 signal多租戶歸因與逾時控制
10env.AI.aiSearch()env.AI.autorag() 型別上已標 @deprecatedAutoRAG 已被 AI Search 取代,改用獨立 binding
11@cf/openai/gpt-oss-120binstructions + input(但 messages 也接受,會自動判斷)泛用包裝函式的唯一特例
12第三方模型(openai/...必須帶 gateway,走 Unified Billing不需自備 API key
13Unified Billing 有 5% 手續費($100 credit 收 $105)
14@cf/ 模型經由 AI Gateway 路由時仍走 Neurons,不走 Unified Billing三種前綴要分清楚
15第三方 model ID 走 fallback overload,inputs 與回傳都是 Record<string, unknown>完全沒有型別檢查
163036(額度用盡)與 3040(容量不足)都是 HTTP 429只看狀態碼會在額度用盡時空轉
17官方 errors 頁是沒有任何說明文字的表格 —— 無重試建議本章的退避策略是自己的
18Batch API 提交與輪詢用同一個 run()queueRequest 在 options,request_id 在 inputspayload 上限 10 MB
19Batch 的輪詢間隔、結果保留期、完整終態集合都沒有文件用 Cron 或 Workflow 輪詢,別 busy-wait
20Prompt caching 靠 extraHeaders: { "x-session-affinity": ... };快取輸入可便宜 5 倍長 system prompt 一定要設
21即使設定裡沒寫 remote,wrangler 也自動把 AI binding 標成 remote
22沒有憑證時 wrangler dev 直接失敗(需要 CLOUDFLARE_API_TOKEN
23官方:Workers AI「本機開發也會產生費用本系列唯一一個開發即計費的 primitive
24/workers-ai/configuration/bindings/ 仍只寫 {"ai":{"binding":"AI"}},無 remote、無費用警告與 Workers local-dev 文件不一致
25Neurons 每日 10,000 免費(UTC 00:00 重置),超出 $0.011/1,000

  1. /audit 接進 CI,讓任何 live: false 使 build 失敗。然後刻意把 MODELS.chat 改成 @cf/microsoft/phi-2,確認它擋得住。
  2. { messages, stream: true }{ messages }, { stream: true } 各打一次,比較回傳型別 —— 驗證 34.2 那個靜默失效。
  3. 對同一個長 system prompt 打十次,一半帶 x-session-affinity 一半不帶,比較 dashboard 上的 cached input token 數。
  4. 故意用完當天的 10,000 Neurons,記錄 3036 的完整錯誤物件形狀,確認 e.code 真的存在(本章的重試邏輯依賴它)。
  5. 打一個已下架的 model ID,記錄實際回傳 —— 這是 34.1 那個「文件沒說」的缺口,值得回報成 issue。

下一章(第 35 章):AI Gateway —— 快取、速率限制、fallback、以及本章那個 Unified Billing 的完整樣貌。