LiteLLM ProxyでAPI障害を吸収:フォールバックとRedisキャッシュの集約
APIのレート制限やプロバイダー障害が起きるたびに、各サービスでリトライとフォールバックを書き足していませんか。キャッシュやコスト集計までサービスごとに実装していると、挙動はいつの間にかばらつきます。この記事では、LiteLLM Proxyを社内のAPIゲートウェイとして1つ立て、モデルルーティング、フォールバック、Redisキャッシュ、キー単位の予算管理を集約する構成を、Docker Composeの最小構成まで通して説明します。
アプリケーションからプロバイダー依存コードとリトライ処理を切り離す
LLMを呼ぶコードが散らばる最大の原因は、プロバイダーごとにSDK、エラー形式、レート制限の返し方が違うことです。OpenAIの429とAnthropicのoverloadedでは例外クラスが違うため、アプリ側のtry/exceptは増える一方になります。しかもそのロジックは、サービスが3つあれば3回コピーされます。
LiteLLM Proxyは、OpenAI互換のエンドポイントを公開する常駐サーバーです。裏側で100を超えるプロバイダーへの呼び出しを吸収する、というのが公式ドキュメントの位置づけです。アプリ側は「OpenAI SDKでゲートウェイに投げる」だけになり、どのモデルに流すか、失敗したらどこへ逃がすかはゲートウェイ側の設定ファイルで決まります。
ここで効くのが、モデル名を論理名にする考え方です。アプリは gpt-4o のような実モデル名ではなく chat-default のような名前で呼び出します。実体を差し替えてもアプリのデプロイは要りません。
model_list:
- model_name: chat-default
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: chat-backup
litellm_params:
model: anthropic/claude-3-5-sonnet
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: embed-small
litellm_params:
model: openai/text-embedding-3-small
api_key: os.environ/OPENAI_API_KEY
モデルIDは例です。契約しているものや、その時点で利用可能なものに置き換えてください。同じ model_name を複数のエントリに付けると、1つのモデルグループとして負荷分散の対象になります。たとえばAzure OpenAI上のデプロイとOpenAI直のAPIを同名で並べれば、片方のクォータが尽きても自然にもう片方へ流れます。
LiteLLM ProxyとRedisによるセマンティックキャッシュの統合
キャッシュは設定ファイルの litellm_settings に書きます。まず完全一致キャッシュなら、Redisを指定するだけです。
litellm_settings:
cache: true
cache_params:
type: redis
host: os.environ/REDIS_HOST
port: 6379
ttl: 3600
リクエストのモデル名とメッセージが同一なら、2回目以降はプロバイダーを呼ばずにRedisから返ります。FAQ応答や定型の要約のように、同じ入力が繰り返し来る用途なら、これだけでも費用と遅延が目に見えて下がります。
もう一段踏み込むのがセマンティックキャッシュです。type: redis-semantic を指定すると、プロンプトを埋め込みベクトルにして類似度で照合します。表現が少し違う同じ質問にもヒットします。
litellm_settings:
cache: true
cache_params:
type: redis-semantic
host: os.environ/REDIS_HOST
port: 6379
similarity_threshold: 0.9
redis_semantic_cache_embedding_model: embed-small
ttl: 3600
注意点は2点あります。1つ目はRedis側の要件です。ベクトル検索にRediSearchが必要となるため、素のRedisではなく Redis Stack のイメージを使うのが無難でしょう。2つ目は閾値の設定です。
個人的には、セマンティックキャッシュは最初から全面適用しない ほうがいいと考えています。類似度が高くても、「請求書Aの合計は?」と「請求書Bの合計は?」のように、数字や固有名詞だけが違う質問は誤ヒットしがちです。ユーザー固有のデータを含むプロンプトでは、他人への回答を返す事故にもつながります。まずは完全一致で始め、社内ヘルプのように入力が公開情報だけの用途に限ってセマンティックを足す、という順序を勧めます。閾値は0.9前後の高めから始め、ヒットした内容を目視で確認しながら下げてください。
リクエスト単位で外したいときは、クライアントから cache パラメータで制御できます。
client.chat.completions.create(
model="chat-default",
messages=[{"role": "user", "content": "最新の障害状況を教えて"}],
extra_body={"cache": {"no-cache": True}},
)
レート制限・エラー時に即時切り替えるフォールバックルールの定義
フォールバックとリトライの設定は router_settings にまとめます。方針は、まず同じモデルグループ内でリトライし、それでも駄目なら別グループへ移ることです。
router_settings:
routing_strategy: simple-shuffle
num_retries: 2
timeout: 30
allowed_fails: 3
cooldown_time: 60
fallbacks:
- chat-default: ["chat-backup"]
context_window_fallbacks:
- chat-default: ["chat-backup"]
allowed_fails と cooldown_time の組み合わせは、失敗が続いたデプロイを一定時間ルーティング対象から外す仕組みです。障害中のエンドポイントに毎回タイムアウト分だけ待たされる状況を避けられます。context_window_fallbacks は、入力が長すぎてコンテキスト長を超えたときの逃がし先です。通常のフォールバックとは別に指定できます。
ルーティング戦略は simple-shuffle から始めれば十分です。レイテンシ重視の latency-based-routing や、TPM/RPMの余裕を見て振り分ける usage-based-routing-v2 もありますが、後者はRedisを共有してインスタンス間で使用量を揃える前提を要します。プロキシを複数台に増やす段階での検討で問題ありません。
正直に言うと、フォールバック先の設計はこの記事の設定よりずっと難しいです。プロバイダーが違えば、JSONモードの厳密さ、ツール呼び出しの引数の癖、システムプロンプトへの従い方が変わります。フォールバックで動いたはいいが、パース失敗が増えて結局アラートが鳴る、という話は十分起こり得ます。バックアップ側のモデルでも自前の評価セットを一度通し、出力形式が崩れないことを確認してから fallbacks に入れてください。
チーム単位のバーチャルキー発行とコスト・レート制限の集中管理
実プロバイダーのAPIキーをアプリに配る運用をやめ、ゲートウェイが発行するバーチャルキーを配ります。この機能にはPostgresが必要で、general_settings に database_url と master_key を設定します。
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
チームを作り、そのチームにキーを発行する流れです。
curl -X POST http://localhost:4000/team/new \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{"team_alias": "search-team"}'
curl -X POST http://localhost:4000/key/generate \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"team_id": "<上で返ってきたID>",
"models": ["chat-default", "embed-small"],
"max_budget": 50,
"budget_duration": "30d",
"rpm_limit": 60,
"tpm_limit": 100000
}'
models でアクセス可能なモデルグループを絞り、max_budget と budget_duration で月額上限、rpm_limit と tpm_limit で秒間の暴れ方を抑えます。予算を超えたキーはゲートウェイ側でエラーを返すので、あるチームの暴走が全体のクォータを食い潰す状況を防げます。
コスト追跡は、散乱していたロジックを消せる最大の利点です。レスポンスヘッダーにリクエスト単位のコスト(x-litellm-response-cost)が付き、キーやチームごとの支出は管理UIとAPIから確認できます。サービスごとにトークン数へ単価を掛けていたコードは、削除候補になります。ただし、単価表はLiteLLM側が持つ値です。新しいモデルや割引契約では請求額とずれる場合があるため、月次の請求書との突き合わせは残してください。
Docker Composeで構築する最小構成とクライアント側エンドポイントの移行手順
最小構成は3コンテナです。ゲートウェイ、キー管理用のPostgres、キャッシュ用のRedis Stackを並べます。
services:
litellm:
image: ghcr.io/berriai/litellm:main-stable
command: ["--config", "/app/config.yaml"]
ports:
- "4000:4000"
volumes:
- ./config.yaml:/app/config.yaml:ro
environment:
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
DATABASE_URL: postgresql://litellm:litellm@db:5432/litellm
REDIS_HOST: redis
OPENAI_API_KEY: ${OPENAI_API_KEY}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
depends_on:
- db
- redis
db:
image: postgres:16
environment:
POSTGRES_USER: litellm
POSTGRES_PASSWORD: litellm
POSTGRES_DB: litellm
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis/redis-stack-server:latest
volumes:
pgdata:
パスワードはローカル検証用の仮の値です。本番ではシークレット管理から注入してください。main-stable のようなタグは追従で挙動が変わりうるので、運用に入るときはバージョンを固定することを勧めます。docker compose up -d のあと、前節のバーチャルキーを使って疎通を確認します。
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer <バーチャルキー>" \
-H "Content-Type: application/json" \
-d '{"model":"chat-default","messages":[{"role":"user","content":"ping"}]}'
クライアント側の移行は、1サービスずつ段階的に進めます。変更するのはベースURLとキーだけです。
from openai import OpenAI
client = OpenAI(
base_url="http://litellm.internal:4000",
api_key="<バーチャルキー>",
)
手順としては、次の順序が安全です。
- 影響の小さい内部ツールを1つ選び、ベースURLとキーだけ差し替える
- アプリ内のリトライとフォールバックのコードを残したまま、ゲートウェイ経由で1週間動かす
- 管理UIでエラー率とコストを確認し、問題がなければアプリ側の重複ロジックを削除する
- 次のサービスに同じ手順を繰り返す
ゲートウェイは新たな単一障害点です。ここを軽視すると、プロバイダー障害に強くなった代わりにゲートウェイ障害で全停止するという本末転倒に陥ります。複数レプリカで動かす構成と、ヘルスチェックの設定は、本番投入前に済ませてください。まずは手元で docker compose up を実行し、1つのサービスを経由させるところから始めるのが現実的です。


