Nano Banana API 이미지 안 나옴: 응답별 원인과 조치
이미지 없이 텍스트만 오거나 응답이 비면 finishReason·blockReason부터 봅니다. 정책 차단은 재시도로 안 풀리고, 종료된 preview ID는 교체해야 합니다.
목차

Nano Banana API를 호출했는데 이미지가 없다면, 원인을 가리는 단서는 대부분 응답 JSON 안에 있습니다. generateContent라면 candidates[0].finishReason과 promptFeedback.blockReason, Interactions API라면 status와 steps를 보면 됩니다. IMAGE_SAFETY·IMAGE_PROHIBITED_CONTENT 같은 정책 차단은 같은 요청을 다시 보내도 결과가 바뀌지 않으니 소재를 바꿔야 하고, STOP인데 텍스트만 왔다면 출력 형식을 이미지로 좁히면 됩니다. 2026년 6월에 종료된 gemini-3.1-flash-image-preview나 gemini-3-pro-image-preview를 아직 쓰고 있다면 재시도보다 모델 ID 교체가 먼저입니다.
코드 없이 Gemini 앱이나 웹에서 이미지가 안 나오는 경우는 원인과 해결 순서가 다릅니다. 그쪽은 Gemini에서 Nano Banana가 안 될 때: 이미지 오류 12가지 해결 분기에서 다룹니다. 아래 내용은 Gemini API, Google AI Studio, 공식 SDK, 그리고 API로 Nano Banana를 호출하는 도구를 쓰는 경우 기준입니다.
먼저 원본 응답을 파일로 저장합니다
"이미지가 안 나왔다"는 판단은 대개 코드가 내립니다. 코드가 .text만 읽었거나, 첫 번째 파트만 봤거나, 예외를 삼켰다면 실제 응답과 다른 결론이 나옵니다. 그래서 첫 단계는 SDK가 가공하기 전의 응답을 JSON 파일로 남기는 것입니다.
Nano Banana는 두 가지 API로 호출할 수 있고, 응답 구조가 완전히 다릅니다. Google은 2026년 6월 GA가 된 Interactions API를 새 프로젝트에 권장하고, generateContent는 레거시로 분류하되 계속 지원합니다.
| 구분 | generateContent | Interactions API |
|---|---|---|
| 이미지가 들어 있는 곳 | candidates[].content.parts[].inlineData | steps[] 중 type: "model_output"의 content[]에서 type: "image" |
| 끝난 이유 | candidates[].finishReason (+ finishMessage) | status (completed, failed, incomplete 등) |
| 요청 단계 차단 | candidates가 비고 promptFeedback.blockReason이 채워짐 | 공개 문서에 별도 필드 설명 없음 |
| 사용량 | usageMetadata | usage |
| 사용자 지정 안전 설정 | 지원 | 지원하지 않음 |
두 구조를 섞어 읽으면 안 됩니다. candidates를 찾는 코드로 interaction 객체를 읽으면 이미지가 있어도 "없음"으로 나옵니다.
Python SDK(google-genai)로 generateContent를 호출한다면 응답 객체를 REST와 같은 camelCase JSON으로 저장할 수 있습니다. google-genai 1.65.0 기준으로 model_dump_json(exclude_none=True, by_alias=True)는 finishReason, inlineData 같은 camelCase 키를 출력하고, by_alias를 빼면 finish_reason 같은 snake_case 키가 나옵니다.
from google import genai
from google.genai import types
client = genai.Client() # GEMINI_API_KEY 환경 변수 사용
response = client.models.generate_content(
model="gemini-3.1-flash-image",
contents="비 내리는 서울 골목의 네온 간판을 사진처럼 이미지로 생성해 주세요.",
config=types.GenerateContentConfig(response_modalities=["IMAGE"]),
)
with open("response.json", "w", encoding="utf-8") as f:
f.write(response.model_dump_json(exclude_none=True, by_alias=True))Interactions API는 SDK 버전에 따라 객체 필드가 문서와 다를 수 있어, 원본 확인용으로는 REST 응답을 그대로 저장하는 편이 확실합니다. 예를 들어 google-genai 1.65.0의 Interaction 모델에는 steps 대신 outputs 필드가 정의돼 있습니다. 문서 예제의 interaction.steps가 없다는 오류가 나면 SDK부터 최신으로 올리세요.
curl -s -X POST "https://generativelanguage.googleapis.com/v1beta/interactions" \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3.1-flash-image",
"input": "비 내리는 서울 골목의 네온 간판을 사진처럼 이미지로 생성해 주세요.",
"response_format": {"type": "image"}
}' \
-o interaction.json응답 형태별 원인과 다음 조치
저장한 JSON에서 아래 표의 왼쪽 열에 해당하는 형태를 찾으면 됩니다. 마지막 열은 같은 요청을 그대로 다시 보내는 것이 의미가 있는지입니다.
| 응답에서 보이는 것 | 의미 | 다음 조치 | 같은 요청 재시도 |
|---|---|---|---|
최상위 error 객체, HTTP 4xx/5xx | 요청 자체가 실패 | code·message 확인, 모델 ID가 종료 목록에 있는지 확인 | 429·408·5xx만 백오프 후 재시도 |
candidates 없음 + promptFeedback.blockReason | 프롬프트나 입력 이미지가 요청 단계에서 차단 | 프롬프트·참조 이미지 소재 변경 | 의미 없음 |
finishReason: IMAGE_SAFETY, IMAGE_PROHIBITED_CONTENT, SAFETY, PROHIBITED_CONTENT, BLOCKLIST, SPII | 생성 결과가 정책에 걸림 | 소재 자체를 변경, finishMessage 확인 | 의미 없음 |
finishReason: IMAGE_RECITATION, RECITATION | 기존 데이터와 비슷한 결과로 판정 | 프롬프트·참조를 더 고유하게 | 의미 없음 |
finishReason: NO_IMAGE, IMAGE_OTHER | 이미지를 기대했지만 만들어지지 않음 | 이미지 생성을 명시하고 요청을 단순화 | 한 번 정도 |
finishReason: STOP + 텍스트 파트만 | 모델이 텍스트로 답함 | 출력 형식을 이미지로 지정 | 설정을 바꾼 뒤 |
finishReason: MAX_TOKENS / status: incomplete | 출력 토큰 한도 도달 | maxOutputTokens 제거 또는 상향 | 설정을 바꾼 뒤 |
finishReason: MISSING_THOUGHT_SIGNATURE | 멀티턴 기록에서 thought signature 누락 | 직전 응답 전체를 기록에 그대로 전달 | 기록을 고친 뒤 |
status: failed, cancelled | interaction 실패·취소 | 원본 JSON 보관, 입력 확인 | 원인 확인 뒤 |
| 이미지 파트가 있는데 파일이 없음 | 파싱·저장 코드 문제 | 모든 파트를 순회하고 thought 파트 구분 | 필요 없음 |
finishReason 목록은 Google API 레퍼런스의 FinishReason 열거형 기준입니다. 이 값들은 어떤 단어나 요소가 차단을 일으켰는지까지는 알려 주지 않습니다. 표에 없는 새 값이 나오면 해석하지 말고 원본 값 그대로 기록해 두는 편이 안전합니다.
blockReason: OTHER로 후보가 비어 있는 경우는 Nano Banana 2 blockReason OTHER 오류 해결: 원인 미공개일 때 확인할 것에서, 멀티턴 편집 중 MISSING_THOUGHT_SIGNATURE로 실패하는 경우는 Nano Banana 2 Thought Signature 오류: 모든 시나리오 완벽 수정 가이드 (2026)에서 따로 다룹니다. 이미지가 아니라 429가 돌아온다면 결제 등급별 한도와 429 오류를 보세요.
재시도 기준도 Google 문제 해결 문서에 정리돼 있습니다. 429, 408, 5xx 같은 일시적 오류만 지수 백오프로 재시도하고 400, 402, 403은 재시도하지 않습니다. Python SDK는 이런 일시적 오류를 기본적으로 최대 4회 자동 재시도합니다. 정책 차단은 HTTP 오류가 아니라 정상 응답 안의 finishReason으로 오기 때문에, SDK의 자동 재시도 대상이 아니고 직접 재시도 루프를 돌려도 같은 판정이 반복됩니다.

