第 2 章:换一颗大脑 —— 用 AI Platform 一行切 provider
一句话定位:把 Llama 换成 Claude / GPT / Gemini,只动一个 env 变量;不装任何
@ai-sdk/*provider 包,统一走 Cloudflare AI Platform。
想要什么
第 1 章那个 CoderAgent 已经会流式回话了 —— Think 替你接好了 Vercel AI SDK 的 SSE 流,客户端用 useAgentChat 一行收完。这一章我们盯着另一个旋钮:模型本身。
打开终端:
# Terminal
echo 'AI_PROVIDER = "anthropic"' >> .dev.vars
npx wrangler dev
再 curl 同一个 /api/chat,这次背后是 Claude Opus,不是 Llama。把 anthropic 改成 openai、google、workers-ai,模型立刻换,业务代码一行不动 —— 连 package.json 都不用动。
具体场景:你写代码用 Anthropic,跑批用便宜的 Workers AI 开源模型,生成长文摘要用 Gemini 的 1M context。三套 prompt 共用一个 CoderAgent,provider 是配置,不是依赖。
为什么
Vercel AI SDK 的传统玩法:每个 provider 一个 npm 包 —— @ai-sdk/openai、@ai-sdk/anthropic、@ai-sdk/google、@ai-sdk/mistral、@ai-sdk/cohere。每个包带自己的鉴权 / 重试 / 限流逻辑、自己的 model id 命名习惯、自己的版本节奏。装四个就是四份心智负担。
更糟的是 secret 管理:每个 provider 一个 API key,得分别 wrangler secret put,在 dashboard 里分别看用量,出账时分别对账。要做灰度(70% 流量打 Claude、30% 打 GPT),你得自己写路由。
Cloudflare AI Platform(2026 Agents Week GA)把这一层全平掉了。env.AI.run("anthropic/claude-opus-4-6", ...) 直接调 Anthropic;env.AI.run("openai/gpt-5", ...) 调 OpenAI;env.AI.run("@cf/meta/llama-3.3-70b-instruct-fp8-fast", ...) 还是 Workers AI —— 同一个 binding、同一份账单、同一份 AI Gateway 可观测性、同一套自动 failover。你只要把 provider 的 BYOK key 配在 dashboard 的 AI Gateway 里(或者用 Cloudflare 自己的额度),代码端完全不知道下游是谁。
对 Think 来说更省事:getModel() 仍然返回一个 Vercel AI SDK 的 LanguageModel,只是这个 model 的“adapter“统一从 workers-ai-provider 来,model id 带个 provider 前缀就够了。第 1 章的 createWorkersAI 不用扔,只是它的“领地“扩大了:整个 AI Platform 都归它管。
图 2-1:AI Platform 把 provider 收成一个 binding
方案选择
| 方案 | 适合什么 | 我们用吗 |
|---|---|---|
每个 provider 一个 @ai-sdk/* 包(原生 baseURL 指 Gateway) | 想吃单家 SDK 的全部参数细节 | 不用,4 个包 4 份心智 |
直接 OpenAI / Anthropic 官方 SDK | 不用 Vercel AI SDK 的统一 streaming 协议 | 不用,跟 Think 不顺手 |
workers-ai-provider 适配器 + AI Platform 前缀 | 想用 AI SDK 抽象,但 provider 只用 Workers AI | 不用,3.1.x 不认 anthropic/openai/google 响应,silent 200 OK 空 body |
ai-gateway-provider + createUnified() | 一行 init 切所有 provider,享 gateway retry/cache/fallback | 用 —— Cloudflare 官方包装,业务代码完全不感知下游 |
@ai-sdk/openai-compatible 裸用 + /compat baseURL | 跟 D 等价但少了 gateway 元能力 | 能跑但更繁琐 |
env.AI.run({stream:true}) + 自己解析 SSE | 想绕开 SDK 自己控制每家 provider 的原生格式 | 不用(可作兜底),代码量大、要维护多家 SSE 字段差异 |
AI Platform 的 provider 前缀约定有两套 —— 用哪个看你打的端点:
- 走
env.AI.run("...")binding:Cloudflare catalog id,如anthropic/claude-haiku-4.5(.分隔)、@cf/meta/llama-3.3-70b-instruct-fp8-fast、google/gemini-2.5-pro。- 走 AI Gateway
/compat端点:provider 自家原生 id,如anthropic/claude-haiku-4-5(-分隔)、workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast(双前缀)、google-ai-studio/gemini-2.5-pro(注意 provider 前缀也变了)。本章用的是
/compat路径,所以下面所有 model id 用第二套。
落地
这一章的所有改动只动 src/agent.ts 和 wrangler.jsonc。不装新包,不改入口。
1. 真相先讲清:三条可行路径的取舍
理论上有三条路把多 provider 接进来:
| 路径 | npm 包 | 端点 | 响应归一? | 评价 |
|---|---|---|---|---|
A. workers-ai-provider + env.AI binding | workers-ai-provider | env.AI.run("provider/...") | ❌ 只认 Workers AI 与 OpenAI shape | 第三方 provider 静默失败(200 OK + 空 body),不能用 |
B. 三个原生 @ai-sdk/* + provider URL | @ai-sdk/anthropic、@ai-sdk/openai、@ai-sdk/google | 各自 https://gateway.ai.cloudflare.com/v1/.../<provider> | ✅ 每家 SDK 自己懂 | 装 4 个包、4 份 init,重 |
C. @ai-sdk/openai-compatible + /compat 端点(裸用) | @ai-sdk/openai-compatible | https://gateway.ai.cloudflare.com/v1/{ACC}/{GW}/compat | ✅ Cloudflare 把所有 provider 翻译成 OpenAI shape | 能跑,但 baseURL 拼装、metadata/cache/retry 都要手写 |
D. ai-gateway-provider + createUnified() | ai-gateway-provider(内部用 @ai-sdk/openai-compatible) | 同 C | ✅ 同上 | 官方包装,免拼 URL,带 retry/cache/metadata/fallback chain —— 我们用这个 |
/compat 是 Cloudflare AI Gateway 暴露的“统一推理面“:你 POST 标准 OpenAI Chat Completion 请求,在 model 字段写 "<provider>/<model-id>",Cloudflare 在背后帮你跟下游 API 通信、把回应翻成 OpenAI shape 再返回。ai-gateway-provider 把 /compat 端点 + createUnified() 包装好了,顺便还提供 createAiGateway 给你拿到 retry / cache / metadata / fallback chain 这些 gateway 元能力。
路径 A 我们写书时实测发现
workers-ai-provider@3.1.13的processText()只识别 OpenAI 的choices[0].message.content和 Workers AI 的output.response,不认 Anthropic 的content[0].text,不认 Anthropic SSE 的event: content_block_delta,不认 Gemini 的candidates[0].content.parts[]—— 所以streamText会“成功“返回 200 OK,body 全空,onError里能拿到错但响应已经发出去了。该 bug 在仓库里没看到 issue/PR 跟进,所以本章绕开它。
2. 装新包 + 完整 agent.ts(实测可跑,4 provider 都验过)
# Terminal
npm install ai-gateway-provider@latest
ai-gateway-provider 是 Cloudflare 官方维护的 AI Gateway 适配器,内部基于 @ai-sdk/openai-compatible,但帮你包了三件事:/compat URL 拼装、createUnified() 统一 provider 入口、gateway 元能力(retry / cache / metadata / fallback chain)。ai-gateway-provider 已经把 @ai-sdk/openai-compatible 作为 transitive dependency 装上了,你不用再单独装。
// src/agent.ts
import { Think } from "@cloudflare/think";
import { createAiGateway } from "ai-gateway-provider";
import { createUnified } from "ai-gateway-provider/providers/unified";
import {
streamText,
convertToModelMessages,
type LanguageModel,
type UIMessage,
} from "ai";
export type AIProvider = "workers-ai" | "anthropic" | "openai" | "google";
export type Env = Omit<Cloudflare.Env, "AI_PROVIDER"> & {
AI_PROVIDER?: AIProvider;
AI_GATEWAY_ID?: string;
CF_ACCOUNT_ID?: string;
CF_AIG_TOKEN?: string;
};
// ⚠️ /compat 端点的 model id 是 "provider/model-id",有三个反直觉命名差异:
// - workers-ai 仍带 `@cf/...` 老前缀 —— /compat 上要写双前缀:workers-ai/@cf/...
// - anthropic 用 **provider 自家命名**(claude-haiku-4-5,横线),
// 而不是 env.AI.run 直调时 Cloudflare catalog 的命名(claude-haiku-4.5,小数点)
// - google 的 provider 前缀是 `google-ai-studio`,不是 `google`
const MODEL_MAP: Record<AIProvider, string> = {
"workers-ai": "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast",
"anthropic": "anthropic/claude-haiku-4-5",
"openai": "openai/gpt-5-mini",
"google": "google-ai-studio/gemini-2.5-pro",
};
export class CoderAgent extends Think<Env> {
// createAiGateway:gateway 元信息(账号/网关/auth token + 可选 retry/cache/metadata)
// createUnified:返回一个 provider,model id 用 "provider/model" 字符串切下游
// 组合 aigateway(unified("anthropic/...")) 就得到一个 LanguageModel
getModel(): LanguageModel {
const provider = this.env.AI_PROVIDER ?? "workers-ai";
const aigateway = createAiGateway({
accountId: this.env.CF_ACCOUNT_ID!,
gateway: this.env.AI_GATEWAY_ID ?? "default",
apiKey: this.env.CF_AIG_TOKEN,
});
const unified = createUnified();
return aigateway(unified(MODEL_MAP[provider]));
}
getSystemPrompt() {
return "你是一个有帮助的中文技术助手,回答简洁、准确。回答时优先给可执行示例,不啰嗦。";
}
async onRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/chat" && request.method === "POST") {
const { messages } = await request.json<{ messages: UIMessage[] }>();
const result = streamText({
model: this.getModel(),
system: this.getSystemPrompt(),
messages: await convertToModelMessages(messages),
onError: ({ error }) => console.error("streamText:", error),
});
return result.toTextStreamResponse();
}
return super.onRequest(request);
}
}
整个 onRequest 没有任何 provider 分支 —— provider 切换、shape 翻译、SSE 归一全部由 /compat 端点 + createUnified() 内部完成。streamText 完全感知不到下游是 Llama / Claude / GPT-5 / Gemini。
createAiGateway 在 getModel() 里还能加这些 gateway 元能力(本章不展开,但重要):
const aigateway = createAiGateway({
accountId, gateway: "default", apiKey,
options: {
cacheTtl: 300, // 5 分钟相同 prompt 走缓存
metadata: { tenant: this.name, agent: "ch02" }, // 在 dashboard logs 里能按 tenant 过滤
retries: { maxAttempts: 3, backoff: "exponential" },
},
});
// 还能传一组 model 实现 fallback chain —— 第一个挂了自动转下一个:
aigateway([unified("anthropic/claude-haiku-4-5"), unified("openai/gpt-5-mini")]);
七个真踩过的细节:
AI_PROVIDER走.dev.vars/ 部署时--var,不要写进wrangler.jsonc的vars。一旦写进去,wrangler 会把它推成"workers-ai"字面量,跟我们Env里的宽联合类型直接打架,tsc 报Type ... is not assignable to type "workers-ai"。- AI Gateway auth token 是新东西,跟 wrangler OAuth token 不是一码事。dashboard → AI Gateway → 你的 gateway → Settings → Authentication → Create Token,形如
cfut_xxx。 /compat上的 workers-ai model id 是双前缀:workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast。少了workers-ai/前缀返Invalid provider,留下来的@cf/...仍然要写全。- Anthropic 的 model id 在
/compat上用-分隔版本号(claude-haiku-4-5),跟env.AI.run直调时 Cloudflare catalog 的命名(claude-haiku-4.5)是两套。/compat 把 model id 透传给下游 Anthropic,所以用 Anthropic 自家命名。 - Google 的 provider 前缀是
google-ai-studio,不是google。写google/gemini-2.5-pro会返Invalid provider。 - 第三方 provider 都需要充值或 BYOK。dashboard → AI Gateway → Add credit(unified billing)或 Provider Keys → 粘自家 key(BYOK)。否则统一返
Insufficient balance; add money to your gateway or use BYOK。Workers AI 自家模型走免费额度,不受影响。 streamText的失败会被toTextStreamResponse()吞成 200 OK + 空 body。务必加onError把错误打到日志(wrangler tail能看到),不然 curl 看到空响应会摸不着头脑。
3. 准备凭证 + .dev.vars
/compat 必须带 AI Gateway 的 auth token(我们叫它 CF_AIG_TOKEN),还要知道你的 account id 和 gateway id:
# Terminal
# 1. dashboard → AI Gateway → 你的 gateway(没有就先 Create,默认叫 "default")→
# Settings → Authentication → Create Token,拷贝下来,形如 cfut_xxxxxxx
# 2. dashboard 右上角侧栏 → Account ID,拷贝下来
cat > .dev.vars <<EOF
AI_PROVIDER = "workers-ai"
CF_ACCOUNT_ID = "<你的 account id>"
AI_GATEWAY_ID = "default"
CF_AIG_TOKEN = "cfut_xxxxxxxxxxxxxxxxxxxxxxxxx"
EOF
CF_AIG_TOKEN是真敏感凭证(任何人拿到都能用你账号扣费),生产环境必须用wrangler secret put CF_AIG_TOKEN而不是--var。本章用--var只为了演示流程,生产部署一定要切到secret。
4. wrangler.jsonc(跟 ch01 完全一致,不加 vars)
// wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "agent-coder",
"main": "src/index.ts",
"compatibility_date": "2026-04-30",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "class_name": "CoderAgent", "name": "CODER_AGENT" }]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["CoderAgent"] }
]
}
我们故意不在
wrangler.jsonc的vars里写AI_PROVIDER—— wrangler 会把它推成字面量"workers-ai",跟我们Env里宽联合类型冲突,tsc 报Type ... is not assignable to type "workers-ai"。改用下面两条任意一条来设值即可。改完
wrangler.jsonc别忘了重跑npx wrangler types。
5. 客户端不动
useAgentChat 收的还是 Vercel AI SDK 的 UIMessage 流,跟模型无关 —— 这正是统一 SDK 的好处。第 1 章那段前端代码(或 curl)一行不用改。
验证
跟第 1 章一样,部署到边缘 curl 是最稳的路径(本地 wrangler dev → 远端推理的代理常见挂)。先准备 4 个 var,然后逐个 provider 部署 + curl。
# Terminal
ACC=<你的 account id>
TOKEN=cfut_xxxxxxxxxxxxxxxxxxxxxxxxx # 第 3 步生成的 AI Gateway auth token
# Workers AI(免费额度)
CLOUDFLARE_ACCOUNT_ID=$ACC npx wrangler deploy \
--var AI_PROVIDER:workers-ai \
--var "CF_ACCOUNT_ID:$ACC" \
--var "CF_AIG_TOKEN:$TOKEN"
curl -N -X POST https://agent-coder.<your-subdomain>.workers.dev/api/chat \
-H 'content-type: application/json' \
-d '{"messages":[{"role":"user","parts":[{"type":"text","text":"用一句话讲 Cloudflare Durable Objects"}]}]}'
# → Llama 中文回答
# Anthropic(需充值或 BYOK)
CLOUDFLARE_ACCOUNT_ID=$ACC npx wrangler deploy \
--var AI_PROVIDER:anthropic \
--var "CF_ACCOUNT_ID:$ACC" \
--var "CF_AIG_TOKEN:$TOKEN"
curl -N -X POST https://agent-coder.<your-subdomain>.workers.dev/api/chat \
-H 'content-type: application/json' \
-d '{"messages":[{"role":"user","parts":[{"type":"text","text":"用一句话讲 Cloudflare Durable Objects"}]}]}'
# → Claude Haiku 4.5 中文回答(语气更结构化)
把 AI_PROVIDER 换成 openai / google 重跑,会拿到 GPT-5-mini / Gemini 2.5 Pro 的回答 —— 业务代码一行不用动,只是 worker 重新部署,因为这是生产 var 注入。本地开发要切 provider 时改 .dev.vars 重启 wrangler dev 就行,无需重 deploy。
没钱?预期看到的错误
切到 Anthropic / OpenAI / Google 后,如果你账号没充值也没配 BYOK,curl 会看到 200 OK + 空 body(streamText.toTextStreamResponse() 把错吞了)。wrangler tail 看日志能看到真错:
streamText: AI_APICallError: Insufficient balance; add money to your gateway or use BYOK
两条路任选:
- Unified billing(充值):dashboard → AI Gateway → 你的 gateway → Add credit。Cloudflare 按 token 转售下游 provider,统一一份账单。
- BYOK(自带 key):dashboard → AI Gateway → 你的 gateway → Provider Keys → 粘上你自己的 Anthropic / OpenAI / Google key。这样调用走你自己 provider 的额度,Cloudflare 只做透传 + 可观测。
想直接看下游真错?加一个 /debug/raw 端点
streamText 那条链路有时会吞 error。要看下游返回的真原因,在 src/index.ts 入口加一个 /debug/raw,直接调 env.AI.run 拿到原始 JSON:
// src/index.ts(调试用,验证完可删)
if (url.pathname === "/debug/raw") {
const provider = url.searchParams.get("provider") ?? "anthropic";
// 注意:env.AI.run 用的是 Cloudflare catalog id(`.` 分隔),跟 /compat 用的是两套
const map: Record<string, string> = {
anthropic: "anthropic/claude-haiku-4.5",
openai: "openai/gpt-5-mini",
google: "google/gemini-2.5-pro",
"workers-ai": "@cf/meta/llama-3.3-70b-instruct-fp8-fast",
};
try {
const out = await env.AI.run(map[provider] as any, {
messages: [{ role: "user", content: "测试" }],
max_tokens: 256,
} as any, { gateway: { id: "default" } } as any);
return Response.json({ ok: true, out });
} catch (e) {
return Response.json({ ok: false, message: (e as Error).message }, { status: 500 });
}
}
curl https://你的域名/debug/raw?provider=anthropic 出来三种典型结果:
"5007: No such model anthropic/..."→ model id 写错(注意 env.AI.run 用.)"Insufficient balance..."→ ✅ 路由通,需要充值或 BYOK{"ok": true, "out": {...}}→ ✅ 全通
边界与坑
env.AI.runvs/compat用两套 model id 命名(踩坑实测):env.AI.run→ Cloudflare catalog 命名(anthropic/claude-haiku-4.5,.分隔;workers-ai 单前缀@cf/...)/compat→ provider 原生命名(anthropic/claude-haiku-4-5,-分隔;workers-ai 双前缀workers-ai/@cf/...;google 前缀变google-ai-studio)
- provider 列表以 dashboard 为准。Cloudflare 在不停加(Bytedance、Alibaba、AssemblyAI、Pixverse 等都在 2026 GA 列表),写代码前先去 AI Platform 文档 确认拼写。
workers-ai-provider@3.1.x不能跨 provider:它的processText()只认 Workers AI 自家output.response和 OpenAI 的choices[0].message.content,Anthropic 的content[0].text/content_block_deltaSSE 不认,Gemini 的candidates[0].content.parts[]也不认。直接用streamText+workers-ai-provider("anthropic/...")会 silent 200 OK 空 body,onError才能拿到真错。/compat必须带 AI Gateway auth token(不是 wrangler OAuth token,也不是 CF API token —— 是 dashboard → AI Gateway → Settings → Authentication 里专门生成的,形如cfut_xxx)。streamText+toTextStreamResponse()会吞 error:HTTP 200 + 空 body,onError回调能拿到错误但响应已发。Debug 时用/debug/raw端点直接调env.AI.run,错误更直白。- tool call 行为差异不会被 SDK 抹平:Claude 偏好
parallel_tool_calls,GPT 偏好strict: trueschema,Llama 在工具上表现弱。第 4 章引入工具时会讲选模型策略。 - failover 是 provider 级别,不是 model 级别:Anthropic 整家挂了,Cloudflare 会自动转给 OpenAI(在 Gateway 配 fallback);单个 model ID 不可用不会触发自动转移。
AI_PROVIDER别写进wrangler.jsonc的vars:wrangler 会推成字面量"workers-ai",跟Env里宽联合类型打架。只用.dev.vars/--var做运行时注入。CF_AIG_TOKEN是真敏感凭证:生产环境务必wrangler secret put CF_AIG_TOKEN而不是--var。
延伸阅读
- AI Platform 公告(中译) —— 为什么 env.AI 现在能调 70+ 模型
- Using AI Models ——
env.AI.run的完整签名,包括 multimodal input shape - AI Gateway 文档 —— BYOK、缓存、限流、可观测性怎么开
- workers-ai-provider on npm —— 适配器源码,看它怎么把 SDK 的
LanguageModel接到env.AI
下一章预告
模型可以换了,但有个更大的痛点没解:对话只有一条线。用户说“再换个角度试试“时,你只能把整段历史复制出来从头再来,旧的回答和新的回答互相覆盖。下一章我们用 Project Think 的 Session API 把消息历史改成树形 —— 任意一点都能 fork 一条新分支重试,旧分支保留;过长的历史可以 compact 成摘要;还能 FTS5 全文搜过往结论。这是 Think 区别于上一代 AIChatAgent 的核心创新。