AI Gateway
⚠️ AI Gateway 走 AI binding,因此永遠是遠端的(第 34 章),本機開發也會計費。
第 34 章結束在一堆散落的問題上:推論很貴、模型會下架、第三方模型要另外計費、而且你沒有任何統一的地方能看到「這個月誰在燒錢」。
AI Gateway 就是那個統一的地方 —— 快取、限流、重試、可觀測性、成本歸因,全部在一層 proxy 上。
而且它不是一個新的 binding。
35.1 沒有 AI Gateway binding
Section titled “35.1 沒有 AI Gateway binding”實測 —— env 上沒有 GATEWAY 之類的東西。入口是第 34 章那個 ai binding:
const gw = env.AI.gateway("default");型別(實測 worker-configuration.d.ts):
declare abstract class AiGateway { patchLog(logId: string, data: AiGatewayPatchLog): Promise<void>; getLog(logId: string): Promise<AiGatewayLog>; run(data: AIGatewayUniversalRequest | AIGatewayUniversalRequest[], options?): Promise<Response>; getUrl(provider?: AIGatewayProviders | string): Promise<string>;}⚠️
run()的狀態需要說清楚。 本系列大綱初稿寫「gateway.run()已從文件消失」。查證的結果比較細緻:它確實從文件消失了,但仍然存在於 shipped 的型別裡。也就是說它可以編譯、可能還能跑,但已經不是官方指引的路徑。範例的
/gateway-run路由留著就是為了讓你自己確認它現在的實際行為 —— 但不要在新程式碼裡用它。
從 2026-03 起,第一次帶認證的請求會自動建立一個名為 default 的 gateway,你不需要先去 dashboard 手動建。所以 { gateway: { id: "default" } } 是可以直接寫的。
35.2 從 binding 就能全部設定完
Section titled “35.2 從 binding 就能全部設定完”這是最實用、也最少人知道的一點。你不需要手動設 cf-aig-* header —— 透過 env.AI.run() 的 gateway 選項就能設定全部:
type GatewayOptions = { id: string; cacheKey?: string; cacheTtl?: number; skipCache?: boolean; metadata?: Record<string, number | string | boolean | null | bigint>; collectLog?: boolean; eventId?: string; requestTimeoutMs?: number; retries?: GatewayRetries;};await env.AI.run(model, inputs, { gateway: { id: "default", cacheKey: promptHash, // 你自己決定什麼算「一樣」 cacheTtl: 3600, metadata: { tenant: tenantId, feature: "summarise" }, eventId: crypto.randomUUID(), requestTimeoutMs: 20_000, retries: { maxAttempts: 3, retryDelayMs: 500, backoff: "exponential" }, },});metadata 是這裡最有價值的欄位。 它會進到 log 與 analytics,讓「哪個租戶、哪個功能花了多少錢」變成一個可查詢的維度 —— 對比第 22 章那個要自己設計 schema 的 Analytics Engine,這是免費的歸因。
需要手動設 header 的時候
Section titled “需要手動設 header 的時候”如果你不是用 env.AI.run()(例如用 OpenAI SDK 指向 gateway URL),才需要 header。實測型別 AIGatewayHeaders 給了完整清單:
| Header | 用途 |
|---|---|
cf-aig-cache-key | 自訂快取鍵 |
cf-aig-cache-ttl | 快取秒數 |
cf-aig-skip-cache | 跳過快取 |
cf-aig-metadata | 歸因資料 |
cf-aig-event-id | 自訂 event id |
cf-aig-collect-log | 是否記錄 |
cf-aig-custom-cost | 自訂成本:{per_token_in, per_token_out} 或 {total_cost} |
cf-aig-request-timeout | 逾時 |
cf-aig-max-attempts | 重試次數 |
cf-aig-retry-delay | 重試延遲 |
cf-aig-backoff | 退避策略 |
cf-aig-custom-cost 是型別裡才看得清楚的一個 —— 它讓你覆寫 gateway 對成本的估算。用途很具體:你自己談了折扣價、或者你在 gateway 後面接了自架模型,預設的定價表算出來的數字是錯的。
已棄用:
cf-cache-ttl與cf-skip-cache(沒有-aig-的舊版)。舊教學裡看到的話直接換掉。
35.3 認證的陷阱
Section titled “35.3 認證的陷阱”這是最常見的 401 來源。兩個入口的認證方式不一樣:
| 入口 | Cloudflare token 放哪 | Authorization 給誰 |
|---|---|---|
gateway.ai.cloudflare.com/... | cf-aig-authorization | provider 的 key |
api.cloudflare.com/client/v4/accounts/{id}/ai/... | Authorization: Bearer | Cloudflare |
在 gateway.ai.cloudflare.com 上,Authorization 這個 header 是保留給下游 provider 的 —— 因為 gateway 要把它原封不動轉發給 OpenAI 或 Anthropic。你自己的 Cloudflare token 必須走 cf-aig-authorization。
寫錯的話你會得到一個很難懂的 401:可能是 Cloudflare 拒絕你,也可能是 provider 拒絕你的 Cloudflare token。
用 env.AI.run() 的話這個問題完全不存在 —— binding 幫你處理認證。這是另一個「能用 binding 就用 binding」的理由。
入口本身也換過位置
Section titled “入口本身也換過位置”現在建議的路徑是:
POST https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1/chat/completions(以及 /responses、/messages)而且有一條兩段式的棄用鏈:
- Universal Endpoint 的文件頁標題直接寫著 「(Deprecated)」,指向 OpenAI-compatible endpoint。
- 而那個 OpenAI-compatible endpoint 的頁面自己又寫著已棄用,請改用 REST API。
Provider-specific endpoints 並沒有被棄用。
這種「A 棄用指向 B,B 又棄用指向 C」的鏈條,是照著搜尋結果做最容易掉進去的坑。如果你在 Worker 裡,這整段都繞過去了 —— 用
env.AI.run()加gateway選項就好。
35.4 快取:省錢最直接的一刀
Section titled “35.4 快取:省錢最直接的一刀”推論的成本結構和第 6 章的 HTTP 快取完全不同:一次快取命中省下的不是幾毫秒的延遲,是一次完整的模型呼叫費用。
{ gateway: { id: "default", cacheKey: hash, cacheTtl: 3600 } }cacheKey 由你決定,這是關鍵設計點。 預設的快取鍵是整個請求,代表 prompt 差一個字就 miss。但很多場景下你想要更寬鬆的等價關係:
// 摘要同一個 URL 的頁面:內容 hash 當鍵,prompt 的細微差異不影響cacheKey: `summarise:v2:${await sha256(pageContent)}`
// 分類:正規化後的輸入當鍵cacheKey: `classify:v1:${normalise(text)}`把版本號寫進鍵裡(v2)—— 改了 prompt 或換了模型時,遞增版本號就等於全域失效,不用等 TTL。這和第 12 章 R2 的快取策略是同一個模式。
範例的 /cache-proof 路由用同一個 cacheKey 打兩次,比較延遲與 log 中的 cached 欄位。
35.5 可觀測性:log 的形狀
Section titled “35.5 可觀測性:log 的形狀”AiGatewayLog 的完整欄位(實測型別):
type AiGatewayLog = { id: string; provider: string; model: string; model_type?: string; path: string; duration: number; status_code: number; success: boolean; cached: boolean; tokens_in?: number; tokens_out?: number; cost?: number; custom_cost?: boolean; metadata?: Record<...>; request_size: number; response_size: number; request_head?: string; request_head_complete: boolean; response_head?: string; response_head_complete: boolean; step?: number; created_at: Date;};三個特別有用的:
cached: boolean—— 直接告訴你這一筆有沒有命中快取。快取命中率是一個 SQL 查詢的事。cost與custom_cost—— gateway 幫你算好了每一次呼叫的成本,custom_cost標示是否被cf-aig-custom-cost覆寫過。step—— 多步驟(例如 fallback 到第二個 provider)時的步驟編號。
人類回饋的迴路
Section titled “人類回饋的迴路”patchLog(logId: string, data: { score?: number | null; feedback?: -1 | 1 | null; metadata?: Record<...> | null;}): Promise<void>;流程是:呼叫模型 → 從 env.AI.aiGatewayLogId 拿到這次的 log id → 把 id 送給前端 → 使用者按讚/倒讚 → patchLog()。
const answer = await env.AI.run(model, inputs, { gateway: { id: "default" } });const logId = env.AI.aiGatewayLogId; // ← 這一次呼叫的 log idreturn Response.json({ answer, logId });// 使用者回饋await env.AI.gateway("default").patchLog(logId, { feedback: -1, score: 20 });feedback 的型別是 -1 | 1 | null —— 就是倒讚 / 讚 / 清除。score 是 0-100 的自由分數。
這件事的價值在於:評估資料集是免費長出來的。 你不需要另外設計一張表、不需要把 prompt 和回應再存一份 —— gateway 已經有完整的請求與回應,你只是把人類的判斷貼上去。之後要做模型比較或 fine-tune,資料就在那裡。
35.6 支援的 provider
Section titled “35.6 支援的 provider”實測型別 AIGatewayProviders,恰好 20 個:
workers-ai · anthropic · aws-bedrock · azure-openai · google-vertex-aihuggingface · openai · perplexity-ai · replicate · groq · coheregoogle-ai-studio · mistral · grok · openrouter · deepseek · cerebrascartesia · elevenlabs · adobe-firefly網路上(含本系列大綱初稿)常見「23 家 provider」的說法。shipped 型別裡是 20 個。 兩者差異可能是文件與型別的發布時間差,也可能是把 custom provider 之類的算進去了。以型別為準,因為那是你的程式碼實際能用的。
注意 getUrl(provider?: AIGatewayProviders | string) 的 | string —— 型別刻意留了逃生口給 custom provider(beta 功能)。
35.7 GA 與 beta 的界線
Section titled “35.7 GA 與 beta 的界線”這一節很重要,因為 AI Gateway 的功能清單看起來很豐富,但有相當一部分是 beta。
| GA | Beta |
|---|---|
| Analytics、Logging | Spend limit |
| Caching | Dynamic Routing |
| Rate limiting | DLP |
| Retries / timeouts | Guardrails |
| 20 家 provider | BYOK |
| Custom cost | Custom provider |
| OTel export、Logpush | WebSockets |
Spend limit在 beta 這件事值得單獨提醒。 很多人選 AI Gateway 就是為了「設一個上限避免帳單失控」,但那個功能目前是 beta。在它 GA 之前,成本控制的第一道防線仍然是你自己的程式碼(快取 + 便宜模型 + 用量記錄),不是 gateway 的開關。
Guardrails 的兩個成本
Section titled “Guardrails 的兩個成本”Guardrails(beta)用的是 @cf/meta/llama-guard-3-8b —— 就是第 34 章 LinkForge 掃描用的那個模型。
兩個必須知道的代價:
- 增加約 500 ms 延遲。 因為它是在你的請求前後各跑一次分類。
- 不支援 streaming。 這一條是硬傷 —— 如果你的產品是聊天介面,開了 Guardrails 就不能串流輸出,使用者體驗會明顯變差。
替代做法:自己在 Worker 裡呼叫 llama-guard-3-8b,只對輸入做檢查(輸入檢查不影響輸出串流),輸出則用串流 + 前端的漸進式過濾。這樣你拿回了控制權,代價是要自己寫。
35.8 一個名字撞車的陷阱
Section titled “35.8 一個名字撞車的陷阱”AI Gateway 裡沒有任何 MCP 功能。
Cloudflare 的 MCP Server Portals 是 Cloudflare One / Zero Trust 的功能,不是 AI Gateway 的。兩者名字都有「gateway 到 AI 服務」的味道,害很多人在 AI Gateway 的文件裡繞了半天。
第 37 章談 MCP 時走的是 Agents SDK 那條路,和本章無關。
35.9 LinkForge:把所有 AI 呼叫收攏
Section titled “35.9 LinkForge:把所有 AI 呼叫收攏”第 34 章的掃描與摘要現在全部走 gateway:
const GATEWAY = "default";
export async function summarise(env: Env, tenantId: string, content: string) { const key = `sum:v3:${await sha256(content)}`; // 版本號在鍵裡(35.4)
const out = await env.AI.run( MODELS.cheap, { messages: [ { role: "system", content: SUMMARY_PROMPT }, { role: "user", content: content.slice(0, 4000) }, ], max_tokens: 60, }, { gateway: { id: GATEWAY, cacheKey: key, cacheTtl: 86400 * 7, // 摘要一週內不會變 metadata: { tenant: tenantId, feature: "summarise" }, // 成本歸因 requestTimeoutMs: 15_000, retries: { maxAttempts: 3, retryDelayMs: 400, backoff: "exponential" }, }, tags: [`tenant:${tenantId}`, "feature:summarise"], // 第 34 章 }, );
return { text: extractText(out), logId: env.AI.aiGatewayLogId };}四層防護疊起來:
| 層 | 機制 | 擋掉什麼 |
|---|---|---|
| 1 | KV 快取(第 8 章) | 同一 URL 的重複請求,連 gateway 都不碰 |
| 2 | gateway cacheKey | 不同 URL 但內容相同的頁面 |
| 3 | retries | 第 34 章的 3040(容量不足) |
| 4 | requestTimeoutMs | 卡住的請求佔用 CPU 時間 |
metadata.tenant 加上 log 裡的 cost 與 cached,就構成了一個完整的成本歸因模型。搭配第 22 章的 Analytics Engine 做雙寫:
const { text, logId } = await summarise(env, tenantId, content);
env.AE.writeDataPoint({ blobs: ["summarise", tenantId, MODELS.cheap], doubles: [1, /* tokens */ 0], indexes: [tenantId],});為什麼要雙寫? gateway 的 analytics 有自己的保留期與查詢介面,而 Analytics Engine 是你自己的(第 22 章)。把「AI 成本」和你其他的用量指標放在同一個查詢平面上,才做得出「這個租戶整體毛利」這種問題的答案。
app.post("/api/summary/:slug/feedback", async (c) => { const { logId, thumb } = await c.req.json<{ logId: string; thumb: "up" | "down" }>(); // 驗證 logId 屬於這個租戶,避免任意 patch 別人的 log if (!(await ownsLog(c.env, c.get("session").tenantId, logId))) { return c.json({ error: "forbidden" }, 403); } await c.env.AI.gateway("default").patchLog(logId, { feedback: thumb === "up" ? 1 : -1, metadata: { tenant: c.get("session").tenantId }, }); return c.body(null, 204);});注意那個 ownsLog 檢查。 logId 是從前端傳回來的,而 patchLog() 不會替你驗證擁有權 —— 這和第 27 章的原則一致:從外面回來的識別碼都是使用者輸入。
35.10 本章實測結論彙整
Section titled “35.10 本章實測結論彙整”| # | 結論 | 影響 |
|---|---|---|
| 1 | 沒有獨立的 AI Gateway binding,入口是 env.AI.gateway(id) | |
| 2 | AiGateway 有 patchLog / getLog / run / getUrl 四個方法 | |
| 3 | gateway.run() 從文件消失但仍存在於 shipped 型別 | 可編譯,但不是官方路徑,新程式碼不要用 |
| 4 | 2026-03 起首次認證請求會自動建立名為 default 的 gateway | 不必先手動建 |
| 5 | GatewayOptions 能從 binding 設定 cacheKey/cacheTtl/skipCache/metadata/collectLog/eventId/requestTimeoutMs/retries | 不需要手寫 cf-aig-* header |
| 6 | AIGatewayHeaders 型別給出完整 header 清單,含未被廣泛記載的 cf-aig-custom-cost | 可覆寫成本估算 |
| 7 | 已棄用:cf-cache-ttl、cf-skip-cache(無 -aig- 的舊版) | |
| 8 | 認證位置隨入口而異:gateway.ai.cloudflare.com 用 cf-aig-authorization,api.cloudflare.com 用 Authorization: Bearer | 最常見的 401 來源;用 binding 則完全不用管 |
| 9 | 兩段式棄用鏈:Universal Endpoint →(已棄用)OpenAI-compatible →(也已棄用)REST API | Provider-specific endpoints 未棄用 |
| 10 | cacheKey 完全由你決定 | 把版本號寫進鍵裡,改 prompt 時可全域失效 |
| 11 | AiGatewayLog 含 cached、cost、custom_cost、tokens_in/out、step | 快取命中率與成本都是現成的 |
| 12 | patchLog 的 feedback 型別是 -1 | 1 | null,score 是自由數值 | 評估資料集免費長出來 |
| 13 | env.AI.aiGatewayLogId 是取得本次 log id 的管道 | 回饋迴路的起點 |
| 14 | AIGatewayProviders 型別列出恰好 20 個 provider | 常見的「23 家」說法與型別不符,以型別為準 |
| 15 | getUrl(provider?: AIGatewayProviders | string) 保留 | string | 給 custom provider(beta)用的逃生口 |
| 16 | Spend limit 是 beta | 成本控制第一道防線仍是你自己的程式碼 |
| 17 | Guardrails(beta)用 @cf/meta/llama-guard-3-8b,+約 500ms 且不支援 streaming | 聊天介面要自己實作輸入端檢查 |
| 18 | Dynamic Routing、DLP、BYOK、custom provider、WebSockets 皆為 beta | |
| 19 | AI Gateway 沒有 MCP 功能 | MCP Server Portals 屬於 Cloudflare One / Zero Trust |
| 20 | patchLog() 不驗證 log 的擁有權 | 從前端回傳的 logId 必須自己驗證 |
35.11 動手練習
Section titled “35.11 動手練習”- 用同一個
cacheKey連打兩次,用getLog()確認第二筆的cached是true,並比較duration與cost。 - 把
cf-aig-custom-cost設成一個你談好的折扣價,確認 log 裡的cost與custom_cost都變了。 - 實作 35.5 的回饋迴路,然後用
getLog()讀回來,確認score與feedback真的貼上去了。 - 呼叫
/gateway-run,記錄gateway.run()現在的實際行為 —— 它從文件消失但還在型別裡,這個缺口值得自己確認。 - 開啟 Guardrails,量測同一個請求開/關時的延遲差異,並確認 streaming 真的失效。
- AI Gateway · Worker binding methods · Unified Billing
- Caching · Rate limiting · Guardrails · Custom costs
- 型別來源:
wrangler types產生的worker-configuration.d.ts(AiGateway、GatewayOptions、AiGatewayLog、AiGatewayPatchLog、AIGatewayProviders、AIGatewayHeaders)—— 本章大部分結論來自這裡,因為它比文件完整
下一章(第 36 章):Vectorize 與 RAG —— 以及第 34 章型別裡那個
@deprecated的 AutoRAG 現在變成了什麼。