Cloudflare ハンズオン教科書|2026-08 版
第 13 章

AI Gateway と Agents — AI アプリの観測とエージェント

AI 呼び出しに可観測性を。そしてステートフルなエージェントへ

この章で学ぶこと

AI Gateway とは — AI 呼び出しの中継点

第 11 章では env.AI.run() で Workers AI を直接呼びました。アプリを本番運用に近づけると、すぐに次の疑問が出てきます。誰がどれだけ AI を呼んでいるのか。トークンコストは月いくらか。同じプロンプトに毎回課金するのは無駄ではないか。プロバイダ障害時に別モデルへ切り替えられるか。AI Gateway は、アプリと AI プロバイダの間に挟まるプロキシ層としてこれらをまとめて解決するサービスです。導入は接続先を 1 行変えるだけで、全プランで利用できます。

AI Gateway はアプリとプロバイダの間に入り、応答を返しながら記録する アプリ / Worker AI Gateway AI プロバイダ リクエスト MISS のときだけ転送 応答(HIT ならここで完結) モデルの応答 すべての呼び出しを記録 ログ / 分析 トークン数・コスト・所要時間
図 13-1 破線が「キャッシュ HIT のときは通らない経路」です。ゲートウェイを 1 段挟むと、プロバイダに届かない応答(=課金されない応答)が生まれ、届いた分も届かなかった分も同じログに残ります。
機能できること
Analyticsリクエスト数・トークン数・コスト・エラー数・キャッシュ率をダッシュボードや GraphQL で集計
Logging個々のリクエストのプロンプト・レスポンス・トークン数・コスト・所要時間・ステータスを保存して閲覧
Caching同一リクエストへの応答を Cloudflare のキャッシュから返し、レイテンシとプロバイダ課金を削減
Rate limitingゲートウェイ単位でリクエスト数を制限(固定 / スライディングウィンドウ、超過時は 429)
リトライ・フォールバックエラーやタイムアウト時に別モデル・別プロバイダへ切り替え(現在は Dynamic Routing で構成)
マルチプロバイダWorkers AI・OpenAI・Anthropic・Google AI Studio・AWS Bedrock など多数のプロバイダに対応

コア機能(ダッシュボード分析・キャッシュ・レート制限)は無料です。ログの保存件数はプランで異なり、2026-08 時点で Workers Free はアカウント全体で 10 万件、Workers Paid はゲートウェイあたり 1,000 万件です。上限到達時は古いログの自動削除を有効にするか、保存を停止するかを選べます。コスト表示はトークン数からの推定値であり、正確な請求額はプロバイダ側のダッシュボードで確認します。

ゲートウェイの作成 — default は自動で作られる

ゲートウェイは明示的に作らなくても使い始められます。ゲートウェイ ID を省略するか default を指定して認証済みリクエストを送ると、アカウントに default という名前のゲートウェイが自動作成されます。自動作成された default ゲートウェイの初期設定は次のとおりです。

設定初期値
Authentication(認証)On
Log collection(ログ収集)On
Caching(キャッシュ)Off(TTL 0)
Rate limitingOff
Workers AI billingStandard billing

自動作成されるのは ID が default の場合だけで、それ以外の名前を使うには、ダッシュボードの AI → AI Gateway → Create Gateway(名前は 64 文字まで)か、AI Gateway - Read / Edit 権限の API トークンで REST API から事前に作成します。用途ごと(本番 / 検証、アプリ別など)にゲートウェイを分けると、分析やレート制限を独立して管理できます。

プロバイダ接続 — 3 つの入口と Universal Endpoint

アプリから AI Gateway を通す方法は 3 つあります。

