メインコンテンツへスキップ

Nano Banana 2のblockReason OTHER:意味と空応答の処理方法

15 分で読めますAPIトラブルシューティング

Nano Banana 2のblockReason OTHERは、原因が特定されていない入力ブロックを表します。特定のポリシー違反と決めつけず、応答JSONと候補の終了理由を確認し、空の結果を成功扱いしない処理を実装しましょう。

OTHERが返った応答JSONを拡大して確認し、候補と画像データを調べる開発者

Nano Banana 2で promptFeedback.blockReason: "OTHER" が返った場合、公式の定義は理由不明の入力ブロックです。OTHER という値だけでは、著作権、人物、地域制限などの具体的な原因を特定できません。まず応答全体を保存し、入力に対するフィードバックと、生成候補の終了理由を別々に確認してください。GoogleのBlockReasonリファレンス

この記事は、Gemini APIの generateContent が返すJSON を扱います。Nano Banana 2の現在のモデルIDは gemini-3.1-flash-image です。現行の公式画像生成ガイドにはInteractions APIの例も掲載されていますが、そちらの応答を以下の candidates 用コードにそのまま渡すことはできません。最初に、実際に呼び出したエンドポイントとモデル名を確認しましょう。

blockReasonfinishReason はどこが違うのか

同じ OTHER でも、JSON内の場所が違います。

promptFeedbackのblockReasonと候補のfinishReasonを別々のJSON例で示す図

確認する場所対象OTHER から分かること
promptFeedback.blockReason入力に対するフィードバック入力がブロックされたが、具体的な理由は不明
candidates[i].finishReason各生成候補の終了理由その候補の生成が終了した理由は不明

Python SDKでは、対応する属性が prompt_feedback.block_reasoncandidate.finish_reason のように書かれます。RESTのJSONキーとSDKの属性名を混ぜると、存在する値を読み落とすことがあります。

以下は入力ブロックを説明するためのJSON例です。実際の問い合わせで取得したログではありません。

json
{ "promptFeedback": { "blockReason": "OTHER" }, "candidates": [] }

公式の PromptFeedback の説明では、ブロック理由が設定された場合、候補は返されません。SDKの表現によっては候補が空リストではなく None になることも想定して処理します。一方、次の説明用JSONでは候補が存在し、その終了理由が OTHER です。

json
{ "candidates": [ { "finishReason": "OTHER", "content": { "parts": [] } } ] }

後者を「著作権が検出された」と読み替える根拠はありません。finishMessagesafetyRatings が付いていれば併せて読み、付いていなければ原因不明として記録します。FinishReasonの公式定義

混同しやすい値

次の表は画像の失敗を調べる際に区別したい値の抜粋です。すべての終了コードを網羅する表ではありません。

値と場所読み方・確認事項
blockReason: SAFETYpromptFeedback.safetyRatings を確認する。返されたカテゴリや判定を記録する
blockReason: BLOCKLIST用語のブロックリストに関する理由。OTHER と同じものにまとめない
blockReason: PROHIBITED_CONTENT禁止コンテンツに関する理由。具体的な表示を確認する
BLOCK_REASON_UNSPECIFIEDBlockReasonの未指定値。BLOCKED_REASON_UNSPECIFIED ではない
finishReason: STOP自然な停止点、または指定した停止シーケンスで終了。画像があることは別途確認する
finishReason: NO_IMAGE画像が期待されたが生成されなかった
finishReason: IMAGE_SAFETY生成画像の安全性に関する停止理由
finishReason: OTHER / IMAGE_OTHERそれぞれ別の値。具体的な隠れた原因を補わず、そのまま保存する

HTTP 200は応答を受け取れたことを示すだけで、画像ファイルの完成を保証しません。STOP があっても、候補にテキストだけが入っている、画像データが空、受信側が画像パートを読み飛ばしている、といった状況を切り分ける必要があります。

先に保存する情報と、切り分ける順番

設定を変える前に、失敗した一回の情報をまとめます。時刻とタイムゾーン、呼び出し先、指定モデル名、SDKを使っていればパッケージ名とバージョン、HTTPステータス、応答本文を保存してください。応答の responseIdmodelVersionusageMetadata も、あれば残します。

そのうえで次の順に確認します。

  1. HTTPエラーか、生成結果の問題か。 400、401、403、429、503などが返っている場合は、まずそのエラー本文を読みます。503なら過負荷エラーの確認手順へ進みます。HTTPエラーをすべて OTHER に置き換えないでください。
  2. 入力がブロックされたか。 promptFeedback.blockReason を見ます。SAFETY の場合は安全性評価も確認し、OTHER の場合は理由を推定で埋めません。
  3. 候補があるか。 欠落、null、空配列を扱います。候補がなければ先頭要素を読まず、画像なしとして処理します。
  4. 各候補がどう終了したか。 最初の候補だけでなく全候補の finishReason を記録します。内容が存在することと、利用できる完成画像があることを分けます。
  5. 実際に画像を開けるか。 content.parts にある画像パートのMIMEタイプ、Base64、復号後のバイト数を確認し、画像デコーダーで読み込みます。

