TL;DR
OpenCode から、Cortex AI Gateway 経由で Claude を呼び出し、エージェントの動きをトレースで確認してみました。
以下、本記事の要点
- すべてのユーザーやアプリが Cortex AI Gateway を経由してモデルに到達することで、アクセス制御、トレース、コスト把握をまとめて行える
- アカウントに最初から用意されており、権限も既定で全員に付いているため、設定なしで使い始められる
- エージェントへの1回の依頼が内部で何回のモデル呼び出しに分かれ、各回でどれだけトークンを使ったかが、SQL で引ける
AI ゲートウェイとは?
Cortex の前にAI ゲートウェイが何なのかについて、ゲートウェイがない構成で起きる問題、ゲートウェイを挟むと変わることの順に簡単に説明します。すでに知っている方は読み飛ばしてください。
前提として、AI ゲートウェイとは、アプリと LLM(API) の間で呼び出しを制御・管理してくれるものです。
AI ゲートウェイがない(=アプリがモデルを直接呼ぶ)構成
LLM を使うアプリケーションは、素朴に作ると、モデル提供者の API を直接呼ぶ、すなわちアプリの中に API キーを持ち、そのキーで OpenAI や Anthropic にリクエストを送る形になります。
これを行うチームが組織に1つしかなければ特に困ることはありませんが、この方式で同じことをするチームが複数(例えば3つ)あると管理が分散してしまいます。(図は Claude くんが作成)
ここで、次のような問題が発生
- アクセス権の付与:誰がどのモデルを使えるかを、提供元ごとのキーの発行と管理で決めることになる
- 支出の按分:利用状況が提供元ごと、キーごとに集計されるため、チームやアプリの単位で按分するには、各社の画面を突き合わせる必要がある
- 活動の監査:どのアプリがいつどのモデルを呼んだかの記録が、提供元ごとに分かれて残る
間にゲートウェイを挟むと何が変わるか
AI ゲートウェイは、アプリとモデルのあいだに置く中継サーバーのようなもので、アプリの接続先を提供元それぞれのエンドポイントからゲートウェイに集約することができます。ゲートウェイは受け取ったリクエストをモデルへ転送し、ログを取ります。
呼び出しを一点に集めることで、権限管理や支出の按分などをアプリの外側でまとめて行うことができます。
API 形式
多くの AI ゲートウェイは、OpenAI の Chat Completions や Anthropic と同じリクエスト形式をそのまま受け付け、さらにパスも提供元のものを写しており、差し替わるのは base URL の部分だけなので接続先を変えるのも簡単です。これは、Cortex AI Gatewayも同じ。
直接呼ぶ場合 https://api.anthropic.com/v1/messages
ゲートウェイ経由 https://<ゲートウェイの base URL>/v1/messages
Cortex AI Gateway とは
Cortex AI Gateway は、2026年9月15日にプレビューとして公開された Snowflake の AI ゲートウェイで、前章の仕組みを、Snowflake の権限管理と記録の仕組みの上に載せることができます。
さらに、利用者がゲートウェイを構築する必要はなく、Snowflake が各アカウントに SNOWFLAKE という名前のゲートウェイを1つ用意しており、これは SQL 上では AI GATEWAY という種別のオブジェクトです。テーブルやウェアハウスと同じく、GRANT と REVOKE で権限の付け外しも可能。
SHOW AI GATEWAYS;
DESCRIBE AI GATEWAY SNOWFLAKE;
エンドポイントはこの形
https://<アカウントのホスト>/api/v2/aigateways/SNOWFLAKE/v1/messages
受け付ける API 形式は2つで
- [Anthropic Messages 形式]
/v1/messagesを使う。Claude のモデルだけが対象。 - [OpenAI Chat Completions 形式]
/v1/chat/completionsを使う。Claude 以外のモデルが対象。
なお、OpenAI の Responses API(/v1/responses)は現状未対応。
必要な権限
ゲートウェイ経由でモデルを呼ぶには、ゲートウェイ側とモデル側の両方の権限が必要
| 権限 | 何に必要か | 確認の方法 |
|---|---|---|
AI GATEWAY SNOWFLAKE への USAGE
|
ゲートウェイにリクエストを送る |
SHOW GRANTS ON AI GATEWAY SNOWFLAKE の結果 |
AI GATEWAY SNOWFLAKE への MONITOR
|
トレースと使用量を参照する |
SHOW GRANTS ON AI GATEWAY SNOWFLAKE の結果 |
モデルへのアクセス権(SNOWFLAKE.CORTEX_USER など) |
モデルを実際に呼ぶ |
AI_COMPLETE でモデルを1回呼ぶ |
なお、USAGE は既定で PUBLIC に付与されているため、ゲートウェイには初期状態で誰でもリクエストを送れる。なお、ゲートウェイの USAGE を持っていても、モデル側のアクセス権がなければリクエストは通らないため、ゲートウェイを導入しても既存のアクセス制御の範囲が広がることはない。
リクエストの認証には、プログラマティックアクセストークン(PAT)を使う。
PAT は Snowsight の自分のプロフィールから発行し、Authorization: Bearer ヘッダに入れて送る。
また、PAT を使うには、ユーザーにネットワークポリシーが適用されている必要がある。
人間のユーザーの場合、ネットワークポリシーがなくても PAT の発行自体は成功し、使う段階で 401 が返る。
私の環境でも発生し、今回は検証用だったため Snowsight の PAT の設定の Bypass requirement for network policy から一時的に回避した。継続して使う場合、ネットワークポリシーを設定する必要がある。
ゲートウェイの設定
ゲートウェイの設定や URL はDESCRIBE AI GATEWAY SNOWFLAKE で確認できる。設定はspecification を参照すればよく、例えば次のようになっている
schema_version: 1
models:
- name: '*'
logging:
enabled: true
この例だと、models はゲートウェイ経由で呼べるモデルの範囲で、'*' はアカウントで利用できるモデルすべてを表し、logging.enabled が true なので、ゲートウェイを通ったリクエストは記録されます。記録されるのは、1回の推論呼び出しごとに、誰が、どのモデルを、何トークン使い、どれだけ時間がかかったか、といったメタデータで、AGENT_TRACE_TABLE で参照可能。なお、消費したクレジットはトレースには含まれない。
プロンプトと応答の中身は既定では記録されず、ログに残すには、 logging.capture_payload.request_response を true にする必要がある。
仕様はアカウントに1つしかなく、有効にすると同じアカウントの全利用者の入力が記録対象になるため今回は有効にしませんでした。
試してみる
今回は、小さな TODO アプリに OpenCode で機能を2つ追加させ、そのあいだに発生したモデル呼び出しを Cortex AI Gateway のトレースから見てみました。
題材にした TODO アプリ
題材には、Streamlit で作った TODO アプリを使いました。
土台の作成は Claude Code で行ったため、ゲートウェイは経由していません。
アプリでできることは
- サイドバーで、タイトル、メモ、優先度、期限を入力してタスクを追加する
- 同じサイドバーで、状態、優先度、キーワードによって一覧を絞り込む
- メイン画面に件数(全件、未完了、完了、期限切れ)と達成率を表示し、その下にタスクを並べる
- 各タスクで、完了のチェック、編集、削除を行う
程度で、ディレクトリ構成はこんな感じ
.
├── app.py エントリポイント。ページ設定と画面部品の呼び出しのみ
├── todo/
│ ├── models.py データ表現(Todo / Priority / TodoFilter / Summary)
│ ├── database.py 接続の生成とスキーマ定義(DDL)
│ ├── repository.py CRUD、絞り込み、集計。SQL を書くのはここだけ
│ ├── state.py 再実行をまたいで持ち越す状態(編集中の行など)
│ └── views.py 画面部品(サイドバー、サマリー、一覧行、編集フォーム)
├── tests/
│ └── test_repository.py データ層のテスト
├── docs/ opencode に渡す実装指示書
├── pyproject.toml uv の依存定義
└── README.md
追加させた機能は、次の2つ
- CSV エクスポート:タスクの一覧を CSV ファイルとして書き出す。タスクのデータ構造を読めば実装でき、既存の画面への影響は小さい
- タグ機能:タスクにタグを付け、タグで絞り込めるようにする。追加のフォーム、絞り込み、一覧の表示の3か所にまたがるため、アプリ全体を読む必要がある
実装の詳細は docs/ の指示書に書き、プロンプトではその指示書を参照させました。指示書には完了条件とコードの規約、触る見込みのファイルを記載しています。
CSV エクスポートのプロンプトはこんな感じ
docs/csv-export.mdの指示に従って、CSVエクスポート機能を実装してください
OpenCode をゲートウェイにつなぐ
OpenCode をつなぐ前に、curl で1回リクエストを送り、ゲートウェイまで届くことを確かめました。PAT は read -rs で環境変数に読み込むと、画面にもシェルの履歴にも残りません。
read -rs SNOWFLAKE_PAT && export SNOWFLAKE_PAT
curl -i "https://<アカウントのホスト>/api/v2/aigateways/SNOWFLAKE/v1/messages" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SNOWFLAKE_PAT" \
-d '{"model": "claude-sonnet-5", "max_tokens": 256, "messages": [{"role": "user", "content": "hello"}]}'
エージェントには OpenCode を使いました。OpenCode は、ターミナルで動くオープンソースのコーディングエージェントで、自前のモデルを持たず、設定ファイルで指定した提供元にリクエストを送る形で動作します。
接続先を差し替えられるので、ゲートウェイに向けるのに都合がよく、Snowflake のドキュメントにも設定例が載っていました。
設定ファイルは、リポジトリのルートに opencode.json として置きました。
OpenCode は起動したディレクトリから設定ファイルを探すので、プロジェクトごとに接続先を分けられます。
{
"$schema": "https://opencode.ai/config.json",
"model": "snowflake-cortex/claude-sonnet-5",
"provider": {
"snowflake-cortex": {
"npm": "@ai-sdk/anthropic",
"name": "Snowflake Cortex AI Gateway",
"options": {
"baseURL": "https://<アカウントのホスト>/api/v2/aigateways/SNOWFLAKE/v1",
"apiKey": "{env:SNOWFLAKE_PAT}",
"headers": {
"Authorization": "Bearer {env:SNOWFLAKE_PAT}",
"snow-agent-name": "opencode"
}
},
"models": {
"claude-sonnet-5": {
"name": "Claude Sonnet 5"
}
}
}
}
}
Snowflake のドキュメントの設定例は Chat Completions 形式でしたが、私の環境で使えるのは Messages 形式の Claude なので、少し変更しています。
baseURL の末尾は、最初は /v1 を付けずに書いて 404 になりました。
起動は、環境変数に PAT を入れてからリポジトリのルートで行う。
read -rs SNOWFLAKE_PAT && export SNOWFLAKE_PAT
opencode
なお、claude-sonnet-4-5 では、OpenCode がリクエストに含めるツール定義のうちeager_input_streaming というフィールドをゲートウェイが受け付けず、エラーが返りました。claude-sonnet-5 に変えるとそのまま通ったため、以降の計測はすべてこのモデルで行いました。
なお、プレビュー段階のため、この挙動は今後変わる可能性がある。
トレースを見てみる
トレースは AGENT_TRACE_TABLE で参照でき、1行が1回のモデル呼び出しとなっている。
時刻などは通常の列に入り、モデル名やトークン数は record_attributes に、リクエストを送ったユーザーなどは resource_attributes に入る。
OpenCode の実行では、gen_ai.conversation.id に ses_ で始まるセッション ID が入っていました。OpenCode のセッション単位で呼び出しがまとまるので、この値で絞り込みました。
直近のセッションを一覧にするクエリは次のとおり
SELECT
record_attributes:"gen_ai.conversation.id"::STRING AS conversation_id,
COUNT(*) AS calls,
MIN(start_timestamp) AS started
FROM TABLE(AGENT_TRACE_TABLE('SNOWFLAKE'))
GROUP BY 1
ORDER BY started DESC
LIMIT 10;
対象のセッション内の呼び出しを順に並べる。
SELECT
ROW_NUMBER() OVER (ORDER BY start_timestamp) AS seq,
record_attributes:"gen_ai.request.model"::STRING AS model,
TRY_TO_NUMBER(record_attributes:"gen_ai.usage.input_tokens"::STRING) AS input_tokens,
TRY_TO_NUMBER(record_attributes:"gen_ai.usage.cache_read.input_tokens"::STRING) AS cache_read_tokens,
TRY_TO_NUMBER(record_attributes:"gen_ai.usage.output_tokens"::STRING) AS output_tokens,
record_attributes:"gen_ai.client.operation.duration"::FLOAT AS duration_sec
FROM TABLE(AGENT_TRACE_TABLE('SNOWFLAKE'))
WHERE record_attributes:"gen_ai.conversation.id"::STRING = '<conversation_id>'
ORDER BY seq;
セッション全体の集計
SELECT
COUNT(*) AS calls,
SUM(TRY_TO_NUMBER(record_attributes:"gen_ai.usage.input_tokens"::STRING)) AS input_tokens,
SUM(TRY_TO_NUMBER(record_attributes:"gen_ai.usage.cache_read.input_tokens"::STRING)) AS cache_read_tokens,
SUM(TRY_TO_NUMBER(record_attributes:"gen_ai.usage.output_tokens"::STRING)) AS output_tokens,
ROUND(SUM(record_attributes:"gen_ai.client.operation.duration"::FLOAT), 1) AS model_sec,
DATEDIFF('second', MIN(start_timestamp), MAX(timestamp)) AS elapsed_sec
FROM TABLE(AGENT_TRACE_TABLE('SNOWFLAKE'))
WHERE record_attributes:"gen_ai.conversation.id"::STRING = '<conversation_id>';
なお、同じ内容は、Snowsight の AI Gateway の Observability 画面からも確認できますが、OpenCode の呼び出しは1回ごとに別のトレースとして記録されるため、Snowsight の Observability 画面では1回の依頼をまとめて追いにくい状態でした。
OpenTelemetry のプラグインを入れて traceparent ヘッダを送るようにすると、1回の依頼が1つのトレースにまとまり、画面上でも見やすくなる可能性があります。
CSV エクスポートを実装したセッションを集計した結果は次のとおり
| 項目 | 値 |
|---|---|
| 呼び出し回数 | 23 |
| 入力トークン | 876,514 |
| うちキャッシュ読み込み | 826,083 |
| 出力トークン | 11,463 |
| モデルの応答時間の合計 | 151.1 秒 |
| セッション時間 | 185 秒 |
タグ機能を実装したセッションはこんな感じ
| 項目 | 値 |
|---|---|
| 呼び出し回数 | 50 |
| 入力トークン | 3,621,770 |
| うちキャッシュ読み込み | 3,526,839 |
| 出力トークン | 34,022 |
| モデルの応答時間の合計 | 421.9 秒 |
| セッション時間 | 542 秒 |
想定通りタグ機能を追加するセッションの方が重く、呼び出し回数はおよそ2倍、入力トークンは約4倍となりました。
また、毎回送られる入力の大部分は、前回までと同じ内容としてキャッシュで処理されているようです。
ユーザー・クライアント別の集計
最後に、ゲートウェイの本来の目的に近い集計もしてみました。
ユーザーとクライアントごとに、呼び出し回数とトークン数をまとめるクエリ
SELECT
resource_attributes:"user"::STRING AS user_name,
record_attributes:"http.request.header.snow-agent-name"::STRING AS client,
COUNT(*) AS calls,
SUM(TRY_TO_NUMBER(record_attributes:"gen_ai.usage.input_tokens"::STRING)) AS input_tokens,
SUM(TRY_TO_NUMBER(record_attributes:"gen_ai.usage.output_tokens"::STRING)) AS output_tokens
FROM TABLE(AGENT_TRACE_TABLE('SNOWFLAKE'))
GROUP BY 1, 2
ORDER BY calls DESC;
今回は私1人が OpenCode と curl だけで使ったので、結果は2行になりました。curl の場合、CLIENT は null になっていました。
利用者やクライアントが増えれば、それぞれが1行ずつ並ぶのでかなり集計しやすそう。
まとめ
Snowflake の Cortex AI Gateway を、OpenCode から使って試してみました。
- ゲートウェイはアカウントに最初から用意されているので、基本的には PAT を発行して接続先を差し替えるだけで使い始められます。ただし、PAT のネットワークポリシー要件と、モデルによってツール定義が弾かれる点には注意が必要です
- エージェントへの1回の依頼が裏で何回のモデル呼び出しに分かれ、どれだけトークンを使ったかを、SQL で簡単に確認できました
- ユーザーやクライアントごとの集計が1本のクエリで出せるのは便利そうです。組織で複数のエージェントやアプリを使っている場合でも、誰が何にどれだけ使っているかを、アプリ側に手を入れずに把握できます
なお、本記事の内容は2026年9月時点のプレビュー版で検証したものです。対応する API 形式や使えるモデルなどは、今後変わる可能性があります。
次回は、Databricks の Unity Gateway も同じように試して、比較してみようと思います。




