失敗したモデルリクエストを追跡する

CloudflareBeginner
オンラインで実践に進む

はじめに

AI リクエストが失敗したとき、呼び出し元から見えるのは最終的な HTTP レスポンスだけです。そのレスポンスから問題が発生したことは分かりますが、リクエストの形式が正しくなかったのか、ゲートウェイに拒否されたのか、アップストリームのモデルプロバイダーに拒否されたのかまでは、必ずしも分かりません。可観測性(Observability)とは、リクエストが呼び出し元を離れた後も追跡し、どの境界で処理されたのかを説明できるだけの証拠を収集することです。

AI Gateway は、ゲートウェイに到達したリクエストごとに 1 件のログエントリを記録します。ログには、プロバイダー、モデル、HTTP ステータス、処理時間、トークン使用量を表示できます。また、いくつかのカスタムメタデータを付加することもできます。これは、後からリクエストを見つけるための小さなラベルです。メタデータは秘密情報を保管するための場所ではありません。この実験では、ランダムなトレース ID、合成ケース名、Boolean フラグだけを使用します。認証情報、プロンプト、メールアドレス、アカウント ID は決して含めません。

この実験では、破棄可能な認証済みゲートウェイを 1 つ作成し、安全なトレースラベルを付けた、意図的に不正な Workers AI リクエストを送信します。失敗したログを見つけ、ゲートウェイ認証の失敗とアップストリーム認証の失敗を比較し、入力を修正して、同じトレースに成功したリクエストが記録されることを確認します。これにより、推測ではなく証拠に基づいてトラブルシューティングを行えるようになります。

このコースに直接アクセスした場合は、まず LabEx を Cloudflare アカウントに接続する を完了してください。この実験では、LabEx VM のターミナル、Wrangler のデバイス認証、学習用アカウントの確認、明示的なアカウント ID について学びます。また、この実験は 2 つの別々の認証ヘッダーを使用するため、先に ゲートウェイを経由したルート推論を完了してください。

この実験では、Cloudflare がホストする @cf/meta/llama-3.3-70b-instruct-fp8-fast モデルを、Workers AI の Standard 課金で使用します。Workers Paid、Unified Billing、外部プロバイダーのアカウントは必要ありません。リクエストは小規模な合成データです。共有の 1 日あたりの Workers AI 割り当てが利用できない場合は、繰り返し再試行せず停止してください。

セットアップでは、Node.js 22.22.0 とプロジェクトローカルの Wrangler 4.132.0 を /home/labex/project/ai-gateway-trace にインストールします。読み取り専用の独立したアセスメントを準備しますが、Wrangler の認証、クラウドリソースの作成、モデルへのトラフィック送信は行いません。実験終了時に LabEx が一時 VM を破棄します。ただし、VM の破棄だけではクラウドリソースを削除できないため、ログアウトする前にゲートウェイとトークンを削除してください。

VM を認証し、安全なトレース ID を作成する

このステップでは、新しい VM を学習用アカウントに接続し、破棄可能なゲートウェイ 1 つと合成トレース 1 つの名前を作成します。

トレース IDは、関連する観測情報に共通して付けるラベルです。ユーザーが何を入力したか、または誰であるかを漏らさずに、リクエストを識別できる値にします。この実験ではランダムな値を生成し、認証情報ではなくリソース名と一緒に保存します。

準備済みのプロジェクトに移動し、固定された CLI バージョンを確認して、この VM を認証します。

cd /home/labex/project/ai-gateway-trace
npx wrangler --version
npx wrangler login --device --browser=false --scopes account:read user:read ai:write

表示されたリンクを開き、コードを入力して、対象の学習用アカウントを認証します。構造化された ID 情報を確認します。

npx wrangler whoami --json

Wrangler 4.132.0 と loggedIn: true が表示されることを確認します。以下の YOUR_ACCOUNT_ID を、対象アカウントに表示された実際の 32 文字の ID に置き換えます。

GATEWAY_ID="labex-c09-g02-$(openssl rand -hex 6)"
TOKEN_NAME="$GATEWAY_ID-token"
TRACE_ID="trace-$(openssl rand -hex 8)"
cat > .labex/state.json <<JSON
{
  "accountId": "YOUR_ACCOUNT_ID",
  "gatewayId": "$GATEWAY_ID",
  "tokenName": "$TOKEN_NAME",
  "traceId": "$TRACE_ID"
}
JSON
cat .labex/state.json