一般的な「200 OKなのに画像がない」問題は、無画像応答の切り分けも参照してください。会話の二回目以降で署名エラーが出る場合は、thought signatureの扱いが別の確認対象です。

空候補や壊れた画像を成功扱いしないPython例

候補、MIME、Base64、画像の読み込みを確認して結果を分類する手順

次のコードは、保存した 非ストリーミングのREST応答JSON を解析する例です。通信、再試行、別モデルへの切り替えは行いません。画像を使ってよいかの判定と、調査用の情報を取り出す処理に絞っています。

python -m pip install Pillow で画像の読み込みに使うライブラリを用意し、コードを inspect_response.py として保存します。以下の例で対応する画像はPNG、JPEG、WebPです。MIMEタイプを名乗るだけのデータを通さず、Pillowの画像読み込みで実体も確認します。

python
import base64 import binascii import io import json import sys from pathlib import Path from PIL import Image, UnidentifiedImageError FORMATS = {"image/png": "PNG", "image/jpeg": "JPEG", "image/webp": "WEBP"} def as_dict(value): return value if isinstance(value, dict) else {} def as_list(value): return value if isinstance(value, list) else [] def inspect_response(response): if not isinstance(response, dict): raise ValueError("応答の先頭はJSONオブジェクトである必要があります") feedback = as_dict(response.get("promptFeedback")) block = feedback.get("blockReason") report = { "status": "no_image", "response_id": response.get("responseId"), "model_version": response.get("modelVersion"), "usage": response.get("usageMetadata"), "block_reason": block, "prompt_safety_ratings": as_list(feedback.get("safetyRatings")), "candidates": [], "images": [], } if response.get("error"): report["status"] = "api_error" return report if block and block != "BLOCK_REASON_UNSPECIFIED": report["status"] = "prompt_blocked" return report candidates = as_list(response.get("candidates")) for ci, raw_candidate in enumerate(candidates): candidate = as_dict(raw_candidate) reason = candidate.get("finishReason") entry = { "index": ci, "finish_reason": reason, "finish_message": candidate.get("finishMessage"), "safety_ratings": as_list(candidate.get("safetyRatings")), "image_issues": [], } report["candidates"].append(entry) content = as_dict(candidate.get("content")) for pi, raw_part in enumerate(as_list(content.get("parts"))): part = as_dict(raw_part) if part.get("thought"): continue blob = as_dict(part.get("inlineData")) if not blob: continue mime = blob.get("mimeType") encoded = blob.get("data") problem = None data = b"" if not isinstance(mime, str) or mime not in FORMATS: problem = "unsupported_mime" elif not isinstance(encoded, str) or not encoded: problem = "empty_or_missing_data" else: try: data = base64.b64decode(encoded, validate=True) if not data: raise ValueError("empty bytes") with Image.open(io.BytesIO(data)) as img: actual_format = img.format img.load() if actual_format != FORMATS[mime]: raise ValueError("MIME does not match image format") except (ValueError, binascii.Error, OSError, UnidentifiedImageError, Image.DecompressionBombError): problem = "invalid_image_data" if problem: entry["image_issues"].append({"part": pi, "reason": problem}) elif reason == "STOP": report["images"].append({ "candidate": ci, "part": pi, "mime_type": mime, "data": data }) else: entry["image_issues"].append({ "part": pi, "reason": "finish_reason_requires_review" }) if report["images"]: report["status"] = "image_ready" elif any(c["finish_reason"] not in (None, "STOP", "FINISH_REASON_UNSPECIFIED") for c in report["candidates"]): report["status"] = "candidate_stopped" return report if __name__ == "__main__": response = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) result = inspect_response(response) # バイナリや全文ログを端末に出さず、確認用の概要だけ表示する。 print(json.dumps({ "status": result["status"], "block_reason": result["block_reason"], "finish_reasons": [c["finish_reason"] for c in result["candidates"]], "image_count": len(result["images"]), "image_issues": [c["image_issues"] for c in result["candidates"]], }, ensure_ascii=False, indent=2))

保存済みの応答を python inspect_response.py response.json で確認します。前述の入力ブロック例なら prompt_blocked、終了理由が OTHER の候補だけなら candidate_stoppedSTOP でも画像がなければ no_image です。images に入るのは、この例の条件を満たして読み込めた画像のバイト列です。

