Cloudflare PagesとWorkersとAI GatewayでAIチャットBotの土台を作る
Cloudflare Pagesの静的UI、Pages Function、Workers AI binding、AI Gatewayを組み合わせ、ブラウザにAPIキーを出さないAIチャットBotの最小構成を検証する。
Cloudflare AI Gatewayを触っていると、最初は「LLM APIのURLを差し替えればログが見える」という理解から入ります。これは導入としては正しいのですが、AIチャットBotとして公開する段階では、もう少し設計する場所が増えます。
ブラウザにモデルAPIキーを置かないこと。フロントエンドとLLM呼び出しの境界を分けること。AI Gatewayのログ、cache、metadataを、あとから運用で見られる形にしておくこと。小さいBotでも、このあたりを曖昧にすると、あとで「動いているが何が起きているか分からない」状態になりやすいです。
今回は、Cloudflare Pages、Pages Function、Workers AI binding、AI Gatewayを組み合わせて、AIチャットBotの土台を作ります。実験では、まずローカルのmock modeでAPI境界とGateway optionの組み立てを確認し、その後にCHAT_MODE=liveへ切り替えてWorkers AIの実モデル呼び出しとAI Gatewayログ取得まで確認しました。
今回の構成
構成はかなり小さくしています。
Browser
↓
Cloudflare Pages static UI
↓ /api/chat
Pages Function on Workers runtime
↓ env.AI.run(..., { gateway })
Cloudflare AI Gateway
↓
Workers AI model
Pagesは静的UIを配信し、/api/chatだけをPages Functionで受けます。AI Gatewayは、LLM呼び出しの途中に入る観測と制御のレイヤーです。Cloudflareの公式ドキュメントでは、WorkersのAI bindingからenv.AI.run()を呼び、その第3引数でgatewayを指定できます。ここにGateway ID、cache制御、log収集、metadataを渡せます。
今回のサンプルではWorkers AIの@cf/zai-org/glm-5.2を指定しています。Workers AIはローカル開発中でもCloudflareアカウント上の推論を使うため、最初から実モデルへ投げるのではなく、mock modeでUI/API契約を確認してからlive modeへ進めました。
まずUIとAPI境界を作る
Pages側の画面は、単純なチャットUIにしました。ブラウザは/api/chatへmessageとskipCacheだけを送ります。モデル名、Gateway ID、metadata、AI bindingの存在はブラウザ側へ持たせません。

Pages Function側は薄くして、処理本体を共通ハンドラへ寄せます。
import { handleChat, handleOptions } from "../../src/chat.mjs";
export const onRequestPost = (context) => handleChat(context);
export const onRequestOptions = () => handleOptions();
export const onRequestGet = () =>
Response.json({
ok: true,
route: "/api/chat",
methods: ["POST", "OPTIONS"],
});
この分け方にしておくと、Pages Functionとして動かすときも、ローカルのmock serverで同じhandleChat()を呼ぶときも、入力検証とレスポンス形式を揃えられます。
AI Gateway optionをアプリ側で組み立てる
CloudflareのAI bindingでは、env.AI.run()にモデル入力を渡すだけでなく、Gateway optionを一緒に渡せます。今回のサンプルでは、Gateway ID、cacheをスキップするか、ログを収集するか、アプリ識別用のmetadataを付けています。
const raw = await env.AI.run(
model,
{
messages,
},
{
gateway: {
id: env.CLOUDFLARE_AI_GATEWAY_ID ?? "default",
skipCache: Boolean(body.skipCache),
collectLog: env.COLLECT_AI_LOG !== "false",
metadata: {
app: "pages-workers-ai-chatbot",
env: env.ENVIRONMENT ?? "local",
route: "api-chat",
},
},
},
);
逆に、ユーザーIDや問い合わせIDのような値を入れる場合は注意が必要です。Gatewayのログは運用に便利ですが、今回の実測で確認できたのは、provider、model、status、duration、cost、token、metadataといったメタデータ寄りの情報です。promptやresponse本文をどこまで保存し、誰が見られる状態にするかは、Gateway側のログ設定、payload保存方針、アプリのデータ分類に合わせて別途決めるべきです。
ローカルではmock modeで確認する
今回の検証では、いきなり実モデルへ接続しませんでした。理由は単純で、最初に確認したいのはLLMの回答品質ではなく、AIチャットBotとしての境界だからです。
検証用に用意したコマンドは次の3つです。
npm run check
npm run dev
npm run verify
npm run checkは、サンプル内のJavaScriptをnode --checkで構文確認するためのコマンドです。npm run devは、Pagesの静的UIと/api/chatをローカルで動かすmock serverです。npm run verifyは記事用に作った検証スクリプトで、起動中のローカルプレビューへ/api/healthと/api/chatを送り、次の点を確認します。
- health endpointが
ok=trueを返すこと - local previewでは
source=mockになること - Gateway IDが
defaultとして組み立てられること - metadataに
app、env、routeが入ること - 空メッセージがHTTP 400になること

