Azure OpenAI Batchがある日すべて500に — 入力ファイル500個の上限と、その400を隠した中間API
削除・期限設定・定期掃除のどれを選ぶか、そして4xxを潰さない書き方
先に結論。Azure OpenAI Batchを動かすコードに expires_after がなく、ファイルを消す処理もないなら、その機能はいつか全部止まる。この記事を読めば、いつ止まるかを計算でき、どの片付け方を選ぶか決められる。
会社(Railsで作った契約管理SaaS)で、契約書の項目をAIで一括抽出する機能が、ある朝から全組織で最初のチャンクから失敗するようになった。直すまで1週間近くかかった。Rails側のエラー監視に出たのはこの1行だけだった。
Batch create failed (500)
500だから抽出サーバーが落ちたんだろうと思った。違った。
本当の原因は一番下の層にあった
Batch APIは、リクエストを1つのファイルにまとめて上げると24時間以内に非同期で処理してくれるAPIだ。構成はRails Job → 抽出API(Python)→ Azure OpenAIで、契約を一定件数ずつ分けた塊(チャンク)ごとにバッチを1つ作る。そのたびにJSONL(1行に1リクエストのテキストファイル)の入力ファイルをFiles APIに上げ、終わったら出力ファイルを読む。ところが誰もファイルを消していなかった。
Azureは期限なしのバッチ入力ファイルをリソースあたり500個までしか受け付けない。上限を超えるとAzureは 400 quotaExceeded を返し、抽出APIはその例外を捕まえて500に変えてRailsへ渡した。
原因を見るには抽出API側のエラーまで開く必要があった。呼び出し側には400もquotaも残っていなかったからだ。
「特定の組織のデータの問題」に見えた理由
最初の失敗は数千件の実行で、半分ほど処理して止まった。ファイル数が上限に届いた瞬間、残りのチャンクが拒否されたからだ。これだけ見ると、その組織のデータのどこかがおかしいように見える。
実際はリソース全体で共有するカウンターの問題だった。その後はどの組織が押しても失敗し、みんな何度も押し直していた。ファイル一覧を数えると入力ファイルは501個。ドキュメントの500と1個ずれる理由はわからない。
自分のリソースはいつ止まるか
計算は単純だ。入力ファイルはバッチに1つなので 500 ÷ 月のバッチ数 か月。うちはリリースから2か月もたたずに止まった。
月のバッチ数 止まる時期
50 10か月後
100 5か月後
300 約1.7か月後
チャンクに分けて送るなら、ユーザーの1回の実行でファイルが複数できる。実行数ではなくチャンク数で数える。今の数はFiles APIの一覧で purpose がbatchのものを数えればわかる。
削除 vs 期限設定 vs 定期掃除
| 方式 | 得られるもの | 弱いところ | いつ使うか |
|---|---|---|---|
アップロード時に expires_after |
上限が10,000個に上がり、14〜30日後に消える | それまでファイルは残る。出力は別に指定が要る | デフォルトとして |
結果を読んだ後に files.delete |
機微な入力がすぐ消える | failed・expired・cancelled の経路を忘れやすい | 個人情報や契約書など、長く置きたくない入力 |
| 定期掃除ジョブ | すでに溜まった分も片付く | 実行中バッチのファイルを消さない確認が要る | 古いファイルの整理・安全網 |
自分ならまず期限を付ける。1行で済み、失敗経路で漏れることもない。入力が機微なら削除も足す。削除だけに頼るのは少し怖い。例外が1回出ればファイルが残り、それが数か月後の障害になるからだ。
batch_file = client.files.create(
file=open('chunk.jsonl', 'rb'),
purpose='batch',
extra_body={'expires_after': {'seconds': 1209600, 'anchor': 'created_at'}}, # 14日
)
batch = client.batches.create(
input_file_id=batch_file.id,
endpoint='/chat/completions',
completion_window='24h',
extra_body={'output_expires_after': {'seconds': 1209600, 'anchor': 'created_at'}},
)
数字は公式ドキュメントのものだ。seconds は1209600〜2592000(14〜30日)のみ。期限付き入力ファイルの上限10,000個を14日で割ると、1日約700個まで持つ。出力・エラーファイルはこの上限に入らないが、デフォルトの期限がなく、放っておくとずっと残る。だから output_expires_after も一緒に付ける。
もう1つ。すでに上げたファイルには期限が付いていないので、コードを直してもカウンターは下がらない(推測:ドキュメントに遡及の話はない)。古いファイルは自分で消す必要がある。
openai SDK 2.48.0をローカルのスタブサーバーにつないでリクエストを見ると、extra_body はフォームフィールド expires_after[seconds] で出ていった。ドキュメントのcurl例は expires_after.seconds だ。HTTPを直接呼ぶならドキュメントに従おう。本物のAzureで試し直したわけではない。
4xxを500に潰さない
ファイルの片付けは半分でしかない。今回時間を食ったのは、中間APIのこの形だった。
# 修正前:何が来ても500
try:
upload()
except Exception:
raise HTTPException(500, 'Batch create failed')
# 修正後:上位サービスのステータスとコードを渡す
try:
upload()
except openai.APIStatusError as e:
raise HTTPException(e.status_code, {'code': e.code, 'message': e.message})
上限3個のスタブで動かすと、前者は HTTP 500、後者は 400 quotaExceeded を返す。呼び出し側がこのコード1つを受け取れていれば、調査は数分で終わったはずだ。ただし上位の401などをそのまま渡すと、呼び出し側は自分の認証の問題だと誤解する。そういうものは502に変え、元のコードだけ本文に載せるほうがいい。
あの日の応急処置
順番が大事だ。まずバッチがすべて completed で、アプリ側に結果待ちの抽出がないことを見た。次に入力ファイルを消し、反映済みの出力ファイルを消した。合わせて千個あまり。失敗した抽出の出力は調査用に残した。最後に本番コンテナからテストファイルを上げ、成功を見てすぐ消した。
見落としやすいのは、止まっている間に失敗した抽出は自動では再実行されないこと。画面から押し直す必要があるので、影響を受けた組織に知らせる。
OpenAI本家(Azureではない方)に同じ上限があるかは確認できていない。ドキュメントのページを読めなかったので、自分で見てほしい。Files APIに上げない道もある。AzureはBlob Storageを使うBatchを別に案内している。
動いていたAI機能がこんなふうにある日止まって原因が見えないなら、negabaroに聞いてもらってかまわない。層を一つずつ下りて、一緒に原因を探せる。
キーコンセプト
`500 ÷ 月のバッチ(チャンク)数` で止まる時期を計算する
アップロードに `expires_after`、バッチ作成に `output_expires_after` を付ける
溜まったファイルは実行中のバッチがないのを見てから消す
中間APIは上位の4xxのステータスとコードを呼び出し側へ渡す