はじめに
Cloudflare Workers AI には 無料枠 10,000 Neurons/日 がある。個人レベルなら十分すぎる量だが、唯一のクセは API キーではなく Worker binding 経由で呼び出す ところにある。
OpenAI / Anthropic / Google のような AI Gateway 経由で呼べるプロバイダーと混ぜて使う場合、抽象化レイヤーが必要になる。本記事は、11 プロバイダーを単一の callProvider() でまとめた実装をコード付きで共有する。
完成イメージ
const result = await callProvider(env, 'cloudflare', '@cf/meta/llama-3.3-70b-instruct-fp8-fast', {
system: 'You are a poet.',
user: 'Describe wind through cedar leaves.',
});
console.log(result.text);
呼び出し側は provider と model を文字列で渡すだけ。中で binding か HTTP かを切り替える。
ステップ 1: wrangler.toml に binding を追加
# wrangler.toml
name = "my-worker"
main = "worker/index.ts"
compatibility_date = "2025-10-01"
[ai]
binding = "AI"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxxx"
[ai] binding = "AI" で env.AI 経由で Workers AI が呼べるようになる。API キー不要、無料枠が自動で適用される。
ステップ 2: 型定義
// worker/types.ts
export interface Env {
AI: Ai; // Cloudflare 提供の型
DB: D1Database;
OPENAI_API_KEY: string;
ANTHROPIC_API_KEY: string;
GOOGLE_API_KEY: string;
AI_GATEWAY_URL: string; // https://gateway.ai.cloudflare.com/v1/<account>/<gateway>
}
export type Provider = 'openai' | 'anthropic' | 'google' | 'cloudflare';
export interface ChatRequest {
system: string;
user: string;
maxTokens?: number;
}
export interface ChatResponse {
text: string;
provider: Provider;
model: string;
}
ステップ 3: プロバイダー別ディスパッチャ
// worker/lib/providers.ts
export async function callProvider(
env: Env,
provider: Provider,
model: string,
req: ChatRequest,
): Promise<ChatResponse> {
switch (provider) {
case 'cloudflare': return callCloudflareAI(env, model, req);
case 'openai': return callOpenAI(env, model, req);
case 'anthropic': return callAnthropic(env, model, req);
case 'google': return callGoogle(env, model, req);
default: throw new Error(`unknown provider: ${provider}`);
}
}
Cloudflare Workers AI(binding 経由)
async function callCloudflareAI(
env: Env,
model: string,
req: ChatRequest,
): Promise<ChatResponse> {
const result = await env.AI.run(model as any, {
messages: [
{ role: 'system', content: req.system },
{ role: 'user', content: req.user },
],
max_tokens: req.maxTokens ?? 400,
});
// モデルにより response 形状が違う:
// - chat 系: { response: "..." }
// - 一部: string をそのまま返す
// - 一部: { result: { response: "..." } }
const text =
typeof result === 'string'
? result
: (result as any).response ?? (result as any).result?.response ?? '';
return { text, provider: 'cloudflare', model };
}
ポイント:
-
env.AI.run(model, payload)で完結。fetch不要。 - レスポンス形状はモデルにより微妙に違う。実モデルで確認してから flatten する。
OpenAI / Anthropic / Google(AI Gateway 経由)
async function callOpenAI(env: Env, model: string, req: ChatRequest): Promise<ChatResponse> {
const r = await fetch(`${env.AI_GATEWAY_URL}/openai/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: req.system },
{ role: 'user', content: req.user },
],
max_tokens: req.maxTokens ?? 400,
}),
});
const data = await r.json<any>();
return { text: data.choices?.[0]?.message?.content ?? '', provider: 'openai', model };
}
async function callAnthropic(env: Env, model: string, req: ChatRequest): Promise<ChatResponse> {
const r = await fetch(`${env.AI_GATEWAY_URL}/anthropic/v1/messages`, {
method: 'POST',
headers: {
'x-api-key': env.ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
system: req.system,
messages: [{ role: 'user', content: req.user }],
max_tokens: req.maxTokens ?? 400,
}),
});
const data = await r.json<any>();
return { text: data.content?.[0]?.text ?? '', provider: 'anthropic', model };
}
AI Gateway を通すメリット:
- 全プロバイダーのログ・コスト・レイテンシが 1 ダッシュボードで見られる
- キャッシュ・レート制限を Gateway 側で設定できる
- フォールバック設定も可能
ステップ 4: 無料枠とコストの整理
| Provider | 無料枠 | 認証 | ストリーミング |
|---|---|---|---|
| Cloudflare Workers AI | 10,000 Neurons/日 | binding(キー不要) | あり |
| Google Gemini Flash | 無料枠あり | API key | あり |
| OpenAI | なし | API key | あり |
| Anthropic | なし | API key | あり |
Workers AI は 1 call ≈ 200〜500 Neurons(モデルによる)。1 日 20〜50 calls までは完全無料で動く。
cron で 2 時間ごとに 1 リクエスト × 12 回/日 ≈ 240 calls/日 の運用でも、推定 60,000〜120,000 Neurons でちょっと枠を超えるくらい。サイズの小さいモデルを混ぜれば余裕で収まる。
ステップ 5: 実運用での落とし穴
落とし穴 1: モデル名がいきなり deprecated になる
私の運用では @cf/google/gemma-7b-it が突然 404 を返すようになった(Gemma 3 12B に置き換わっていた)。Workers AI のモデルカタログは静的ではないので、定期的にチェックが必要。
→ 対策: 週次 cron でプロバイダー API を叩いて pool と diff し、メールで通知。
落とし穴 2: env.AI.run の型が any 寄り
// 動くが型情報がほぼない
const r = await env.AI.run('@cf/meta/llama-3.3-70b-instruct-fp8-fast' as any, { /* ... */ });
→ 対策: provider ごとに型付き wrapper を書いて、呼び出し側からは型安全にする。
落とし穴 3: Reasoning model はストリーミングで content が空になる
gpt-oss-120b、o3、deepseek-reasoner 系は SSE ストリームで delta.content が空、reasoning_content のみが流れることがある。これを content || reasoning で fallback すると 思考プロセスが意図せずユーザーに見える。
→ 対策: reasoning model は専用の non-streaming パスでハンドルし、最終出力 message.content だけを返す。
const REASONING_MODELS = new Set([
'gpt-oss-120b',
'o3',
'o4-mini',
'deepseek-reasoner',
]);
if (REASONING_MODELS.has(model)) {
return callProviderNonStream(env, provider, model, req);
}
落とし穴 4: max_tokens の意味がプロバイダーで違う
- OpenAI: completion トークン数の上限
- Anthropic: 出力トークン数の上限(必須パラメータ)
- Workers AI: モデル依存。一部モデルでは無視される
→ 対策: provider ごとに「適切なデフォルト値」を抽象化レイヤーで決め打ちにする。私の運用ではデフォルト 400、ストリーミングのみ 800 に上げている。
まとめ
- Cloudflare Workers AI は 無料枠 10,000 Neurons/日 で個人運用には十分
- API キーではなく binding(
env.AI) で呼び出す - 複数プロバイダーを混ぜるなら AI Gateway で一元化(ログ・レート制限・キャッシュ)
- レスポンス形状はプロバイダーごとに違うので flatten レイヤー を 1 枚噛ませる
- Reasoning model は non-streaming のみ で扱う
実プロダクトでは、上記抽象化に モデル健全性追跡 と AI Gateway 経由の自動リトライ を組み合わせて、11 社のモデルを混ぜて運用している。