トレース ID は安全な合成データです。アカウント ID とリソース名はローカルの state ファイルに保存されるため、後のクリーンアップではこの実験で作成したリソースだけを対象にできます。

可観測な認証済みゲートウェイを作成する

このステップでは、呼び出し元の認証境界を通過したリクエストを記録するゲートウェイを作成します。

Cloudflare Dashboard を開き、AI → AI Gateway → Create gateway → Custom gateway を選択します。ゲートウェイ名には、保存した gatewayId を使用します。リクエストログとゲートウェイ認証は有効なままにします。キャッシュ、レート制限、支出制限、リトライは無効なままにし、Workers AI の課金は Standard のままにします。

作成後、パンくずリストで一意のゲートウェイ ID を確認し、Settings を開きます。ログはこの実験で使用する証拠を作成します。認証を有効にすると、未知の呼び出し元がログを増やしたり、モデル使用量を消費したりできなくなります。

Create an AI Gateway authentication token を選択します。保存した tokenName を使用し、対象の学習用アカウントだけを含め、次の権限を正確に設定します。

  • AI Gateway — Run:認証済みゲートウェイにリクエストを入力するための権限。
  • AI Gateway — Edit:ログの読み取りと、この破棄可能なゲートウェイの削除に必要な権限。

Workers AI の権限は追加しないでください。Wrangler が、別の短期間有効なアップストリーム認証情報を提供します。アカウントと権限を確認してからトークンを作成し、一度しか表示されない値を画面に出力せず保存します。

bash -c '
while :; do
  read -rsp "Paste the AI Gateway token: " GATEWAY_TOKEN
  printf "\n"
  [ -n "$GATEWAY_TOKEN" ] && break
  printf "Token cannot be empty; paste it again.\n" >&2
done
umask 077
printf "%s" "$GATEWAY_TOKEN" > .labex/gateway-token
unset GATEWAY_TOKEN
chmod 600 .labex/gateway-token
'

認証済みの管理 API を使って、正しいリソースであることを確認します。

ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID" \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const b=JSON.parse(s),g=b.result||{};console.log(JSON.stringify({success:b.success,id:g.id,collect_logs:g.collect_logs,authentication:g.authentication},null,2))})'
unset GATEWAY_TOKEN

保存した ID とともに、collect_logs: true および authentication: true が表示されることを確認します。

不正な入力を含むタグ付きリクエストを送信する

このステップでは、制御された入力エラーを作成します。ゲートウェイとアップストリームの認証情報は有効なままにし、モデル入力だけを不正な状態にします。

カスタムメタデータには、フラットな文字列、数値、Boolean の値を最大 5 個まで指定できます。cf. で始まるキーは Cloudflare が予約しています。このリクエストでは、安全な 3 つの値を使用します。ランダムなトレース ID、ケース名 bad-input、Boolean 値 synthetic: true です。

選択したモデルにはプロンプトが必要です。プロンプトを意図的に省略し、レスポンスと HTTP ステータスの両方を保存します。

ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
MODEL='@cf/meta/llama-3.3-70b-instruct-fp8-fast'
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-input",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -D .labex/bad-input-headers.txt \
  -o .labex/bad-input-response.json -w '%{http_code}' \
  -H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
  -H "Authorization: Bearer $UPSTREAM_TOKEN" \
  -H "cf-aig-metadata: $METADATA" \
  -H 'Content-Type: application/json' \
  --data '{"max_tokens":16}' \
  "https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-input-status.txt

HTTP 400 または 422 が表示されることを確認します。これはクライアント入力の失敗であり、認証問題の証拠ではありません。レスポンス本文は範囲を限定したトラブルシューティング用に保存されますが、自動的には表示されません。

ゲートウェイログで失敗を関連付ける

このステップでは、時刻だけを頼りに検索するのではなく、トレース ID を使ってリクエストレコードを見つけます。

ログが表示されるまで少し時間がかかる場合があります。管理 API から既存のログ一覧を読み取り、各フラットメタデータオブジェクトを解析して、失敗を説明するために必要なフィールドだけを表示します。

ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID/logs?per_page=50" \
  > .labex/logs-after-input.json
unset GATEWAY_TOKEN
node - <<'NODE'
const body = require('./.labex/logs-after-input.json')
const trace = require('./.labex/state.json').traceId
const meta = row => {
  try { return typeof row.metadata === 'string' ? JSON.parse(row.metadata) : (row.metadata || {}) }
  catch { return {} }
}
const matches = (body.result || []).filter(row => meta(row).trace_id === trace && meta(row).case === 'bad-input')
console.log(matches.map(row => ({
  id: row.id,
  provider: row.provider,
  model: row.model,
  success: row.success,
  status_code: row.status_code,
  duration: row.duration,
  tokens_in: row.tokens_in,
  tokens_out: row.tokens_out,
  metadata: meta(row)
})))
if (!matches.some(row => row.success === false)) process.exit(2)
NODE

