説明
Google の Gemini API がストリーミング応答の途中で失敗した場合、Discourse AI はその応答を完了したものとして扱います。AI 翻訳が有効な場合、失敗前に受信した断片が、投稿またはトピックタイトルの完成した翻訳として保存されます。エラーとして記録されることはなく、翻訳行が存在するため、バックフィルがその項目を再試行することはありません。
release/2026.7(2026.7.3、コミット f1caa6321287f918644fba9ff8943579fdcf027a)を実行しているセルフホスト環境で、バンドルされた discourse-ai プラグインを使用しており、LLM は Google プロバイダー経由の gemini-3.8-flash、サービスティアは flex に設定されていました。Flex は Google が負荷を分散させるティアであり、これらのストリーミング中の失敗を頻繁に発生させます。以下に記載する処理はティアに依存しません。
これらの場合、Google は HTTP 200 で応答し、1つ以上の通常イベントをストリーミングした後、同じストリーム内でエラーイベントを送信します:
data: {"candidates": [ ... ],"usageMetadata": { ... },"serviceTier": "flex","modelVersion": "gemini-3.8-flash","responseId": "..."}
data: {"error": {"code": 503,"message": "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.","status": "UNAVAILABLE"}}
監査ログには response_status 200 と小さな response_tokens 数が記録されます。保存された内容の例としては、917文字の投稿が26文字(挨拶行のみ)として保存され、リンクだけの投稿がURLの先頭8〜24文字として保存され、トピックタイトルが単語の途中で切れているものがあります。
このサイトで測定した結果、Flexティアの翻訳呼び出し394件のうち23件(5.8%)がこのように終了しました。同じモデルへの Standardティアの呼び出し952件中、該当するものは1件もありませんでした。
根本原因
DiscourseAi::Completions::Endpoints::Gemini#decode_chunkは、各解析済みストリームイベントからcandidatesのみを読み取ります。トップレベルのキーがerrorであるイベントはパーツを生成せず、チェックなしでスキップされます。DiscourseAi::Completions::Endpoints::Baseは、HTTP ステータスコードのみ(response.code.to_i != 200)に基づいて、成功、リトライ、失敗を判定します。ここではステータスが200であるため、503用の既存のリトライロジックには到達しません。- ストリームは正常に終了し、呼び出し元はそれまで蓄積されたテキストを受信します。
DiscourseAi::Translation::PostLocalizerとTopicLocalizerがそれを保存します。
2026-10-03時点の main ブランチにも同じコードが存在します。
提案される修正
ストリーミングされた Gemini イベント内のトップレベルの error オブジェクトを、失敗したコンプリーションとして扱う:部分出力を破棄し CompletionFailed を送出することで、503などのリトライ可能なコードに対して既存のリトライ処理が適用され、呼び出し元が断片を完全な応答として受信することを防ぎます。
再現方法
この失敗は Google の負荷に依存するため、強制的に発生させることはできません。Flexティアの Gemini モデルと AI 翻訳を使用しているサイトでは、影響を受けた呼び出しは後で監査ログから確認できます:
SELECT id, created_at, post_id, topic_id, response_status, response_tokens
FROM ai_api_audit_logs
WHERE feature_name = 'translation'
AND raw_response_payload LIKE '%data: {"error"%'
AND response_tokens > 0
ORDER BY created_at
各行は、HTTP 200 を返し、ある程度の出力を生成した後、エラーイベントを伴った呼び出しを表します。post_localizations または topic_localizations 内の対応する行には、切り詰められたテキストが保持されています。
回避策
翻訳エージェントを Standard サービスティアに移行することで、新たな発生が停止しました。保存された断片は上記のクエリで見つけて削除し、再翻訳する必要がありました。