あるプロバイダが、残高ゼロになっていることに、しばらく気づけませんでした。レート制限のリトライは実装してある。なのに、そのプロバイダだけ、待っても待っても回復しない。ログを見ると429ではなく402が返っていて、リトライ処理はそれを素通りしていました。HTTPの429はレート制限、これは4社共通の記号です。ところがその記号の内側で、正しい対処が社ごとに真逆になる。今回はその話です。この連載の前回で、エラーの種別フィールドが4社で四分五裂する話を書きました。今回はもう一段実際的な、「同じ429を受け取ったあと、何をすれば正しいのか」が割れる話です。
先に問いを置きます。あなたのリトライ処理は、429を見たら一律で待って再送していませんか。4社を束ねてみると、その「一律で待つ」が正解なのは、4社のうち1社だけでした。
OpenAI
いちばん素直でした。429が返ってきたら、それはレート制限。待って再送すればいい。Retry-Afterヘッダがあればその秒数だけ待つ。ここには裏がありません。4社を実装したあとで振り返ると、この「429を見たら待つ」がそのまま通じるのは、実はOpenAIくらいでした。基準点として置いておくと、他の3社の癖がくっきり見えてきます。
Claude
冒頭で書いた事故がこれです。Claudeでは、レート制限は429で来ますが、クレジット切れは402で来ます。402はHTTPでは「支払いが必要」を意味するコードで、レート制限とはまったく別の事象です。ところが呼び出す側の体感は似ています。どちらも「急に応答が返らなくなる」。429だけを見てリトライを組んでいると、402で来た残高ゼロを取りこぼし、回復しない相手を延々と待ち続けることになります。実装では、429と402を同列に見て、402のときは「待っても無駄、これは課金の問題」と判定を分けるようにしました。待てば直るものと、財布を開くまで直らないものを、コードで区別する必要があったわけです。

Gemini
Geminiの429は、429の中でもう一段、枠が分かれていました。無料枠には分あたりの上限と日あたりの上限が別々にあって、どちらを叩いても同じ429が返ってきます。つまり同じ429でも、分の枠を使い切っただけなら数十秒待てば回復し、日の枠を使い切っていたら翌日まで回復しない。回復までの時間が、桁で違う。429という記号は同じでも、その裏で枯れているのが分の枠か日の枠かで、取るべき行動が「少し待つ」か「今日はもう諦める」かに分かれます。ここは、エラーに添えられるstatus(前回書いたRESOURCE_EXHAUSTED)や本文の文言から、どちらの枠かを読み分ける必要がありました。
OpenRouter
OpenRouterの429は、そもそも意味が違いました。OpenRouterは複数の上流プロバイダへ橋渡しする仲介なので、ここで返る429は「あなたが叩きすぎた」ではなく「上流が混んでいる」ことが多い。だから待っても、あなたの番が回ってくるわけではありません。正しい対処は、待つことではなく、別のモデルへ逃がすことでした。特に無料枠のモデル(末尾に :free が付くもの)は上流の混雑を受けやすく、429が出たら同じモデルにしがみつくより、代替モデルへ切り替えるほうが速い。同じ429でも、OpenAIは「待て」、OpenRouterは「離れろ」。対処がちょうど裏返しになります。

Retry-Afterも、額面どおりには使えなかった
4社ともRetry-Afterヘッダには対応しました。サーバが「何秒後に再試行してよいか」を教えてくれる、あの仕組みです。ただ実装では、全社でRetry-Afterの値を is_numeric() に通してから使っています。素直に数値が入っている前提で書くと、返ってこないケースや、数値でない値が入っているケースで落ちる。これは想像ではなく、実際にそういう応答を踏んだから、数値かどうかを確かめる一枚をはさんだのだと、コードが物語っています。サーバが教えてくれる秒数すら、額面どおりには信じられませんでした。
1枚のレイヤーで、4通りの429をどう受けたか
まとめると、429という同じ記号に対して、4社の正しい対処はこう割れます。OpenAIは待って再送する。Claudeは402と併せて見て、課金の問題なら待たずに知らせる。Geminiは分の枠か日の枠かを見極めて、待つ時間を変える。OpenRouterは待たずに別モデルへ逃がす。この4通りを、呼び出す側からは「レート制限に当たったので、いい感じに対処してくれる」の一言で使えるように、レイヤーの内側へ押し込みました。前回の言い方を借りれば、ここでも差は消えたのではなく、1か所に集めて引き受けられています。
次の自分に渡すメモとして残します。複数のAPIを束ねるとき、HTTPステータスコードは「同じ番号なら同じ意味」ではありません。429の下には、性格の違う4つが同居していました。
- 待てば直るもの(OpenAI)
- 財布を開くまで直らないもの(Claudeの402)
- 時間の単位で回復が桁違いのもの(Geminiの分枠と日枠)
- そもそも自分のせいですらないもの(OpenRouterの上流混雑)
ステータスコードは入口の看板にすぎず、正しい対処はその奥のボディを読むまで決まらない。次回は、この「奥を読む」の別の場面、モデル一覧の取得で3社中1社だけ前提が崩れた話を書きます。
連載「4社のAIプロバイダを1枚のレイヤーで束ねた記録」:
第1回 AIチャットボットを4社対応にした話
第2回 error.messageは4社で揃っていた。なのに「何が起きたか」だけが揃わなかった
第3回 この記事


コメント