入口エンドポイント / 書き方向いている場面
REST APIapi.cloudflare.com の OpenAI 互換エンドポイントプロバイダの API キー管理を Cloudflare に寄せたいとき
プロバイダ別エンドポイントgateway.ai.cloudflare.com/v1/.../{provider}既存の OpenAI / Anthropic SDK のコードを最小変更で通したいとき
Workers バインディングenv.AI.run() の第 3 引数に gateway を渡すWorker 内から Workers AI やサードパーティモデルを呼ぶとき
入口は 3 つ、通り道は 1 つ — 違いはアプリ側で何を書き換えるかだけ REST API api.cloudflare.com プロバイダ別エンドポイント gateway.ai.cloudflare.com Workers バインディング env.AI.run() の第 3 引数 API トークンで直接 base URL を差し替え gateway オプション AI Gateway ログ / 分析 キャッシュ / 制限 @cf/ 接頭辞のモデル それ以外のプロバイダ Workers AI @cf/ モデル サードパーティ OpenAI・Anthropic
図 13-2 3 つの入口で違うのは「アプリ側で書き換える 1 か所」と、プロバイダ名を書く場所(モデル名の接頭辞 / URL のパス / バインディングの引数)だけです。通り道はどれも同じゲートウェイなので、Workers AI とサードパーティの呼び出しが 1 つのログに並びます。

REST API は、Cloudflare の API トークンだけで Workers AI もサードパーティモデルも同じ形式で呼べる入口です。モデル名は openai/gpt-4.1-mini のような {プロバイダ}/{モデル} 形式、Workers AI は @cf/ 接頭辞で指定します。Workers AI モデルを呼ぶ場合のみ、どのゲートウェイを通すかを示す cf-aig-gateway-id ヘッダが必須です(サードパーティモデルは省略すると default が使われます)。

curl -X POST "https://api.cloudflare.com/client/v4/accounts/<YOUR_ACCOUNT_ID>/ai/v1/chat/completions" \
  --header "Authorization: Bearer <YOUR_API_TOKEN>" \
  --header "cf-aig-gateway-id: default" \
  --header "Content-Type: application/json" \
  --data '{
    "model": "@cf/meta/llama-3.1-8b-instruct",
    "messages": [
      { "role": "user", "content": "What is Cloudflare?" }
    ]
  }'

<YOUR_ACCOUNT_ID> は npx wrangler whoami で表示されるアカウント ID に、<YOUR_API_TOKEN> は npx wrangler auth token で取得できるトークン(または AI Gateway - Read / Edit と Workers AI - Read 権限を付けて作成した API トークン)に置き換えます。

プロバイダ別エンドポイントは、各プロバイダの API スキーマをそのまま維持しつつ AI Gateway の機能を追加する入口です。SDK の base URL を差し替えるだけで移行できます。

プロバイダ差し替え後の base URL
OpenAIhttps://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/openai
Anthropichttps://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic
Workers AI@cf/ モデルを REST API またはバインディング経由で(上記・後述)
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: "<YOUR_OPENAI_API_KEY>",
  // 認証付きゲートウェイでは cf-aig-authorization ヘッダも付ける
  defaultHeaders: {
    "cf-aig-authorization": "Bearer <YOUR_CF_API_TOKEN>",
  },
  baseURL:
    "https://gateway.ai.cloudflare.com/v1/<YOUR_ACCOUNT_ID>/<YOUR_GATEWAY_ID>/openai",
});

const response = await client.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello, world!" }],
});

プロバイダへの認証は 3 通りから選べます。リクエストヘッダで自分の API キーを渡す方法のほか、BYOK(プロバイダのキーを Cloudflare に保管し、実行時に AI Gateway が自動付与)と、Unified Billing(プリペイドの AI Gateway クレジットで支払い、プロバイダのキー自体が不要になる方式。クレジット購入時に 5% の手数料、推論単価はプロバイダと同額。2026-08 時点)があります。

Universal Endpoint とフォールバック

かつての AI Gateway では、https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id} という単一 URL に「プロバイダ名 + エンドポイント + ペイロード」の配列を POST し、先頭から順に試して失敗したら次に切り替える Universal Endpoint がフォールバックの手段でした。どのステップが応答したかはレスポンスヘッダ cf-aig-step(0 = 1 番目のモデル、1 = 2 番目にフォールバック…)で確認できます。