텍스트만 돌아올 때는 출력 형식을 이미지로 좁힙니다
Nano Banana 모델은 별도 설정이 없으면 텍스트와 이미지를 함께 반환합니다. 기본 출력에 텍스트가 포함되므로, 모델이 텍스트만으로 답하고 finishReason이 STOP으로 끝난 응답도 형식상으로는 정상 종료입니다. 오류 처리 코드에 잡히지 않는 이유가 여기에 있습니다.
조치는 두 가지를 같이 하는 것입니다. 하나는 프롬프트에서 "이미지를 생성해 주세요"처럼 결과물이 이미지라는 점을 분명히 쓰는 것이고, 다른 하나는 출력 형식을 지정하는 것입니다.
generateContent:generationConfig.responseModalities에["IMAGE"]또는["TEXT", "IMAGE"]를 넣습니다. API 레퍼런스는 이 값을 응답 모달리티와 "정확히 일치"해야 하는 목록으로 설명하고, 지원하지 않는 조합이면 오류를 반환한다고 적고 있습니다. 빈 목록은 텍스트만 요청하는 것과 같습니다.- Interactions API:
response_format에{"type": "image"}를 넣으면 대화형 텍스트를 빼고 이미지만 요청합니다. 텍스트와 이미지를 모두 원하면[{"type": "text"}, {"type": "image"}]처럼 배열로 넘깁니다.
이 설정은 출력 모달리티를 제한할 뿐, 모든 프롬프트에서 이미지가 나온다고 보장하지는 않습니다. 이미지로 좁혀도 NO_IMAGE나 정책 차단으로 끝날 수 있으니, 설정 후에도 위 표로 다시 판정해야 합니다.
한국어 프롬프트를 의심하는 경우도 있습니다. 한국어로 요청했더니 이미지 대신 글이 나왔다는 경험담이 SNS에 올라와 있지만, 재현 조건이 붙은 보고는 아닙니다. Google 이미지 생성 문서는 권장 언어 목록에 ko-KR을 포함하고 있습니다. 먼저 이미지 생성을 명시하고 출력 형식을 지정해 보고, 그래도 같은 프롬프트에서 반복된다면 같은 내용을 영어로 한 번 보내 비교하는 정도가 비용이 적게 드는 확인 방법입니다.
IMAGE_SAFETY·PROHIBITED_CONTENT는 안전 설정으로 풀리지 않습니다
IMAGE_SAFETY는 생성된 이미지에 안전 위반이 있다는 뜻이고, IMAGE_PROHIBITED_CONTENT는 그 밖의 금지 콘텐츠가 있다는 뜻입니다. promptFeedback.blockReason에 IMAGE_SAFETY가 오면 이미지 생성 콘텐츠 때문에 후보가 차단됐다는 의미입니다.
safetySettings의 임계값을 BLOCK_NONE이나 OFF로 내리는 것은 해결책이 되지 않는 경우가 대부분입니다. 이유는 세 가지입니다.
- 조정할 수 있는 필터는 괴롭힘, 증오 표현, 음란물, 위험한 콘텐츠의 네 가지 카테고리입니다. Gemini 2.5와 3 모델에서는 임계값을 지정하지 않으면 기본값이 이미
OFF라서, 내릴 여지가 없는 경우가 많습니다. - 아동 안전처럼 핵심적인 위해에 대한 기본 보호는 항상 차단되며 조정할 수 없습니다.
- Interactions API는 사용자 지정 안전 설정 자체를 지원하지 않습니다.
그래서 이 형태에서 할 일은 소재를 바꾸는 것입니다. finishMessage가 채워져 있으면 먼저 읽습니다. Google은 어떤 요소가 차단을 일으켰는지 알려 주지 않으므로, Google 생성형 AI 금지 사용 정책에 비춰 타인의 권리를 침해하거나 사람을 속이고 괴롭히는 이미지로 읽힐 수 있는 요소를 하나씩 빼거나 바꿔 가며 다시 요청하는 수밖에 없습니다. 같은 모델을 다른 클라이언트나 중계 API로 호출하는 것도 우회 수단이 되지 않습니다. 호출 경로가 달라도 결과를 판정하는 것은 같은 Google 모델과 정책이기 때문입니다.
Interactions API에서 이미지 안전 차단이 어떤 필드로 표현되는지는 공개 문서에 따로 설명돼 있지 않습니다. 이 경우 status와 steps 전체를 저장해 두고, 이미지 블록 없이 끝난 응답을 차단 후보로 분류해 두는 것이 현실적입니다.
IMAGE_RECITATION은 결과가 기존 데이터와 비슷하다고 판정된 경우입니다. Google 문제 해결 문서는 RECITATION에 대해 프롬프트와 컨텍스트를 가능한 한 고유하게 만들라고 안내합니다. IMAGE_RECITATION에 대한 별도 안내는 없지만, 같은 방향으로 기존 작품을 그대로 재현하라는 표현이나 기존 저작물 참조 이미지를 줄여 보는 것이 첫 시도로 적당합니다.
이미지가 있는데 저장되지 않는다면 파싱 코드를 봅니다
Gemini 3 계열 이미지 모델(Nano Banana 2, Nano Banana 2 Lite, Nano Banana Pro)은 사고 과정이 기본으로 켜져 있고 API에서 끌 수 없습니다. 모델은 구도를 시험하는 중간 이미지를 최대 두 장 만들며, Google 문서에 따르면 사고 과정의 마지막 이미지가 최종 이미지이기도 합니다. 중간 이미지는 과금되지 않지만 사고 토큰은 과금됩니다.
이 구조 때문에 단순한 파서는 다음과 같이 틀릴 수 있습니다. 아래는 문서의 응답 구조에서 나오는 추론입니다.
generateContent에서includeThoughts를 켜면 사고 과정 파트가thought: true로 함께 옵니다. 첫 번째inlineData만 저장하는 코드는 최종 이미지가 아닌 중간 이미지를 저장할 수 있습니다. 공식 예제처럼part.thought가 참인 파트를 건너뛰고 나머지를 순회해야 합니다.- Interactions API의
interaction.output_image는 마지막 이미지 블록만 돌려줍니다. Nano Banana Pro처럼 텍스트와 이미지를 번갈아 내는 응답에서는 편의 속성이 전체 순서를 담지 못하므로,steps를 돌면서model_output단계의image블록을 모두 저장해야 합니다. .text만 출력하는 코드는 이미지가 있어도 "텍스트만 왔다"고 보이게 만듭니다.
또 하나 확인할 것은 요청한 장수입니다. Google은 모델이 사용자가 요청한 이미지 개수를 항상 정확히 따르지는 않는다고 명시합니다. 네 장을 요청했는데 한 장만 왔다고 해서 나머지가 차단된 것은 아닐 수 있습니다.
예전에 되던 코드라면 모델 ID부터 확인합니다
3월에 잘 돌던 코드가 지금 이미지를 못 받는다면, 먼저 의심할 곳은 코드에 적힌 모델 ID입니다. 당시 예제에 흔하던 preview ID는 대부분 이미 종료됐습니다. 아래 표는 Gemini API(Google AI Studio 키) 기준이며, 2026년 9월 24일 기준 Google 지원 중단 페이지와 변경 로그 내용입니다.
| 모델 ID | 상태 | 옮길 모델 |
|---|---|---|
gemini-3.1-flash-image-preview | 2026년 6월 25일 종료 | gemini-3.1-flash-image (Nano Banana 2) |
gemini-3-pro-image-preview | 2026년 6월 25일 종료 | gemini-3-pro-image (Nano Banana Pro) |
gemini-2.5-flash-image-preview | 2026년 1월 15일 종료 | 아래 현행 모델 중 선택 |
gemini-2.0-flash-preview-image-generation | 2025년 11월 14일 종료 | 아래 현행 모델 중 선택 |
gemini-2.5-flash-image (원조 Nano Banana) | 2026년 10월 2일 종료 예정 | gemini-3.1-flash-lite-image 또는 gemini-3.1-flash-image |
gemini-3.1-flash-image | 2026년 5월 28일 GA, 종료일 미발표 | 현행 |
gemini-3-pro-image | 2026년 5월 28일 GA, 종료일 미발표 | 현행 |
gemini-3.1-flash-lite-image (Nano Banana 2 Lite) | 2026년 6월 30일 GA | 현행 |