このコードでは、終了理由が STOP でない候補の画像は自動採用せず、確認が必要な結果として扱います。これはこの記事の実装方針であり、Googleの全用途に共通する成功定義ではありません。後続候補に利用可能な画像があれば取得しつつ、ほかの候補の終了理由や画像エラーも残します。

空候補、nullcontentparts、空の画像データ、非画像MIME、不正なBase64、MIMEと実体の不一致、複数候補を人工データで確認しています。これは受信処理のオフラインでの検証です。実際のGemini APIでブロックが解除されたことや、課金の有無を確認した実験ではありません。

「空配列だからSDKが無限に止まる」とは限らない

通常のPythonで [][0] を実行すると IndexError になります。None に対して len() を使えば TypeError です。まずローカルの例外と、ネットワーク待機、SDK内部の挙動を分けてください。Pythonの例外リファレンス

SDKが止まっている疑いがある場合は、パッケージ名・バージョンと最小コードを残し、同条件のREST応答を取得できるか確認します。旧SDKのIssue #276は、2024年に gemini-pro が映画レビューを処理した際の報告です。それだけを根拠に、Nano Banana 2や現在の google-genai に同じ不具合があるとは判断できません。

通常の依頼でもOTHERになる場合の再現手順

最小構成を試す目的は、動作の違いを記録して原因を絞ることです。成功を保証する「通る言い換え」を探す作業とは分けて考えます。

たとえば「白い背景に置いた青いマグカップの画像を1枚作成してください」という、人物や参照画像を含まない単純な依頼を、同じモデルに一回送って比較します。これも失敗するなら、元の依頼に特有の問題と決めつけず、モデル名・パラメータ・応答を確認します。単純な依頼だけ成功するなら、元の依頼の条件を一つずつ戻し、どの変更で結果が変わったかを記録します。

画像編集なら、参照画像の有無と、変更したい箇所をそれぞれ切り分けます。元の画像や依頼を共有できない場合は、同じ問題を再現する公開可能な素材を用意してください。別の依頼が成功しても、元の失敗が特定のポリシーや地域のせいだったと確定したわけではありません。

安全性の評価が返っている場合は、公式の安全設定の説明と照合します。調整可能なフィルターと、調整できない保護が存在することは公式に説明されていますが、そこから OTHER を特定の内部構造に一対一で対応させることはできません。理由を読まずに全カテゴリを BLOCK_NONEOFF に変えることを、汎用の修正方法にはしないでください。

再試行・タイムアウト・費用を別々に扱う

OTHER だけを条件に、同じ依頼の連続送信や別モデルへの自動切り替えを始めないでください。上のコードにはその処理を実装していません。アプリケーションへ組み込む場合は、少なくとも次の判断が別途必要です。

  • 再試行する失敗の種類。 一時的な通信エラーと、入力ブロックを同じ扱いにしない。
  • 回数と待ち時間。 上限を決め、無制限のループにしない。終了理由不明のまま再試行を増やさない。
  • 元のリクエストがまだ動いていないか。 クライアント側の待機終了は、サーバー側での生成中止を保証しない。
  • 追加費用を許容できるか。 新しい生成は新しい処理として記録し、元の結果と対応づける。

特に asyncio.wait_for(asyncio.to_thread(...)) のタイムアウトを、外部APIの処理を確実に停止する仕組みとして使うことはできません。待機が終了しても、スレッド内で開始したHTTP要求が継続している可能性があります。実際のHTTPクライアントのタイムアウト設定と、サービス側の取り消し可否を確認します。Pythonの非同期処理ドキュメント

料金についても、blockReason があることだけから「入力トークンもゼロ」「必ず無料」と結論づけないでください。usageMetadata があれば使用量を保存し、実際に利用した提供元の料金条件と請求記録を照合します。使用量フィールドが欠けている場合は、ゼロではなく未確認として扱います。

解決しないときに問い合わせへ渡すもの

再現する通常の依頼、時刻、モデル名と返された modelVersionresponseId、HTTPステータス、終了理由、利用SDKのバージョンをまとめます。元のJSONは手元に保管し、問い合わせには必要な範囲を取り出します。APIキー、個人情報、公開できない画像データは除いてください。

「OTHERが出る」という一文より、「このモデル・この入力・この設定で、候補が返らず promptFeedback.blockReasonOTHER になる。単純な別入力でも再現する」という記録の方が、提供元が調査を始めやすくなります。原因が判明するまでは、アプリ側でも理由不明の失敗として扱い、完成していない画像をユーザーへ成功表示しないことが先決です。

#Nano Banana 2#Gemini API#blockReason OTHER#画像生成#Python
Share: