はじめに
レッドハットのソリューションアーキテクトの森です。
本記事は、Red Hat Connectivity Link入門シリーズの第5回です。第1回でRed Hat Connectivity Linkをインストールした環境があれば、本記事の内容を実践できます。
- 過去の記事はこちらから
Connectivity LinkとAI Gateway
Red Hat Connectivity Linkは、従来のAPI Gatewayの機能に加えて、AI Gatewayとしての側面も持ちます。LLM(Large Language Model)APIの普及に伴い、AI特有の要件に対応したGateway機能が求められるようになりました。

Connectivity LinkがAI Gatewayとして提供する主な機能:
- LLMモデル別の利用状況/性能/トラフィック可視化: どのモデルがどれだけ使われているかを可視化
- トークンベースのレート制限: 従来のリクエスト数ではなく、トークン消費量に基づく制限
- 認証/認可: ユーザーやティアごとのアクセス制御
- コスト制御: トークン単価に基づくコスト追跡と予算管理
これらの機能により、Platform Engineersは複数のAIモデルへのアクセスを統一的に管理し、セキュアかつコスト効率的なLLM APIサービスを提供できます。
MCP Gateway(Tech Preview):
Connectivity Link 1.3.3では、さらにMCP Gateway(Model Context Protocol Gateway)がTech Previewとして追加されました。 MCP Gatewayは、AIエージェントがツールやデータソースにアクセスするための標準プロトコルであるModel Context Protocolをサポートし、AIアプリケーションの統合をさらに強化します。 MCP Gatewayの詳細については、次の第6回で詳しく解説する予定です。
本記事の焦点
第5回となる今回は、AI Gateway機能の中でも特に重要なトークンベースのレート制限について解説します。 従来のリクエスト数ベースのレート制限では、LLM APIのコスト管理や公平な利用制限が困難でした。 Kuadrant 1.3で提供されるTokenRateLimitPolicyを使用することで、実際のトークン消費量に基づいた精密なレート制限が可能になります。
本記事では、最小構成でトークンベースのレート制限を体験できるよう、モックLLM APIサービスとシンプルなHTTP Gatewayを使用します。
本記事で扱う内容
- なぜトークンベースのレート制限が必要か: リクエストベースの課題
- モックLLM APIサービスのデプロイ: OpenAI互換のテストAPI
- Gateway作成: シンプルなHTTP Gateway
- TokenRateLimitPolicyの基本: トークン自動抽出と制限設定
- 動作確認: Port-forward経由でのレート制限テスト
前提条件
本記事では、以下が完了していることを前提とします:
- Red Hat Connectivity Link 1.3がインストール済み(第1回を参照)
- oc CLIがインストール済みで、クラスターにログイン済み
なぜトークンベースのレート制限が必要か
LLM APIの特性
LLM APIは、従来のREST APIと異なる特性を持ちます:
従来のREST API:
- リクエストの処理コストがほぼ一定
- レスポンスサイズも比較的均一
- リクエスト数ベースの制限で公平性を確保可能
LLM API:
- リクエストごとの処理コストが大きく異なる
- 入力トークン数(プロンプトの長さ)による変動
- 出力トークン数(生成されるテキストの長さ)による変動
- リクエスト数だけでは公平性とコスト管理が困難
具体例: 不公平なリクエストベース制限
シナリオ: 1分あたり10リクエストの制限
ユーザーA:
リクエスト1: 入力10,000トークン、出力4,000トークン → 14,000トークン リクエスト2: 入力10,000トークン、出力4,000トークン → 14,000トークン ... 合計10リクエスト = 140,000トークン(非常に高コスト)
ユーザーB:
リクエスト1: 入力50トークン、出力150トークン → 200トークン リクエスト2: 入力50トークン、出力150トークン → 200トークン ... 合計10リクエスト = 2,000トークン(低コスト)
同じリクエスト数でも、実際のコストは70倍の差があります。
TokenRateLimitPolicyの利点
| 特徴 | リクエストベース | トークンベース |
|---|---|---|
| 公平性 | リクエスト数のみで制限 | 実際の使用量で制限 |
| コスト管理 | 困難(リクエストサイズ不定) | 精密(トークン単位) |
| 悪用防止 | 大きなリクエストで悪用可能 | トークン制限で防止 |
| ビジネスモデル | 単純なティア分け | トークン単価ベースの課金 |
| ユーザー体験 | 小リクエストが不利 | 使用量に応じた公平な制限 |
LLM APIレスポンスの構造
LLM API(OpenAI互換API)は、以下のようなレスポンスを返します:
{ "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1715580000, "model": "gpt-4", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "I'm doing well, thank you!" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 12, "completion_tokens": 28, "total_tokens": 40 } }
重要: usageフィールドに注目してください。これがRateLimitPolicyで使用するトークン数です。
モックLLM APIバックエンドの準備
本記事では、実際のGPUやモデルがない環境でもテストできるよう、OpenAI互換のモックLLM APIサービスを使用します。
注: 実際の運用環境では、OpenShift AIのvLLMやその他のLLMサービングプラットフォームを使用してください。OpenShift AIの詳細は公式ドキュメントを参照してください。
モックLLM APIサービスのデプロイ
実際のGPUやモデルがない環境でもテストできるよう、モックサービスを使用します:
# MockLLM API用のnamespace作成
oc create namespace llm-api
# MockLLM APIの作成
oc apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: mock-llm-api
namespace: llm-api
data:
app.py: |
from flask import Flask, request, jsonify
import random
import time
app = Flask(__name__)
@app.route('/v1/chat/completions', methods=['POST'])
def chat_completions():
data = request.get_json(silent=True) or {}
messages = data.get('messages', [])
max_tokens = data.get('max_tokens', 100)
# プロンプトトークン数を推定(簡易的に文字数/4)
prompt_text = ' '.join([m.get('content', '') for m in messages])
prompt_tokens = len(prompt_text) // 4
# 出力トークン数(max_tokensの70-90%をランダムに使用)
completion_tokens = int(max_tokens * random.uniform(0.7, 0.9))
response = {
"id": f"chatcmpl-{random.randint(1000000, 9999999)}",
"object": "chat.completion",
"created": int(time.time()),
"model": data.get('model', 'mock-llm'),
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "This is a mock response from the LLM API."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens
}
}
return jsonify(response)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mock-llm-api
namespace: llm-api
spec:
replicas: 1
selector:
matchLabels:
app: mock-llm-api
template:
metadata:
labels:
app: mock-llm-api
spec:
containers:
- name: api
image: python:3.11-slim
command:
- /bin/sh
- -c
- |
pip install --target=/packages flask && \
PYTHONPATH=/packages python /app/app.py
ports:
- containerPort: 8080
volumeMounts:
- name: app
mountPath: /app
- name: packages
mountPath: /packages
volumes:
- name: app
configMap:
name: mock-llm-api
- name: packages
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: mock-llm-api
namespace: llm-api
spec:
selector:
app: mock-llm-api
ports:
- port: 8080
targetPort: 8080
EOF
注: 実際の環境では、OpenShift AIのvLLMやその他のLLMサービングプラットフォームを使用してください。
Gateway作成
モックLLM APIサービスを公開するためのシンプルなHTTP Gatewayを作成します。
# Gateway用のnamespace作成
oc create namespace llm-gateway
# Gateway作成
oc apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: llm-gateway
namespace: llm-gateway
spec:
gatewayClassName: istio
listeners:
- name: http
hostname: "*.llm-api.local"
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
EOF
HTTPRouteでの公開
モックLLM APIをGateway経由で公開します:
oc apply -f - <<EOF
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: llm-api
namespace: llm-api
labels:
service: mock-llm-api
deployment: mock-llm-api
spec:
parentRefs:
- name: llm-gateway
namespace: llm-gateway
hostnames:
- "api.llm-api.local"
rules:
- matches:
- method: POST
path:
type: PathPrefix
value: "/v1/chat/completions"
backendRefs:
- name: mock-llm-api
port: 8080
EOF
TokenRateLimitPolicyの実装
Kuadrant 1.3では、トークンベースのレート制限には専用のTokenRateLimitPolicy CRDを使用します。
トークン抽出の仕組み
TokenRateLimitPolicyは、レスポンスボディからトークン数を自動的に抽出します。OpenAI API互換のレスポンスのusage.total_tokensフィールドからトークン数を取得します:
{ "usage": { "prompt_tokens": 12, "completion_tokens": 28, "total_tokens": 40 } }
TokenRateLimitPolicy作成
oc apply -f - <<EOF
apiVersion: kuadrant.io/v1alpha1
kind: TokenRateLimitPolicy
metadata:
name: llm-api-token-limit
namespace: llm-api
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: llm-api
limits:
token-limit:
rates:
- limit: 1000
window: 1m
counters:
- expression: "1"
EOF
設定の詳細解説
APIバージョン:
kuadrant.io/v1alpha1- TokenRateLimitPolicy専用のAPIバージョン
limits.token-limit:
- rates.limit: 1分あたり1,000トークン(テスト用の小さい値)
- rates.window:
1m- 1分間のウィンドウ
counters:
- expression:
"1"- すべてのリクエストを同じカウンターでカウント - 注: 本番環境では、AuthPolicyと統合して
auth.identity.useridを使用します(第3回記事参照)
トークン数の抽出:
- TokenRateLimitPolicyは、レスポンスボディの
usage.total_tokensを自動的に抽出 - 手動で
costsフィールドを設定する必要はありません
設定後の状態(Policy Topology View)