この段階で確認できたのは、UI、API、入力検証、Gateway optionの組み立てまでです。ここで止めると「実験した」とは言えないので、次にWorkers AIの実モデル呼び出しとAI Gatewayログ取得を確認します。
live modeではWorkers AI bindingを使う
Cloudflare上またはWranglerのlive previewで実モデルを呼ぶときは、CHAT_MODE=liveにします。Wranglerで確認する場合は、AI bindingを付けてPagesを起動します。
wrangler login
npx wrangler pages dev public \
--port 8790 \
--compatibility-date=2026-07-03 \
--ai AI \
--binding CHAT_MODE=live \
--binding CLOUDFLARE_AI_GATEWAY_ID=default \
--binding AI_MODEL=@cf/zai-org/glm-5.2 \
--binding ENVIRONMENT=local-live \
--binding COLLECT_AI_LOG=true \
--binding ENABLE_LOG_INSPECTION=true
サンプルのwrangler.jsoncでは、AI bindingをAIとして設定しています。
{
"name": "cloudflare-pages-workers-ai-gateway-chatbot",
"compatibility_date": "2026-07-03",
"pages_build_output_dir": "public",
"ai": {
"binding": "AI"
},
"vars": {
"CHAT_MODE": "live",
"CLOUDFLARE_AI_GATEWAY_ID": "default",
"AI_MODEL": "@cf/zai-org/glm-5.2",
"ENVIRONMENT": "preview",
"COLLECT_AI_LOG": "true",
"ENABLE_LOG_INSPECTION": "false"
}
}
実行時に一度、compatibility_dateを2026-07-04にしたところ、Cloudflare側ではまだ未来日として拒否されました。この記事を書いているローカル環境の日付は2026年7月4日ですが、Cloudflareの受理基準ではその時点で2026-07-04が未対応だったため、2026-07-03に変更して検証を進めました。
ここでの注意点は、Workers AI binding経由でWorkers AIを呼ぶ構成では、ブラウザに外部プロバイダのAPIキーを渡しません。サンプルはCloudflare内のWorkers AIを使う前提なので、OpenAIやGeminiのAPIキーをWorker secretへ置く構成とは少し違います。
OpenAIやAnthropicなどの外部プロバイダを自分のキーで呼ぶ場合は、AI Gatewayのprovider-native endpointやREST API側の設計を見直す必要があります。公式ドキュメント上、AI bindingから第三者モデルを呼ぶ場合はCloudflare側のUnified Billingを使う説明になっており、BYOKをそのままAI bindingへ渡す設計ではありません。
このあたり、古い「OpenAI互換URLを差し替えるだけ」という理解のままだと少し詰まります。正直、ここは最初に混乱しやすい。
実モデル呼び出しとAI Gatewayログを確認する
live modeで/api/chatへ1回リクエストを送りました。レスポンスでは、sourceがworkers-aiになり、モデルは@cf/zai-org/glm-5.2、Gateway optionにはid: "default"、skipCache: true、collectLog: true、metadataとしてapp、env、routeが入りました。
さらに、レスポンスに含まれたaiGatewayLogIdを使い、検証用に追加した/api/ai-gateway-logからAI Gatewayのログ要約を取得しました。これはCloudflareダッシュボードのAI Gatewayログに出る同じログエントリを、AI bindingのgetLog()で確認するための検証です。