Universal Endpoint は非推奨(2026-08 時点)。既存の統合は動き続けますが、新規開発では単一モデルの呼び出しに REST API(OpenAI 互換)を、フォールバック・リトライ・条件付きルーティングには Dynamic Routing(ダッシュボードのビジュアルエディタまたは JSON でルーティングフローを定義し、dynamic/{route} というモデル名で呼び出す機能)を使います。

Universal Endpoint(非推奨) アプリ 候補の配列を POST AI Gateway 本文の順に試す 1: モデル A → 失敗 2: モデル B → 応答 モデルの差し替え → アプリを再デプロイ 応答元のヘッダ: cf-aig-step Dynamic Routing(推奨) アプリ model: dynamic/{route} AI Gateway 保存済みのルート定義 1: モデル A 2: モデル B(失敗時) モデルの差し替え → ルート定義を更新 応答元のヘッダ: cf-aig-model / cf-aig-provider
図 13-3 箱の並びは左右で同じで、違うのは候補の順序が置かれる場所です。左は順序がリクエスト本文にあるため、モデルを 1 つ差し替えるだけでもアプリの再デプロイが要ります。右は順序がゲートウェイ側にあり、アプリが送るのは dynamic/{route} という名前だけなので、コードは触らずに済みます。応答元を知るヘッダも別物で、cf-aig-step は Universal Endpoint 専用です。

Agents SDK — ステートフルなエージェントの実行基盤

章の後半は視点を変えて、AI アプリの「実行基盤」を扱います。チャットボットやタスク実行エージェントは、リクエストごとに消える Worker と違い会話や作業の状態を持ち続ける必要があります。Agents SDK(npm の agents パッケージ)は、第 8 章で学んだ Durable Objects の上に構築されたフレームワークで、エージェント 1 インスタンスに永続的なアイデンティティ・組み込み SQLite・WebSocket 接続・スケジュール実行を与えます。

エージェントは Agent クラスを継承して定義し、routeAgentRequest() でルーティングします。同じ名前でアクセスすれば常に同じインスタンスに到達するため(ユーザー ID やチケット番号をインスタンス名にする)、セッションストアを別に用意する必要がありません。主なライフサイクルメソッドは次のとおりです。

メソッド呼ばれるタイミング
onStart()インスタンスの起動時・ハイバネーションからの復帰時
onRequest(request)HTTP リクエストごと
onConnect(connection, ctx)WebSocket 接続の確立時
onMessage(connection, message)WebSocket メッセージの受信ごと
onStateChanged(state, source)状態の変更時(サーバー起点・クライアント起点の両方)

状態管理は initialState プロパティと this.setState() で行います。状態は SQLite に自動永続化されて再起動やハイバネーションを生き延び、変更は接続中の全 WebSocket クライアントへ即座にブロードキャストされます。表形式のデータは this.sql タグ付きテンプレートで SQLite を直接クエリすることもできます。

setState() の 1 回の呼び出しが、2 方向へ同時に伝わる ブラウザ タブ 1 ブラウザ タブ 2 WebSocket で常時接続 CounterAgent = Durable Object 1 個 this.setState() 接続 0 で休眠 / 状態は残る 内蔵 SQLite state を自動保存 stub.increment() onStateUpdate 接続中の全タブへ 自動で永続化 再起動後に読み戻す
図 13-4 枠で囲った this.setState() から出る濃い 3 本が、1 回の呼び出しで同時に走る経路です。永続化とブロードキャストが分かれていないため、セッションストアも pub/sub も別に用意せずに、ボタンを押していない側のタブまで表示が揃います。細い矢印はクライアント発の RPC、破線は起動時の読み戻しで、どちらも setState() とは別のタイミングで動きます。

スケジュール実行は 4 つのモードがあり、Durable Object のアラームを使って指定時刻にエージェントを起こします。

// 30 秒後に 1 回実行
await this.schedule(30, "sendReminder", { message: "Check your email" });

