Red Hat Connectivity Link 第5回: TokenRateLimitPolicy for LLM APIs

はじめに

レッドハットのソリューションアーキテクトの森です。

本記事は、Red Hat Connectivity Link入門シリーズの第5回です。第1回でRed Hat Connectivity Linkをインストールした環境があれば、本記事の内容を実践できます。

  • 過去の記事はこちらから

rheb.hatenablog.com

Connectivity LinkとAI Gateway

Red Hat Connectivity Linkは、従来のAPI Gatewayの機能に加えて、AI Gatewayとしての側面も持ちます。LLM(Large Language Model)APIの普及に伴い、AI特有の要件に対応したGateway機能が求められるようになりました。

Connectivity Linkの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を使用します。

本記事で扱う内容

  1. なぜトークンベースのレート制限が必要か: リクエストベースの課題
  2. モックLLM APIサービスのデプロイ: OpenAI互換のテストAPI
  3. Gateway作成: シンプルなHTTP Gateway
  4. TokenRateLimitPolicyの基本: トークン自動抽出と制限設定
  5. 動作確認: 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 PolicyTLS 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_tokenstotal_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から確認できます。

手順:

  1. OpenShift Web Consoleにログイン
  2. 左メニューから Observe > Metrics を選択
  3. クエリ入力欄に以下を入力: authorized_hits{limitador_namespace="llm-api/llm-api"}
  4. Run queries をクリック

グラフの見方:

グラフは、時系列で累計トークン消費量を表示します。リクエストを送信するごとに、authorized_hitsの値が増加していくことが確認できます。

メトリクスの意味:

  • authorized_hits: 許可されたリクエストの累計トークン数
    • レート制限内で処理されたリクエストのトークン数の合計
    • この値が1分間で1,000トークンを超えると、次のリクエストは拒否されます

まとめ

本記事では、TokenRateLimitPolicyを使用したLLM API向けのトークンベースレート制限の基本を解説しました。

本記事で学んだこと

  1. トークンベースレート制限の必要性: リクエストベースとトークンベースの違い
  2. モックLLM APIのデプロイ: OpenAI互換API(usage.total_tokensを含む)
  3. Gateway作成: シンプルなHTTP Gateway
  4. TokenRateLimitPolicy: トークン数の自動抽出とカウント
  5. レート制限の動作: 累計トークン消費による制限発動
  6. 動作確認: Port-forward経由でのテスト

トークンベースレート制限の利点

項目 利点
公平性 実際の使用量に基づく制限
コスト管理 トークン単位でのコスト追跡
悪用防止 大きなリクエストでの制限回避を防止
柔軟性 ビジネスモデルに応じた制限設定

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。