今回取得できたAI Gatewayログの要約は次のとおりです。
| 項目 | 結果 |
|---|---|
| provider | workers-ai |
| model | @cf/zai-org/glm-5.2 |
| status | 200 |
| cached | false |
| duration | 17182 ms |
| cost | 0.0028052000000000003 |
| tokens in | 74 |
| tokens out | 614 |
| metadata | app, env, route を確認 |
| prompt / response body | 今回のgetLog()要約では本文取得なし |
これで、単にWorkerからモデル応答が返っただけではなく、そのリクエストがAI Gatewayログとして残り、provider、model、status、duration、cost、token、metadataまで後から照合できることを確認できました。一方で、今回のartifactではprompt本文とresponse本文は取得できていません。本文保存まで運用で使う場合は、AI Gateway側のログ設定とpayload保存方針を分けて確認する必要があります。
cacheでコスト削減を言いすぎない
AI Gatewayのcacheは、AIチャットBotで魅力的に見えます。同じ質問なら上流モデルへ投げずに返せるので、レイテンシとコストの両方に効く可能性があります。
向いているのは、FAQ、固定メニュー、テンプレート化された質問、同じsystem promptと同じuser promptを繰り返す用途です。逆に、ユーザーごとに文脈が変わる相談Botでは、cacheよりもmetadata、ログ、rate limit、payload保存方針の方が先に効きます。
今回のUIにskip cacheを置いたのは、cacheを常に有効化するためではありません。Botのリクエスト単位で「これはcacheしてよい入力なのか」を考えるためです。
小さいBotでも運用の論点は先に出る
今回の検証で一番大きかった発見は、AIチャットBotの最小構成でも、実装論点と運用論点を完全には分けられないことです。
UIをPagesへ置き、/api/chatをFunctionへ分けるだけなら簡単です。env.AI.run()でモデルを呼ぶことも難しくありません。ただ、公開するBotとして考えると、すぐに次の問いが出ます。
- promptとresponseをGatewayログに残してよいか
- metadataにどの粒度の識別子を入れるか
- cacheを有効にしてよい質問と、毎回モデルへ投げる質問をどう分けるか
- local previewで実モデルを呼ぶか、mockでAPI契約だけを見るか
- Workers AIを使うのか、外部プロバイダをprovider-native endpointで呼ぶのか
この問いを後回しにすると、最初は速く作れます。しかし、ユーザーから「回答がおかしい」「遅い」「コストが高い」と言われたときに、どこを見ればよいか分からなくなります。
個人的には、Cloudflare Pages + Workers + AI Gatewayの組み合わせは、個人開発や小さなAIアプリの基盤としてかなり相性がよいです。静的UI、API境界、推論、観測の入口を同じCloudflare側に寄せられるからです。一方で、AI Gatewayを挟むだけでAgentOpsが完成するわけではありません。
AI Gatewayは、LLM呼び出しの出口を見えるようにする入口です。RAGでどの文書を取ったか、Agentがどのtoolを呼んだか、ユーザー評価がどうだったかまで扱うなら、Langfuseのようなtrace設計は別に必要になります。
まとめ
Cloudflare Pages、Pages Function、Workers AI binding、AI Gatewayを組み合わせると、ブラウザにモデルAPIキーを出さずにAIチャットBotの土台を作れます。今回の検証では、まず静的UIから/api/chatへ送り、同じハンドラで入力検証、mock応答、Gateway ID、metadataを確認しました。そのうえでlive modeに切り替え、Workers AIのGLM-5.2から実応答を受け取り、AI Gatewayログとしてprovider、model、status、duration、cost、tokens、metadataを取得できることを確認しました。
一方で、cache HIT/MISSの比較、payload保存方針、外部プロバイダのBYOK構成は別途確認が必要です。特にcacheは、自由入力のチャット全般で効くものではなく、同一リクエストや固定質問に寄った使い方で効果が出ます。
まずはmock modeでAPI境界を固め、次にWranglerのlive modeでWorkers AIとAI Gatewayログを確認する。この順番にすると、LLMの回答品質、Cloudflare binding、UIの不具合を一度に抱え込まずに済みます。