종료된 ID로 호출하면 이미지가 아니라 오류가 돌아옵니다. 그 오류의 정확한 코드와 메시지는 공개 문서에 기재돼 있지 않으니, 메시지 문자열을 조건으로 쓰기보다 요청에 쓴 모델 ID를 위 목록과 대조하는 편이 확실합니다. 원조 Nano Banana는 Google 모델 페이지가 Nano Banana 2 Lite로 옮기라고 권하고, 가격 페이지는 Nano Banana 2 또는 Nano Banana 2 Lite를 안내합니다. 어느 쪽이 맞는지는 Nano Banana 2 Lite·2·2.1·Pro 차이: 작업별 선택법, 새 프로젝트는 2.1에서 용도별로 비교할 수 있습니다.
같은 무렵 SDK도 바뀌었습니다. 예전 예제에 흔한 google.generativeai 패키지 대신 현재 문서는 from google import genai로 쓰는 google-genai 패키지를 사용합니다.
응답 점검 스크립트
위 표를 코드로 옮긴 스크립트입니다. 저장한 응답 JSON 한 개를 읽어 API 종류, 판정, 최종 이미지와 사고 과정 이미지 개수, 다음 조치, 모델 ID 경고, 과금 확인용 사용량을 출력합니다. 표준 라이브러리만 쓰며 Python 3.9 이상에서 동작합니다. REST 응답의 camelCase 키와 SDK 덤프의 snake_case 키를 모두 받습니다.
#!/usr/bin/env python3
"""Nano Banana 응답 JSON 한 개를 읽고 '이미지가 없는 이유'와 다음 조치를 출력합니다.
사용법: python3 nb_inspect.py response.json [--model 요청한_모델_ID]
종료 코드: 0 = 최종 이미지 있음, 1 = 이미지 없음, 2 = 읽을 수 없는 입력
"""
import json, sys
from datetime import date
SHUTDOWN = { # Google deprecations 페이지 기준 종료일
"gemini-3.1-flash-image-preview": "2026-06-25",
"gemini-3-pro-image-preview": "2026-06-25",
"gemini-2.5-flash-image-preview": "2026-01-15",
"gemini-2.0-flash-preview-image-generation": "2025-11-14",
"gemini-2.5-flash-image": "2026-10-02",
}
POLICY = {"IMAGE_SAFETY", "IMAGE_PROHIBITED_CONTENT", "SAFETY",
"PROHIBITED_CONTENT", "BLOCKLIST", "SPII"}
ACTIONS = {
"IMAGE_OK": "최종 이미지가 있습니다. 저장 코드가 thought 파트를 건너뛰고 모든 파트를 도는지 확인하세요.",
"PROMPT_BLOCKED": "요청 단계에서 차단됐습니다. 같은 요청 재시도는 의미가 없으니 프롬프트나 참조 이미지의 소재를 바꾸세요.",
"POLICY_BLOCK": "생성 결과가 정책에 걸렸습니다. 같은 요청 재시도는 의미가 없으니 소재 자체를 바꾸세요.",
"RECITATION": "기존 저작물과 비슷하다고 판정됐습니다. 프롬프트와 참조 이미지를 더 고유하게 바꾸세요.",
"NO_IMAGE": "이미지를 만들지 않고 끝났습니다. '이미지를 생성해 주세요'를 명시하고 한 번만 다시 시도하세요.",
"TEXT_ONLY": "텍스트만 돌아왔습니다. 이미지 생성을 명시하고 출력 형식을 이미지로 지정하세요.",
"MAX_TOKENS": "출력 토큰 한도에 걸렸습니다. maxOutputTokens 설정을 없애거나 늘리세요.",
"THOUGHT_SIGNATURE": "멀티턴 기록에서 thought signature가 빠졌습니다. 직전 응답 전체를 그대로 기록에 넣으세요.",
"EMPTY": "후보도 차단 사유도 없습니다. 원본 JSON과 HTTP 상태를 보관하고 한 번만 다시 시도하세요.",
"API_ERROR": "API 오류입니다. 이미지가 없는 게 아니라 요청 자체가 실패했습니다. code와 message를 확인하세요.",
"FAILED": "interaction이 실패/취소됐습니다. 원본 JSON을 보관하고 입력을 확인하세요.",
"PENDING": "아직 끝나지 않았습니다. 완료 상태가 된 뒤 다시 조회하세요.",
"UNKNOWN": "처리 규칙이 없는 값입니다. 원본 값을 그대로 기록하세요.",
}
def g(d, *keys): # camelCase(REST)와 snake_case(SDK 덤프)를 모두 받음
for k in keys:
if isinstance(d, dict) and k in d:
return d[k]
return None
def is_image(block):
return str(g(block, "mime_type", "mimeType") or "").startswith("image/")
def inspect_generate(r):
info = {"api": "generateContent", "final": 0, "thought": 0, "text": 0}
cands = g(r, "candidates") or []
block = g(g(r, "promptFeedback", "prompt_feedback") or {}, "blockReason", "block_reason")
for c in cands:
for p in g(g(c, "content") or {}, "parts") or []:
blob = g(p, "inlineData", "inline_data")
if blob and is_image(blob):
info["thought" if p.get("thought") else "final"] += 1
elif g(p, "text") and not p.get("thought"):
info["text"] += 1
reason = str(g(cands[0], "finishReason", "finish_reason") or "") if cands else ""
info["reason"] = f"blockReason={block}" if block else f"finishReason={reason or '-'}"
if info["final"]:
return "IMAGE_OK", info
if block and not cands:
return "PROMPT_BLOCKED", info
if not cands:
return "EMPTY", info
if reason in POLICY:
return "POLICY_BLOCK", info
if reason in ("IMAGE_RECITATION", "RECITATION"):
return "RECITATION", info
if reason in ("NO_IMAGE", "IMAGE_OTHER"):
return "NO_IMAGE", info
if reason == "MAX_TOKENS":
return "MAX_TOKENS", info
if reason == "MISSING_THOUGHT_SIGNATURE":
return "THOUGHT_SIGNATURE", info
if reason == "STOP":
return ("TEXT_ONLY" if info["text"] else "EMPTY"), info
return "UNKNOWN", info
def inspect_interaction(r):
info = {"api": "Interactions", "final": 0, "thought": 0, "text": 0}
status = str(g(r, "status") or "")
info["reason"] = f"status={status or '-'}"
for step in g(r, "steps") or []:
kind = g(step, "type")
blocks = g(step, "content") if kind == "model_output" else g(step, "summary")
for b in blocks or []:
if g(b, "type") == "image":
info["final" if kind == "model_output" else "thought"] += 1
elif g(b, "type") == "text" and kind == "model_output":
info["text"] += 1
if info["final"]:
return "IMAGE_OK", info
if status in ("failed", "cancelled"):
return "FAILED", info
if status == "incomplete":
return "MAX_TOKENS", info
if status in ("in_progress", "queued", "requires_action"):
return "PENDING", info
if status == "completed":
return "TEXT_ONLY" if info["text"] else "EMPTY", info
return "UNKNOWN", info
def model_warning(model):
model = (model or "").removeprefix("models/")
day = SHUTDOWN.get(model)
if not day:
return None
when = "종료됐습니다" if date.today().isoformat() >= day else "종료 예정입니다"
return f"{model}는 {day}에 {when}. 재시도보다 현행 모델 ID로 옮기는 게 먼저입니다."
def main(argv):
if len(argv) < 2:
print(__doc__)
return 2
try:
with open(argv[1], encoding="utf-8") as f:
r = json.load(f)
except (OSError, ValueError) as e:
print(f"읽을 수 없음: {e}")
return 2
model = argv[argv.index("--model") + 1] if "--model" in argv[:-1] else None
model = model or g(r, "modelVersion", "model_version", "model")
if "error" in r:
label, info = "API_ERROR", {"api": "-", "reason": json.dumps(r["error"], ensure_ascii=False)}
elif "steps" in r or "status" in r:
label, info = inspect_interaction(r)
elif "candidates" in r or "promptFeedback" in r or "prompt_feedback" in r:
label, info = inspect_generate(r)
else:
print("Nano Banana 응답 형식이 아닙니다.")
return 2
print(f"API: {info['api']}")
print(f"판정: {label} ({info['reason']})")
if "final" in info:
print(f"최종 이미지 {info['final']} / 사고 과정 이미지 {info['thought']} / 텍스트 {info['text']}")
print(f"다음 조치: {ACTIONS[label]}")
warning = model_warning(model)
if warning:
print(f"모델 경고: {warning}")
usage = g(r, "usageMetadata", "usage_metadata", "usage")
if usage:
print("과금 확인용 usage: " + json.dumps(usage, ensure_ascii=False))
return 0 if label == "IMAGE_OK" else 1
if __name__ == "__main__":
sys.exit(main(sys.argv))finishReason이 IMAGE_SAFETY인 응답을 넣으면 이렇게 출력하고 종료 코드 1을 돌려줍니다. 종료 코드를 보고 CI나 배치 작업에서 "이미지 없음"을 분기할 수 있습니다.
$ python3 nb_inspect.py response.json
API: generateContent
판정: POLICY_BLOCK (finishReason=IMAGE_SAFETY)
최종 이미지 0 / 사고 과정 이미지 0 / 텍스트 0
다음 조치: 생성 결과가 정책에 걸렸습니다. 같은 요청 재시도는 의미가 없으니 소재 자체를 바꾸세요.
과금 확인용 usage: {"promptTokenCount": 14, "candidatesTokenCount": 42, "thoughtsTokenCount": 150, ...}오류 본문에는 모델 이름이 없으므로, 종료 ID 여부까지 보려면 요청에 쓴 ID를 --model gemini-3-pro-image-preview처럼 넘깁니다.
이 스크립트의 검증 범위는 오프라인 테스트까지입니다. 문서의 응답 구조를 따라 손으로 만든 테스트용 JSON(fixture) 22개에서 판정과 종료 코드가 모두 기대값과 일치합니다. fixture는 정상 이미지, snake_case SDK 덤프, 사고 과정 이미지 포함, 텍스트만, 정책 차단 두 종류, NO_IMAGE, IMAGE_RECITATION, MAX_TOKENS, thought signature 누락, blockReason: OTHER, 빈 후보, 미지의 finishReason, 종료 예정 모델, Interactions의 완료·텍스트만·실패·미완료·진행 중, 오류 본문, 형식이 다른 JSON, 깨진 JSON입니다. 또 google-genai 1.65.0의 GenerateContentResponse로 fixture를 읽어 다시 덤프한 JSON(camelCase·snake_case 모두)도 같은 판정을 냈습니다. Google API를 실제로 호출한 응답으로는 검증되지 않았으므로, 실제 응답에서 판정이 어긋나면 그 JSON을 fixture로 추가해 규칙을 고치는 방식으로 쓰세요.
이미지를 못 받은 호출의 요금은 usage 필드로 확인합니다
실패한 호출도 과금되는지는 가장 많이 나오는 질문이지만, 차단되거나 이미지 없이 끝난 응답을 어떻게 과금하는지는 Google 가격 페이지에 규칙으로 공개돼 있지 않습니다. 공개된 것은 모달리티별 토큰 단가뿐입니다. 그래서 추측하지 말고 실패한 호출의 사용량 필드를 직접 읽는 것이 정확합니다.
| 모델 (Standard, 유료 등급) | 입력 | 텍스트·사고 출력 | 이미지 출력 | 이미지 1장 환산 |
|---|---|---|---|---|
Nano Banana 2 gemini-3.1-flash-image | 100만 토큰당 $0.50 | 100만 토큰당 $3 | 100만 토큰당 $60 | 0.5K $0.045, 1K $0.067, 2K $0.101, 4K $0.151 |
Nano Banana Pro gemini-3-pro-image | 100만 토큰당 $2 | 100만 토큰당 $12 | 100만 토큰당 $120 | 1K·2K $0.134, 4K $0.24 |
2026년 9월 24일 기준 Google 가격 페이지 값이며, 두 모델 모두 이미지 출력에는 무료 등급이 없습니다.
확인할 필드는 API마다 다릅니다.
generateContent:usageMetadata.candidatesTokensDetails에IMAGE모달리티 항목이 있는지, 그리고thoughtsTokenCount와promptTokenCount값을 봅니다.- Interactions API:
usage.output_tokens_by_modality에image가 있는지,total_thought_tokens와total_input_tokens값을 봅니다.
과금이 모달리티별 토큰 단위로 매겨지므로, 사용량에 이미지 출력 토큰이 없다면 계산에 남는 것은 입력 토큰과 텍스트·사고 토큰입니다. Nano Banana 2 기준 계산식은 다음과 같습니다.
요금(USD) = 입력 토큰 × 0.50 / 1,000,000
+ (텍스트 출력 토큰 + 사고 토큰) × 3 / 1,000,000
+ 이미지 출력 토큰 × 60 / 1,000,000예를 들어 입력 14토큰, 텍스트 출력 42토큰, 사고 150토큰이고 이미지 토큰이 없는 응답이라면 0.000007 + 0.000576 = 약 0.00058달러입니다. 반대로 1K 이미지 한 장은 가격 페이지 기준 1,120토큰이라 1,120 × 60 / 1,000,000 = 약 0.067달러입니다. 위 숫자는 계산 방법을 보여 주기 위한 예시이고, 사용량 필드와 실제 청구액이 일치하는지는 Google AI Studio나 Google Cloud 결제 화면에서 같은 기간을 대조해 확인해야 합니다. 앞의 스크립트가 출력하는 usage 줄을 로그로 남겨 두면 이 대조가 쉬워집니다.
자주 묻는 질문
Google AI Studio에서 이미지가 안 보이면 어디를 봐야 하나요?
요청이 차단되면 AI Studio에 Content blocked 메시지가 표시되고, 그 위에 마우스를 올리면 차단된 카테고리와 확률이 나옵니다. 안전 설정은 Run settings 패널의 Advanced settings에 있는 Safety settings에서 바꿀 수 있지만, 위에서 설명했듯 핵심 위해 보호와 IMAGE_SAFETY 판정은 이 슬라이더로 풀리지 않습니다. finishReason까지 확인하려면 같은 프롬프트와 모델 ID로 API를 호출해 위에서 설명한 방식으로 원본 응답을 저장합니다.
responseModalities를 ["IMAGE"]로 두면 항상 이미지가 나오나요?
아닙니다. 응답에 들어갈 수 있는 모달리티를 이미지로 제한할 뿐이고, 모델이 이미지를 만들지 못하면 NO_IMAGE나 정책 차단 사유로 끝납니다. 텍스트만 오는 문제에는 효과가 있지만, 정책 차단에는 효과가 없습니다.
원조 Nano Banana(gemini-2.5-flash-image)는 언제까지 쓸 수 있나요?
Google 지원 중단 페이지 기준 2026년 10월 2일에 종료됩니다. 그 뒤에는 이미지 대신 오류가 돌아오므로, 지금 쓰고 있다면 gemini-3.1-flash-lite-image나 gemini-3.1-flash-image로 옮기고 위 스크립트로 응답 형태가 바뀌지 않았는지 확인해 두는 것이 좋습니다.
이미지 없이 끝난 요청을 자동으로 재시도해도 되나요?
형태에 따라 다릅니다. 429·408·5xx 같은 일시적 오류와 NO_IMAGE·IMAGE_OTHER는 한두 번 재시도할 가치가 있습니다. IMAGE_SAFETY, IMAGE_PROHIBITED_CONTENT, PROHIBITED_CONTENT, blockReason이 있는 응답, 종료된 모델 ID는 같은 요청을 반복해도 결과가 바뀌지 않으므로 재시도 대상에서 빼야 합니다. 스크립트의 판정 값을 재시도 조건으로 그대로 쓸 수 있습니다.
공식 문서
요청 파라미터, 가격, 과금 규칙은 LaoZhang API 개발자 문서를 기준으로 합니다: 이미지 생성 과금과 실패 처리.
안내: laozhang.ai는 이 블로그 운영팀의 자사 서비스입니다. 그 가격은 독립적인 비교가 아니라 자사 제시 가격입니다.