今回は、簡略化のため DNS Policy、TLS Policy による外部公開は行なっておりません。こちらの詳細については、第2回記事を参照ください。
動作確認
Port-forward経由でトークンベースのレート制限をテストします。
1. Port-forwardの開始
別のターミナルで以下を実行:
oc port-forward -n llm-gateway service/llm-gateway-istio 8080:80
2. 正常リクエストのテスト
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Host: api.llm-api.local" \
-H "Content-Type: application/json" \
-d '{
"model": "mock-llm",
"messages": [
{"role": "user", "content": "What is OpenShift?"}
],
"max_tokens": 200
}' | jq .
期待されるレスポンス:
{ "choices": [ { "finish_reason": "stop", "index": 0, "message": { "content": "This is a mock response from the LLM API.", "role": "assistant" } } ], "created": 1778639456, "id": "chatcmpl-3377409", "model": "mock-llm", "object": "chat.completion", "usage": { "completion_tokens": 142, "prompt_tokens": 4, "total_tokens": 146 } }
注: モックAPIはmax_tokensの70~90%をランダムに使用するため、completion_tokensとtotal_tokensは実行ごとに変わります。この例では約146トークンが消費されました。
3. レート制限の確認
トークン制限(1分あたり1,000トークン)を超えるまでリクエストを繰り返します:
# 連続してリクエストを送信(max_tokens=200で大きめのトークン消費)
for i in {1..10}; do
echo "=== Request $i ==="
response=$(curl -s -X POST http://localhost:8080/v1/chat/completions \
-H "Host: api.llm-api.local" \
-H "Content-Type: application/json" \
-d '{
"model": "mock-llm",
"messages": [{"role": "user", "content": "What is OpenShift?"}],
"max_tokens": 200
}')
echo "$response" | jq -r '.usage.total_tokens' 2>/dev/null || echo "$response"
sleep 1
done
実行例:
=== Request 1 === 152 === Request 2 === 172 === Request 3 === 151 === Request 4 === 158 === Request 5 === 160 === Request 6 === 145 === Request 7 === 168 === Request 8 === Too Many Requests === Request 9 === Too Many Requests === Request 10 === Too Many Requests
累計トークン消費の例:
- 1~7回目: 152 + 172 + 151 + 158 + 160 + 145 + 168 = 1,106トークン
- 7回目のリクエストで累計1,106トークンとなり、1,000トークンの制限を超過
- 8回目以降:
Too Many Requestsが返される
注:
- モックAPIはランダムなトークン数を返すため、実行ごとに各リクエストのトークン数は変わります
- 制限に達するまでのリクエスト回数も、6~8回程度で変動します
トークン消費の確認
TokenRateLimitPolicyが正しくトークン数をカウントしているか確認します。
OpenShift Web Consoleでのメトリクス確認
Limitador(レート制限エンジン)は、トークン消費状況をPrometheusメトリクスとして公開します。OpenShift Web Consoleから確認できます。
手順:
- OpenShift Web Consoleにログイン
- 左メニューから Observe > Metrics を選択
- クエリ入力欄に以下を入力:
authorized_hits{limitador_namespace="llm-api/llm-api"} - Run queries をクリック