// 指定時刻に 1 回実行
await this.schedule(new Date("2026-09-01T09:00:00Z"), "sendReminder", { message: "報告締切" });

// cron 式で毎日 8:00 に実行
await this.schedule("0 8 * * *", "dailyDigest", { userId: "user-123" });

// 30 秒間隔で繰り返し実行(秒単位の粒度・重複実行防止つき)
await this.scheduleEvery(30, "poll", { source: "api" });

第 2 引数はコールバックとして呼ぶメソッド名、第 3 引数はそのメソッドに渡されるペイロードです。cron と scheduleEvery() は同一引数での重複登録を防ぐ冪等設計なので、onStart() 内で安全にセットアップできます。

クライアント接続は React なら agents/react の useAgent フック、素の JavaScript なら agents/client の AgentClient を使います。どちらも WebSocket で接続し、onStateUpdate コールバックで状態変更を受け取り、サーバー側で @callable() デコレータを付けたメソッドを agent.stub.メソッド名() として型安全に呼び出せます。エージェント内から AI モデルを呼ぶときは第 11 章と同じ this.env.AI.run() が使え、gateway オプションを渡せばその呼び出しも AI Gateway で観測できます。制限値は 2026-08 時点で、インスタンスあたり状態 1 GB、CPU 時間 30 秒(HTTP リクエスト・WebSocket メッセージ・スケジュール実行のたびにリフレッシュ)です。

ハンズオン 13-1|Workers AI を AI Gateway 経由に切り替えてログを観察する

Workers AI を呼ぶ最小の Worker を作り、gateway オプション 1 つで AI Gateway 経由に切り替えて、ダッシュボードでログとコストを観察します。所要 15 分。

Workers AI の推論はローカル開発(wrangler dev)でも実際に Cloudflare アカウント上のモデルを呼び出し、使用量に応じた課金が発生します。

  1. プロジェクトを作成します(Hello World example → Worker only → TypeScript を選択。デプロイは No)。
    npm create cloudflare@latest -- hello-aig
    cd hello-aig
  2. wrangler.jsonc に AI バインディングを追加します。
    {
      "name": "hello-aig",
      "main": "src/index.ts",
      "compatibility_date": "2026-08-10",
      "ai": {
        "binding": "AI"
      }
    }
  3. src/index.ts を次の内容に置き換えます。gateway.id に "default" を指定するだけで、この呼び出しが AI Gateway を通るようになります(default ゲートウェイは初回リクエスト時に自動作成されます)。
    export interface Env {
      AI: Ai;
    }
    
    export default {
      async fetch(request, env): Promise<Response> {
        const response = await env.AI.run(
          "@cf/meta/llama-3.1-8b-instruct-fast",
          {
            prompt: "What is the origin of the phrase Hello, World",
          },
          {
            gateway: {
              id: "default", // これだけで AI Gateway 経由になる
            },
          },
        );
    
        return new Response(JSON.stringify(response), {
          headers: { "Content-Type": "application/json" },
        });
      },
    } satisfies ExportedHandler<Env>;
  4. npx wrangler dev で起動し、表示された http://localhost:8787 をブラウザで数回開きます。{"response":"A fascinating question!..."} のような JSON が返れば成功です。
  5. ダッシュボードで AI → AI Gateway → default を開き、Logs タブを確認します。リクエストごとに、送ったプロンプト・モデルの応答・プロバイダ・トークン数(In / Out)・推定コスト・所要時間が記録されています。

Analytics タブではリクエスト数・トークン消費・エラー率の推移がグラフで見られます。コードの変更は実質 1 行(gateway オプションの追加)だけで、AI 呼び出しの可観測性が手に入りました。

ハンズオン 13-2|キャッシュの HIT / MISS を観察する

