Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 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 改成 openaigoogleworkers-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

env.AI 一个 binding,背后 70+ 模型 12+ 厂商,自动失败转移与账单聚合 env.AI = 统一推理面 CoderAgent (Think) getModel() 返回 workers-ai-provider("anthropic/...") env.AI.run(prefixedId, ...) AI Platform + AI Gateway 缓存 / 限流 / 计费 / failover 下游 provider workers-ai @cf/llama-3.3 anthropic claude-opus-4-6 openai gpt-5 google gemini-2.5-pro …还有 8+ bytedance / alibaba / … 业务代码只看到一个 binding;BYOK 凭证、缓存、限流都在 dashboard 配

方案选择

方案适合什么我们用吗
每个 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-fastgoogle/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.tswrangler.jsonc。不装新包,不改入口。

1. 真相先讲清:三条可行路径的取舍

理论上有三条路把多 provider 接进来:

路径npm 包端点响应归一?评价
A. workers-ai-provider + env.AI bindingworkers-ai-providerenv.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-compatiblehttps://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.13processText() 只识别 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。

createAiGatewaygetModel() 里还能加这些 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")]);

七个真踩过的细节:

  1. AI_PROVIDER.dev.vars / 部署时 --var,不要写进 wrangler.jsoncvars。一旦写进去,wrangler 会把它推成 "workers-ai" 字面量,跟我们 Env 里的宽联合类型直接打架,tsc 报 Type ... is not assignable to type "workers-ai"
  2. AI Gateway auth token 是新东西,跟 wrangler OAuth token 不是一码事。dashboard → AI Gateway → 你的 gateway → Settings → Authentication → Create Token,形如 cfut_xxx
  3. /compat 上的 workers-ai model id 是双前缀:workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast。少了 workers-ai/ 前缀返 Invalid provider,留下来的 @cf/... 仍然要写全。
  4. Anthropic 的 model id 在 /compat 上用 - 分隔版本号(claude-haiku-4-5),跟 env.AI.run 直调时 Cloudflare catalog 的命名(claude-haiku-4.5)是两套。/compat 把 model id 透传给下游 Anthropic,所以用 Anthropic 自家命名。
  5. Google 的 provider 前缀是 google-ai-studio,不是 google。写 google/gemini-2.5-pro 会返 Invalid provider
  6. 第三方 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 自家模型走免费额度,不受影响。
  7. 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.jsoncvars 里写 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

两条路任选:

  1. Unified billing(充值):dashboard → AI Gateway → 你的 gateway → Add credit。Cloudflare 按 token 转售下游 provider,统一一份账单。
  2. 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.run vs /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_delta SSE 不认,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: true schema,Llama 在工具上表现弱。第 4 章引入工具时会讲选模型策略。
  • failover 是 provider 级别,不是 model 级别:Anthropic 整家挂了,Cloudflare 会自动转给 OpenAI(在 Gateway 配 fallback);单个 model ID 不可用不会触发自动转移。
  • AI_PROVIDER 别写进 wrangler.jsoncvars:wrangler 会推成字面量 "workers-ai",跟 Env 里宽联合类型打架。只用 .dev.vars / --var 做运行时注入。
  • CF_AIG_TOKEN 是真敏感凭证:生产环境务必 wrangler secret put CF_AIG_TOKEN 而不是 --var

延伸阅读

下一章预告

模型可以换了,但有个更大的痛点没解:对话只有一条线。用户说“再换个角度试试“时,你只能把整段历史复制出来从头再来,旧的回答和新的回答互相覆盖。下一章我们用 Project Think 的 Session API 把消息历史改成树形 —— 任意一点都能 fork 一条新分支重试,旧分支保留;过长的历史可以 compact 成摘要;还能 FTS5 全文搜过往结论。这是 Think 区别于上一代 AIChatAgent 的核心创新。