グラフの見方:
グラフは、時系列で累計トークン消費量を表示します。リクエストを送信するごとに、authorized_hitsの値が増加していくことが確認できます。
メトリクスの意味:
- authorized_hits: 許可されたリクエストの累計トークン数
- レート制限内で処理されたリクエストのトークン数の合計
- この値が1分間で1,000トークンを超えると、次のリクエストは拒否されます
まとめ
本記事では、TokenRateLimitPolicyを使用したLLM API向けのトークンベースレート制限の基本を解説しました。
本記事で学んだこと
- トークンベースレート制限の必要性: リクエストベースとトークンベースの違い
- モックLLM APIのデプロイ: OpenAI互換API(
usage.total_tokensを含む) - Gateway作成: シンプルなHTTP Gateway
- TokenRateLimitPolicy: トークン数の自動抽出とカウント
- レート制限の動作: 累計トークン消費による制限発動
- 動作確認: Port-forward経由でのテスト
トークンベースレート制限の利点
| 項目 | 利点 |
|---|---|
| 公平性 | 実際の使用量に基づく制限 |
| コスト管理 | トークン単位でのコスト追跡 |
| 悪用防止 | 大きなリクエストでの制限回避を防止 |
| 柔軟性 | ビジネスモデルに応じた制限設定 |