📝 この記事は forge.workstyle.tech に掲載した記事の転載です。
LiteLLM で無料枠の LLM API を7社束ねて、記事生成のパイプラインを動かしています。「1社が落ちても別の社に流れるから止まらない」という構成です。
半年ほど運用して、あるとき成功率を測ったら 10回中6回でした。
原因を追いかけた結果、フォールバックには効く失敗と効かない失敗があり、効かない失敗を放置していたことが分かりました。この記事は、その調査と復旧の記録です。数値はすべて 2026-09-07 の実測です。
何を作っているか
やりたいことは単純で、「無料枠を持つ複数のプロバイダを、1つのモデル名の裏に隠す」ことです。
呼ぶ側は model: "free" としか書きません。
curl http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer sk-local-xxx" \
-H "Content-Type: application/json" \
-d '{"model":"free","messages":[{"role":"user","content":"要約して"}]}'
裏では LiteLLM が、同じ model_name: free を持つ複数のエントリから1つを選んで投げます。
model_list:
- model_name: free
litellm_params:
model: groq/qwen/qwen3.8-27b
api_key: os.environ/GROQ_API_KEY
- model_name: free
litellm_params:
model: gemini/gemini-flash-latest
api_key: os.environ/GEMINI_API_KEY
- model_name: free
litellm_params:
model: cloudflare/@cf/meta/llama-3.3-70b-instruct-fp8-fast
api_key: os.environ/CLOUDFLARE_API_KEY
# …以下同様に、束ねたいだけ並べる
呼び出し側のコードはプロバイダを一切知りません。ここが LiteLLM の一番おいしいところで、プロバイダの入れ替えが設定ファイルだけで済みます。実際この記事の最後に出てくる復旧作業も、アプリ側のコードは1行も触っていません。
router_settings が実際に何をしているか
束ねただけでは切り替わりません。挙動は router_settings で決まります。実際に使っている設定がこれです。
router_settings:
routing_strategy: simple-shuffle # 使える中からシャッフルで選ぶ
num_retries: 5 # 失敗したら最大5回まで再試行
allowed_fails: 1 # 1回失敗したらそのプロバイダを冷却対象に
cooldown_time: 60 # 冷却は60秒。明けたら自動で復帰
litellm_settings:
drop_params: true # プロバイダが対応しないパラメータを自動で落とす
各項目の意味を、実運用で効いた順に書きます。
num_retries が実質的なフォールバックの本体です。1回目が失敗したら、LiteLLM は同じ model_name の別エントリを選んで投げ直します。5回設定してあれば、5社まで試してくれます。
allowed_fails と cooldown_time は、死にかけたプロバイダを一時的に外す仕組みです。1回失敗したら60秒間そのプロバイダを候補から外し、60秒経ったら黙って戻します。レート制限(429)はまさにこの形の失敗なので、相性がいい。
drop_params は地味ですが無いと困ります。無料モデルは対応パラメータがバラバラで、たとえば seed や response_format を受け付けないモデルに投げると 400 で落ちます。これを自動で落としてくれるので、呼ぶ側が最大公約数を気にせずに済みます。
routing_strategy: simple-shuffle は「生きている候補からランダムに選ぶ」です。無料枠は各社のレート制限が薄いので、負荷を散らす意味でもシャッフルが向いています。レイテンシ優先で選ぶ latency-based-routing もありますが、無料枠では速い社に負荷が寄って先に枯れるので使っていません。
失敗:6社構成が、実質1社で動いていた
さて本題です。この構成で同じリクエストを10回投げた結果がこれでした。
成功 6 / 失敗 4
失敗したときのエラーがこれです。
litellm.APIError: APIError: CerebrasException -
Payment required to access this resource. Visit your billing tab.
Received Model Group=free
Available Model Group Fallbacks=None
最後の一行に目が留まりました。
Available Model Group Fallbacks=None
6社束ねているのに、フォールバック先が無いと言っている。
各社の API を個別に叩いて生死を確認したら、理由が分かりました。
| プロバイダ | 結果 |
|---|---|
| Groq | ✗ 401 Invalid API Key |
| Cerebras | ✗ 402 Payment required |
| OpenRouter | ✗ 401 API key expired |
| Gemini | ✗ 404 model no longer available |
| Mistral | ✗ 429 Rate limit exceeded |
| Cohere | ○ 200 |
生きていたのは1社だけでした。
10回中6回成功していたのは、num_retries: 5 がリトライを繰り返して、最終的に Cohere に着地させていたからです。冗長性があるように見えて、実際は1社に全負荷が乗っていました。しかも成功したときも、裏で何回か失敗してから届いているので、その分だけ遅い。
原因:フォールバックしない失敗がある
ここが一番の学びでした。
リトライやフォールバックが想定しているのは、待てば直る失敗です。
- 429(レート制限)… 60秒待てば枠が戻る
- タイムアウト / 5xx(一時的な障害)… 投げ直せば通る
これらは cooldown_time で外して、時間が経ったら戻す、という扱いが理にかなっています。
一方で、待っても直らない失敗があります。
- 402(課金が必要)… 無料枠が終了した。待っても戻らない
- 404(モデルが存在しない)… モデル名が変わった/提供終了。待っても戻らない
- 401(キーが無効)… キーが失効した。待っても戻らない
これらはリトライの対象になりません。 当たった瞬間に、そのリクエストは失敗として返ります。つまり、
死んだプロバイダをプールに残しておくと、確率どおりに失敗を生み続ける。
6社中1社が恒久的に死んでいれば、simple-shuffle が均等に選ぶ以上、約1/6のリクエストが落ちます。「たくさん束ねてあるから安心」は、全社が生きている前提でしか成り立ちません。
これは LiteLLM の不具合ではなく、そもそもリトライで解決できない種類の失敗を、リトライに任せていた設計の問題でした。
復旧:何が効いたか
直した順に、成功率がどう動いたかを記録してあります。
① モデル名を最新に直した
Groq はキーを再発行したらエラーが 401 から 404 に変わりました。
404 The model `llama-3.3-70b-versatile` does not exist or you do not have access to it.
無料枠は健在で、モデルのほうが消えていました。 現在の Groq のカタログには Llama 系が1つも残っていません。qwen/qwen3.8-27b に差し替えました。
Gemini も同様です。
404 NOT_FOUND: This model models/gemini-2.0-flash is no longer available.
Please update your code to use models/gemini-3.6-flash
こちらは gemini-flash-latest というエイリアスに変更しました。次の世代交代に自動で追従するので、同じ失敗を繰り返しません。エイリアスが提供されているプロバイダでは、無料枠の運用に限ってはこちらのほうが安全です。
この2つを直した時点で測り直したら、12回中6回。50%で、ほぼ改善していません。
Groq・Gemini のモデル名を修正しただけ 6/12(50%)← 失敗は全部 Cerebras の402
② 死んだプロバイダをプールから外した
Cerebras は無料枠が終了して 402 を返すようになっていました。キーは有効で、認証は通り、課金を求められる状態です。
これをコメントアウトして外しました。
# 2026-09-07: Cerebras は無料枠が終了し 402 Payment required を返すため無効化。
# 402/404 は LiteLLM のリトライ対象外(=フォールバックせず即失敗)なので、
# 死んだプロバイダを残しておくとプール全体の失敗率がそのまま上がる。
# - model_name: free
# litellm_params:
# model: cerebras/gemma-4-31b
# api_key: os.environ/CEREBRAS_API_KEY
結果はこうなりました。
Cerebras を除外 15/15(100%)
モデル名の修正より、死んだ1社を外すほうが効きました。 これが今回一番の収穫です。
③ 生きているプロバイダを足した
そのあと Cloudflare Workers AI、HuggingFace、NVIDIA NIM を追加して、最終的に7社構成になりました。
成功率の推移: 6/10(60%) → 15/15 → 20/20 → 24/24 → 28/28
もう1つの罠:推論モデルを混ぜると静かに壊れる
プロバイダを足すときに、もう1つ気をつけることがあります。
思考過程を出すタイプのモデル(推論モデル)を混ぜると、エラーにならずに空の応答が返ります。
同じ「2+2は?数字だけ答えて」を投げた結果です。
| モデル | content |
|---|---|
qwen/qwen3.8-27b |
"4" |
qwen/qwen3.6-27b |
"" ← 空
|
openai/gpt-oss-20b |
"" ← 空
|
nvidia/nemotron-3-super-120b-a12b |
"User asks 2+2..." ← 思考が漏れている
|
答えが reasoning_content 側に入ってしまい、content が空で返ります。HTTP は 200 なので、リトライもフォールバックも作動しません。choices[0].message.content だけを読んでいるコードは、静かに空文字を受け取ります。
マルチターンのエージェント用途ではもっと分かりやすく壊れます。会話履歴に reasoning_content を積んで送り返すため、次のターンで弾かれます。
400 property 'messages.*.assistant.reasoning_content' is unsupported
厄介なのは、モデル名の系列では判断できないことです。上の表のとおり、同じ Qwen でも 3.6 は推論を出し、3.8 は出しません。
なので、プールに入れる前に1回叩いて、reasoning_content が付かないことを確認しています。設定ファイルにもその旨を書き残しました。
# ※このプールは【単発生成専用】。推論モデルを含むため、
# マルチターンのコーディングエージェントには使わないこと。
現在の構成と実測レイテンシ
最終的にこうなりました。すべてクレジットカード不要です。
| プロバイダ | モデル | 実測 |
|---|---|---|
| Groq | qwen/qwen3.8-27b |
220 ms |
| Cloudflare Workers AI | @cf/meta/llama-3.3-70b-instruct-fp8-fast |
570 ms |
| Cohere | command-a-03-2025 |
748 ms |
| HuggingFace | meta-llama/Llama-3.3-70B-Instruct |
937 ms |
| OpenRouter | google/gemma-4-26b-a4b-it:free |
957 ms |
| NVIDIA NIM | google/diffusiongemma-26b-a4b-it |
1,070 ms |
| Gemini | gemini-flash-latest |
— |
| Mistral | mistral-small-latest |
429 頻発 |
追加するときに詰まった点を2つだけ書いておきます。
Cloudflare Workers AI は、API トークンの権限が紛らわしいです。必要なのは Account > Workers AI > Read の1つだけで、Edit は要りません。逆に、名前が似ている AI Gateway の権限では通りません。
A token that holds only an AI Gateway permission returns 401 with error code 10000.
HuggingFace は、トークンの権限に inference.serverless.write(UI 上は "Make calls to Inference Providers")が必要です。リポジトリ読み取り権限だけだと 403 になります。キーを発行できたことと、キーが使えることは別でした。
まとめ:束ねる前に、死活監視を作る
同じ構成を組む人に向けて、実務的な結論を3つ書きます。
1. プロバイダを増やすより、死んだプロバイダを検知して外すほうが効く。
今回、モデル名の修正(6/12)より、死んだ1社を外すこと(15/15)のほうが成功率への寄与が大きかったのが答えです。各社に1リクエスト投げて 200 が返るか見るだけのスクリプトで足ります。束ねる仕組みより先に、これを作るべきでした。
2. フォールバックが効く失敗と、効かない失敗を分けて考える。
効くのは 429 とタイムアウト。402(有料化)・404(モデル消滅)・401(キー失効)には効きません。 これらは人間が気づいて設定から外すまで、失敗を生み続けます。ログに Available Model Group Fallbacks=None が出ていたら、たいていこれです。
3. モデル名は固定せず、入れる前に1回叩く。
latest 系のエイリアスがあるなら使う。無いなら、プールに入れる前に 応答が返ることと reasoning_content が付かないことを確認する。同じシリーズでもバージョン違いで挙動が変わります。
注意: この記事の数値はすべて 2026-09-07 時点の実測です。無料枠の条件・提供モデル・レート制限は数か月で変わります。導入時には各社の公式ドキュメントで最新の状態をご確認ください。