保存したトレース ID、case: "bad-input"、Workers AI プロバイダー、失敗したステータスが表示されることを確認します。不正な入力は生成開始前に失敗する場合があるため、トークン数が空になることがあります。まだエントリが表示されない場合は約 20 秒待ってから、同じ読み取り専用ブロックを再実行してください。

Dashboard でゲートウェイの Logs ビューを開きます。メタデータフィルターまたは表示されたタイムスタンプを使って失敗した行を見つけ、その詳細パネルを開きます。モデル、失敗ステータス、カスタムメタデータが、同じ合成リクエストを示していることを確認します。

安全なカスタムメタデータで関連付けられた失敗した Workers AI ログ行

ステータス、処理時間、合成トレースメタデータを示す失敗ログの詳細

ゲートウェイ認証とアップストリーム認証の失敗を切り分ける

このステップでは、一度に 1 つの認証情報だけを変更します。どちらのテストも 401 または 403 を返す可能性があるため、ステータスだけでは不十分です。ログがどこに記録されたかによって、足りない情報を補います。

まず、アップストリームの認証情報は有効なまま、ゲートウェイの認証情報だけを無効にします。認証済みゲートウェイは、リクエストがゲートウェイに入る前に拒否するため、タグ付きのプロバイダーログは作成されない可能性があります。

ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
TRACE_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).traceId')
MODEL='@cf/meta/llama-3.3-70b-instruct-fp8-fast'
METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-gateway-auth",synthetic:true}))' "$TRACE_ID")
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -o .labex/bad-gateway-auth-response.json -w '%{http_code}' \
  -H 'cf-aig-authorization: Bearer deliberately-invalid-gateway' \
  -H "Authorization: Bearer $UPSTREAM_TOKEN" \
  -H "cf-aig-metadata: $METADATA" \
  -H 'Content-Type: application/json' \
  --data '{"prompt":"This request must not reach Workers AI.","max_tokens":8}' \
  "https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-gateway-auth-status.txt

次に、ゲートウェイの認証情報は有効なまま、Workers AI のアップストリーム認証情報だけを置き換えます。このリクエストはゲートウェイに入り、失敗したプロバイダーレコードを残す可能性があります。

METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"bad-upstream-auth",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
STATUS=$(curl --http1.1 -sS -o .labex/bad-upstream-auth-response.json -w '%{http_code}' \
  -H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
  -H 'Authorization: Bearer deliberately-invalid-upstream' \
  -H "cf-aig-metadata: $METADATA" \
  -H 'Content-Type: application/json' \
  --data '{"prompt":"This request should reach the upstream authorization check.","max_tokens":8}' \
  "https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/bad-upstream-auth-status.txt

どちらも 401 または 403 が返ることを確認します。少し待ってから、ログを再生成せずに読み取り、2 つのタグを比較します。

GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID/logs?per_page=50" \
  > .labex/logs-after-auth.json
unset GATEWAY_TOKEN
node - <<'NODE'
const rows = require('./.labex/logs-after-auth.json').result || []
const trace = require('./.labex/state.json').traceId
const meta = row => { try { return typeof row.metadata === 'string' ? JSON.parse(row.metadata) : (row.metadata || {}) } catch { return {} } }
for (const name of ['bad-gateway-auth', 'bad-upstream-auth']) {
  const found = rows.filter(row => meta(row).trace_id === trace && meta(row).case === name)
  console.log(name, found.map(row => ({status_code: row.status_code, success: row.success, provider: row.provider})))
}
NODE

ゲートウェイ認証のタグにはプロバイダーログがなく、アップストリーム認証のタグには失敗した Workers AI の行が表示されるはずです。このように、境界の図と相関ログは、HTTP ステータスだけの場合よりも多くの情報を与えてくれます。

ゲートウェイログによって、記録されたアップストリームの失敗とゲートウェイ到達前の拒否を区別できる

リクエストを修正して成功を確認する

このステップでは、両方の有効な認証情報を戻し、必須のプロンプトを指定します。実行時の出力と可観測性の結果が一致して初めて、修正が完了したと判断できます。

同じトレース ID を使い、新しい repaired ケース名を指定します。

METADATA=$(node -e 'process.stdout.write(JSON.stringify({trace_id:process.argv[1],case:"repaired",synthetic:true}))' "$TRACE_ID")
GATEWAY_TOKEN=$(cat .labex/gateway-token)
UPSTREAM_TOKEN=$(npx wrangler auth token --json | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).token))')
STATUS=$(curl --http1.1 -sS -o .labex/repaired-response.json -w '%{http_code}' \
  -H "cf-aig-authorization: Bearer $GATEWAY_TOKEN" \
  -H "Authorization: Bearer $UPSTREAM_TOKEN" \
  -H "cf-aig-metadata: $METADATA" \
  -H 'Content-Type: application/json' \
  --data '{"prompt":"In one short sentence, explain why trace IDs help debugging.","max_tokens":48}' \
  "https://gateway.ai.cloudflare.com/v1/$ACCOUNT_ID/$GATEWAY_ID/workers-ai/$MODEL")
