About

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へ渡した。

The error loses its cause on the way back upAzure OpenAI400quotaExceededthe real cause is hereExtraction APIexcept Exception500status and message droppedRailsBatch createfailed (500)looks like our server brokeInput files without an expiry: 500 per resource. Nobody deleted them.The counter belongs to the whole resource, so every organization fails at once.

原因を見るには抽出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に聞いてもらってかまわない。層を一つずつ下りて、一緒に原因を探せる。

キーコンセプト

1

`500 ÷ 月のバッチ(チャンク)数` で止まる時期を計算する

2

アップロードに `expires_after`、バッチ作成に `output_expires_after` を付ける

3

溜まったファイルは実行中のバッチがないのを見てから消す

4

中間APIは上位の4xxのステータスとコードを呼び出し側へ渡す

ユースケース

Azure OpenAI Batchで文書・契約書を一括処理する機能 外部APIを包む内部APIのエラー伝達の設計
← 構造は回路図、重みはつまみの位置 — そしてAttentionの変形はn²を避けるための改造だ AIに本番DBを見せるとき — 書き込みはプロンプトではなくDB権限で止める →