REST API 経由で同じリクエストを 2 回送り、キャッシュヘッダの効果をレスポンスヘッダで確認します。所要 10 分。

  1. アカウント ID とトークンを環境変数にセットします。
    export ACCOUNT_ID=$(npx wrangler whoami | grep -oE '[0-9a-f]{32}')
    export CF_TOKEN=$(npx wrangler auth token)

    取得できない場合はダッシュボードでアカウント ID を確認し、AI Gateway - Read / Edit と Workers AI - Read 権限の API トークンを作成してください。

  2. cf-aig-cache-ttl ヘッダ(60 秒以上、最長 1 か月)を付けて同じリクエストを 2 回送り、cf-aig-cache-status ヘッダを確認します。
    for i in 1 2; do
      curl -si -X POST "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai/v1/chat/completions" \
        --header "Authorization: Bearer $CF_TOKEN" \
        --header "cf-aig-gateway-id: default" \
        --header "cf-aig-cache-ttl: 3600" \
        --header "Content-Type: application/json" \
        --data '{
          "model": "@cf/meta/llama-3.1-8b-instruct",
          "messages": [{ "role": "user", "content": "say hello in one word" }]
        }' | grep -i "cf-aig-cache-status"
    done
  3. 期待される出力は 1 回目が cf-aig-cache-status: MISS、2 回目が cf-aig-cache-status: HIT です。2 回目はプロバイダに到達していないため応答も高速で、課金も発生しません。
  4. ダッシュボードの Logs でも 2 件目に Cached のマークが付いていることを確認します。逆にキャッシュを確実に避けたいリクエストには cf-aig-skip-cache: true ヘッダを付けます。
ハンズオン 13-3|状態が同期するミニエージェントを作る

Agents SDK のスターターからカウンターエージェントを作り、永続状態が複数のブラウザタブへリアルタイム同期される様子を確認します。所要 20 分。

  1. スターターテンプレートからプロジェクトを作成し、起動します。
    npm create cloudflare@latest -- --template cloudflare/agents-starter
    cd agents-starter
    npm install
    npm run dev
  2. src/server.ts を次の内容に置き換えます。@callable() を付けたメソッドがクライアントから呼べる API になります。
    import { Agent, routeAgentRequest, callable } from "agents";
    
    export type CounterState = {
      count: number;
    };
    
    export class CounterAgent extends Agent<Env, CounterState> {
      initialState: CounterState = { count: 0 };
    
      @callable()
      increment() {
        this.setState({ count: this.state.count + 1 });
        return this.state.count;
      }
    
      @callable()
      reset() {
        this.setState({ count: 0 });
      }
    }
    
    export default {
      async fetch(request: Request, env: Env, ctx: ExecutionContext) {
        return (
          (await routeAgentRequest(request, env)) ??
          new Response("Not found", { status: 404 })
        );
      },
    } satisfies ExportedHandler<Env>;
  3. wrangler.jsonc にエージェントを登録します。class_name はエクスポートしたクラス名と完全一致させ、new_sqlite_classes で状態永続化用の SQLite を有効にします。
    {
      "name": "my-agent",
      "main": "src/server.ts",
      "compatibility_date": "2026-08-10",
      "compatibility_flags": ["nodejs_compat"],
      "durable_objects": {
        "bindings": [
          { "name": "CounterAgent", "class_name": "CounterAgent" }
        ]
      },
      "migrations": [
        { "tag": "v1", "new_sqlite_classes": ["CounterAgent"] }
      ]
    }
  4. src/client.tsx を useAgent を使う実装に置き換えます。
    import "./styles.css";
    import { createRoot } from "react-dom/client";
    import { useState } from "react";
    import { useAgent } from "agents/react";
    import type { CounterAgent, CounterState } from "./server";
    
    export default function App() {
      const [count, setCount] = useState(0);
    
      const agent = useAgent<CounterAgent, CounterState>({
        agent: "CounterAgent",
        onStateUpdate: (state) => setCount(state.count),
      });
    
      return (
        <div>
          <h1>Counter Agent</h1>
          <p>{count}</p>
          <button onClick={() => agent.stub.increment()}>+</button>
          <button onClick={() => agent.stub.reset()}>Reset</button>
        </div>
      );
    }
    
    const root = createRoot(document.getElementById("root")!);
    root.render(<App />);
  5. http://localhost:5173 をブラウザタブ 2 つで開き、片方で「+」を押します。もう片方のタブのカウントも即座に増えれば成功です。裏では、クライアントの stub.increment() が WebSocket RPC でエージェントに届き、setState() が SQLite へ永続化と全クライアントへのブロードキャストを同時に行っています。開発サーバーを再起動してもカウントが残っていることも確認しましょう。
  6. 仕上げに npm run deploy でデプロイすると、エージェントは Cloudflare のグローバルネットワーク上で動き、クライアントが誰もいない間はハイバネーションしてコストがかかりません。

つまずきポイント

Workers AI モデルの REST API 呼び出しで cf-aig-gateway-id を忘れる。サードパーティモデルはヘッダ省略時に default ゲートウェイへ自動ルーティングされますが、@cf/ 接頭辞の Workers AI モデルではこのヘッダが常に必須です。また自動作成されるのは ID が default のときだけで、他の名前のゲートウェイは事前作成が必要です。

gateway.ai.cloudflare.com のプロバイダ別エンドポイントで認証エラーになる。自動作成された default ゲートウェイは Authentication が On のため、プロバイダの API キーに加えて cf-aig-authorization: Bearer <CF_API_TOKEN> ヘッダが必要です。付け忘れるとプロバイダ以前にゲートウェイで拒否されます。

キャッシュが効かない・HIT にならない。キャッシュキーはプロバイダ・エンドポイント・モデル・認証ヘッダ・リクエストボディ全体のハッシュで、完全一致したリクエストだけが HIT します。メッセージが 1 文字でも違えば別エントリです。対応するのはテキストと画像の応答のみで、キャッシュは揮発的(同時に同じリクエストが来ると 2 件目が MISS になることがある)です。cf-aig-cache-ttl の最小値は 60 秒です。

Agents で「No such Durable Object class」や @callable() の構文エラーが出る。前者は wrangler.jsonc の migrations に new_sqlite_classes を書き忘れたのが原因です。後者(SyntaxError: Invalid or unexpected token)は 2 点で解消します — vite.config.ts のプラグインに agents/vite を追加し(Vite 8 の変換系は TC39 デコレータ未対応のため)、tsconfig は agents/tsconfig を extends する(target: ES2021 を含む推奨設定。スターターは両方設定済み)。なお experimentalDecorators: true は設定してはいけません — Agents SDK は TC39 標準デコレータを使うため、互換性のない変換が適用されて @callable() が実行時に静かに壊れます。

理解度チェック

Q1. REST API 経由で Workers AI モデルを AI Gateway に通すとき、必須のヘッダは?

cf-aig-gateway-id です。@cf/ 接頭辞の Workers AI モデルでは常に必要です。サードパーティモデルの場合は省略でき、そのときは default ゲートウェイが使われます(存在しなければ初回の認証済みリクエストで自動作成)。

Q2. レスポンスがキャッシュから返されたかどうかは、どうやって確認する?

レスポンスヘッダ cf-aig-cache-status を見ます。HIT ならキャッシュから、MISS ならプロバイダから返された応答です。ダッシュボードのログでも Cached のフィルタで確認できます。

Q3. Universal Endpoint は現在どういう位置づけ?新規開発では何を使う?

非推奨です(既存統合は継続動作)。新規では、単一モデルの呼び出しには OpenAI 互換の REST API を、フォールバック・リトライ・条件付きルーティングには Dynamic Routing を使います。

Q4. Agent クラスで this.setState() を呼ぶと何が起きる?

状態がインスタンス内蔵の SQLite に永続化され(再起動・ハイバネーションを生き延びる)、同時に接続中の全 WebSocket クライアントへブロードキャストされます。クライアント側では useAgent の onStateUpdate コールバックが発火します。

まとめと次章へ

AI 編はここまでです。次章からは守りと運用編に入り、Turnstile・WAF・レート制限で bot と攻撃からアプリを守る方法を学びます。