unset GATEWAY_TOKEN UPSTREAM_TOKEN METADATA
printf '%s\n' "$STATUS" | tee .labex/repaired-status.txt
node -e 'const b=require("./.labex/repaired-response.json"); console.log(b.result?.response ?? b.result)'

HTTP 200 と、空でない生成テキストが表示されることを確認します。必要に応じてログが表示されるまで待ち、前のステップにある読み取り専用のログ一覧を再実行します。Dashboard でトレース ID を使ってフィルタリングし、bad-inputbad-upstream-authrepaired を比較します。修正後の行には成功、200 ステータス、トークン使用量が表示されるはずです。

同じ合成トレースの下に、修正後のリクエストが成功ログとして表示されている

破棄可能なゲートウェイを削除する

このステップでは、管理用認証情報を使って削除の完了を確認できるうちに、クラウドリソースを削除します。

ACCOUNT_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).accountId')
GATEWAY_ID=$(node -p 'JSON.parse(require("fs").readFileSync(".labex/state.json")).gatewayId')
GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS -X DELETE \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways/$GATEWAY_ID" \
  | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const b=JSON.parse(s);if(!b.success)process.exit(1);console.log("gateway deletion accepted")})'
unset GATEWAY_TOKEN

GATEWAY_TOKEN=$(cat .labex/gateway-token)
curl --http1.1 -fsS \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai-gateway/gateways" \
  > .labex/gateways-after-delete.json
unset GATEWAY_TOKEN
node -e 'const b=require("./.labex/gateways-after-delete.json"),id=process.argv[1],found=(b.result||[]).some(g=>g.id===id);console.log("gateway absent:",!found);if(found)process.exit(1)' "$GATEWAY_ID"

gateway absent: true が表示されることを確認します。この認証済み一覧では、ログアウトやネットワーク障害でページが表示されないだけなのか、実際に削除されたのかを区別できます。

トークンを削除してログアウトする

このステップでは、残っているクラウド認証情報を無効化し、VM の接続を解除します。

Cloudflare Dashboard で My Profile → API Tokens を開きます。保存した正確な tokenName を見つけ、Actions を開いて Delete を選択します。確認内容を確認し、そのトークンだけを削除します。ゲートウェイの削除がすでに確認できているため、ここで無効化して問題ありません。

VM 上のコピーを消去し、Wrangler の別の認証も終了します。

shred -u .labex/gateway-token
npx wrangler logout
npx wrangler whoami --json || true
test ! -e .labex/gateway-token && echo "local gateway token removed"

loggedIn: falselocal gateway token removed が表示されることを確認します。Dashboard のセッションは別のもので、ログインしたままです。実験終了時に LabEx がこの一時 VM を破棄し、保存は行いません。

まとめ

安全なカスタムメタデータを使って、不正な Workers AI リクエストを AI Gateway のログに関連付けました。また、HTTP ステータスには境界の情報が必要であることを学びました。ゲートウェイ認証が不正な場合はプロバイダーログが作成される前に拒否されます。一方、アップストリーム認証が不正な場合は、失敗した Workers AI レコードとして記録されます。その後、入力を修正し、生成テキストと相関する成功ログを確認して、破棄可能な認証情報とリソースをすべて削除しました。

次の実験では、同じ証拠重視のアプローチをキャッシュに適用します。1 件の制限された公開リクエストを繰り返し、キャッシュヒットと新しいモデル呼び出しを区別し、新しい出力が必要な場合はキャッシュをバイパスします。