Firebase AI LogicのStructured OutputがHTTP 400になるまで:maxItemsと非公開の複雑度制限を切り分けた記録
はじめに
ブラウザアプリからFirebase AI Logic経由でGeminiを呼び出し、音声分析結果をJSONとして受け取る機能を実装しました。
認証、App Check、課金、クォータ、音声送信までは正常に動作したものの、本番用のresponse schemaを指定したリクエストだけが、次のエラーで失敗しました。
HTTP 400 INVALID_ARGUMENT Request contains an invalid argument.
最終的に、原因はJSON Schemaの構文誤りではなく、配列に指定していた大きなmaxItems値でした。Firebase AI Logicへ送るschemaのmaxItemsを最大4に制限すると、同じ完全なproduction schemaが成功しました。
この記事では、なぜ通常のテストでは見つからなかったのか、どのように原因を切り分けたのか、そしてローカルのデータ契約を弱めずに修正した方法を紹介します。
システム構成
対象は、ブラウザ上でゲーム用サウンドを編集するTypeScript/Viteアプリです。AI機能では、編集した音声を短いWAVに変換し、Geminiに分析させます。
実APIへのアクセスにはAPIキーを直接使用せず、Firebase AI Logicを利用しています。リクエストの成立には、次の条件が必要です。
- Firebase Authentication
- Email/Password認証
- アプリ側の許可UID判定
- reCAPTCHA Enterprise App Check
- Firebase production mode
- Firebase AI Logic
- 対象Geminiモデル
この構成には、APIキーをブラウザへ直接置かず、不正利用を抑制できる利点があります。一方で、Firebase AI LogicのEmulatorが存在しないため、Auth、App Check、Firebaseのproxy、Geminiのschema検証をまとめて再現するには、本番相当の環境が必要になります。
最初はHTTP 429だった
当初のリクエストはHTTP 429を返していました。通常のRPMやTPM上限には達していませんでしたが、Google AI Studio側の前払いクレジット不足が原因でした。
クレジットを補充すると429は解消し、次はHTTP 400へ変化しました。この時点で、少なくとも次のことが分かりました。
- 認証経路はGeminiまで到達している
- 課金・残高によるブロックは解消している
- 新しい問題はリクエスト内容にある
同じ「AIが利用できない」というUI表示でも、429と400では原因がまったく異なります。エラーを安全な分類と数値HTTP statusに分けて記録することが重要でした。
MockやEmulatorでは再現しなかった
以下のテストはすべて成功していました。
- TypeScript strict modeのtypecheck
- ESLint
- Vitest
- Firebase Authentication/Firestore/Storage Emulator
- Mock AIを使ったPlaywright
- production bundle検査
これらはアプリ側の型、制御フロー、UI、Firestore Rules、監査ログ、privacy boundaryを検証できます。しかし、実際のGeminiモデルがresponse schemaを受理するかどうかは検証できません。
つまり、問題は「本番Hostingでしか発見できない」のではありません。正確には、「実Firebase AI Logicと実Geminiを使う統合環境がなかったため、既存のテストでは発見できなかった」という状態でした。
一度にschemaを変更しない
Googleから返るメッセージは、Request contains an invalid argument.だけでした。どのfieldが原因かは示されません。
そこで、production schemaを段階的に組み立てる診断を用意しました。
最初の診断は次のような構成です。
| Stage | 検証内容 | 結果 |
|---|---|---|
| A | 基本テキスト | 成功 |
| B | System Instruction | 成功 |
| C | 0.1秒の無音WAV | 成功 |
| D | 最小response schema | 成功 |
| E | scalar fields | 成功 |
| F | nested object | 成功 |
| G | string enum | 成功 |
| H | scalar array | 成功 |
| I | object array | 成功 |
| J | 完全production schema | HTTP 400 |
これにより、Auth、App Check、音声、System Instruction、structured outputそのものは原因から除外できました。
診断は次の原則で実装しました。
- 成功したStageはlocalStorageへcheckpointを保存する
- resetされるまで成功Stageを再送しない
- 失敗・未実行Stageは何度でも実行できる
- 一括実行は最初の失敗で停止する
- 2 requests/minuteなど既存のclient rate limitを守る
- prompt、WAV、Base64、raw response、token、keyはログへ保存しない
- 監査にはStage、schema分類、成功可否、安全なerror code、HTTP statusだけを残す
schemaをさらに分割した結果
production schemaを、知覚評価、観察項目、recommendationsへ分けました。
| 検証内容 | 結果 |
|---|---|
| 8項目のperceptual assessment | 成功 |
| observationのfield構造 | 成功 |
| 大きなcategory enum | 成功 |
| recommendation基本構造 | 成功 |
targetSectionsを含むrecommendations |
成功 |
| 完全observations | HTTP 400 |
| 完全production schema | HTTP 400 |
個々のobject、enum、nested arrayは受理されています。しかし、productionのobservationsを組み立てると失敗します。
次に、構造を変えず、配列上限だけをproduction値へ変更しました。
成功する診断では、次の値を使っていました。
{ "strengths.maxItems": 4, "issues.maxItems": 4, "evidence.maxItems": 4 }
一方、production schemaは次の値でした。
{ "strengths.maxItems": 24, "issues.maxItems": 24, "evidence.maxItems": 12, "recommendations.maxItems": 20, "warnings.maxItems": 20 }
構造を維持したままproduction上限へ切り替えたStageは、単独でもHTTP 400になりました。逆に、完全なproduction schemaのすべてのmaxItemsを最大4にすると成功しました。
これにより、少なくともこのモデルとFirebase AI Logic経路では、大きなmaxItems値がschema complexityの判定に影響していたと確認できました。
公式ドキュメントから分かったこと、分からなかったこと
Firebase AI Logicの公式資料では、maxItemsはresponse schemaのサポート対象として掲載されています。
Geminiの公式資料にも、structured outputはJSON Schemaのsubsetをサポートし、maxItemsを利用できると記載されています。
また、schemaが大きい、深くネストされている、制約が多い場合にはAPIが拒否する可能性があり、schemaを単純化するよう案内されています。
しかし、次の情報は公開されていません。
maxItemsの実質的な上限- 複数の配列やnested objectを含む場合の複雑度計算
maxItems: 24が拒否され、4なら成功するという閾値- モデルごと、Firebase AI Logic経路ごとの差
- HTTP 400になった具体的なfield
したがって、これは公式ドキュメントの単純な見落としではありません。「schema complexity」という抽象的な制限を、実際の環境で段階的に測定する必要がある問題でした。
修正方法
ローカルschemaの上限をすべて4へ変更すると、アプリ内部のデータ契約まで変わってしまいます。そこで、Firebaseへ送信するschemaだけを縮小しました。
概念的には次のような変換です。
function firebaseResponseSchema(
schema: Record<string, unknown>,
options: { maxItemsCap?: number } = {},
): Record<string, unknown> {
return visitSchema(schema, (key, value) => {
if (
key === "maxItems" &&
typeof value === "number" &&
options.maxItemsCap !== undefined
) {
return Math.min(value, options.maxItemsCap);
}
return value;
});
}
audio analysis/refinement経路だけ、次のように指定します。
firebaseGenerationConfig(config, responseSchema, {
maxItemsCap: 4,
});
この結果、境界は次のようになりました。
ローカルschema/parser strengths/issues: 最大24 recommendations/warnings: 最大20 evidence: 最大12 Firebase AI Logicへ送るschema すべてのmaxItems: 最大4
Geminiの出力件数は4に抑えられますが、ローカルのvalidatorは従来の上限を維持します。import済みデータや将来の別経路まで4件へ制限する必要はありません。
また、この変換はaudio AIに限定しました。正常に動作している通常のテキストAI schemaへ、診断由来の制約を不用意に広げないためです。
修正後の結果
修正後、次の条件を満たす完全production schemaが成功しました。
- Firebase Authentication
- 許可UID
- App Check
- Firebase AI Logic
- 音声入力
- System Instruction
- nested object
- string enum
- object array
- recommendations
- 全
maxItemsを最大4へ縮小した完全response schema
上限を縮小していない診断は引き続きHTTP 400になり、同じ構造を上限4で送る最終診断だけが成功しました。この比較により、修正と成功の因果関係も確認できました。
学んだこと
1. 「サポートされているfield」と「任意の値が受理される」は別
maxItemsは公式にサポートされています。しかし、大きな値を含むschemaが必ず受理されるわけではありません。
2. structured outputの制限は、出力サイズだけではない
maxItemsは実際に24件を生成する指示ではなく上限ですが、その値自体がschema complexityへ影響する場合があります。
3. genericなHTTP 400にはdelta debuggingが有効
schema全体を推測で書き換えるより、成功する最小構造へfieldを一つずつ追加するほうが確実です。
4. 成功checkpointが試行回数を減らす
成功Stageを毎回再実行しないことで、rate limit、費用、待ち時間を抑えられました。
5. 本番ユーザー向けHostingと実API診断環境は分離すべき
今回の問題は実Geminiを呼ばなければ再現できません。しかし、本番ユーザー向けサイトで試行錯誤する必要はありません。App CheckとAuthを維持したFirebase Hosting preview channelなど、本番相当の診断環境を用意するのが次の改善です。
6. ログは詳細であるほどよいとは限らない
音声やpromptを保存すれば調査しやすくなりますが、privacy boundaryを破ってしまいます。Stage、schema分類、HTTP statusだけでも、適切に実験を設計すれば原因を絞り込めます。
まとめ
今回のHTTP 400は、課金、クォータ、認証、App Check、音声形式、enum、nested object、recommendationsの問題ではありませんでした。
原因は、Firebase AI Logicへ送ったproduction response schemaの大きなmaxItems値でした。
同じschema構造 + productionの大きなmaxItems → HTTP 400 同じschema構造 + maxItemsを最大4へ制限 → 成功
公式ドキュメントからschema complexityの可能性は予測できますが、具体的な閾値は分かりません。実環境で安全な段階診断を行い、成功するschemaとの差分を測定する必要がありました。
Structured Outputで説明のないHTTP 400に遭遇した場合は、fieldの対応可否だけでなく、property数、nesting、enum数、required数、そしてmaxItemsなどの制約値も含めて、schemaを段階的に縮小してみる価値があります。