При сбое Nano Banana API сначала сохраните сырой ответ, а затем определите уровень сигнала. 400/429/503 — результаты HTTP-запроса, NO_IMAGE — официальная причина завершения генерации в актуальной справке Google, а тайм-аут может означать лишь то, что клиент или gateway прекратил ждать. Один цикл «Повторить» для этих веток неверен.
| Наблюдаемый результат | Что сохранить первым | Первое действие | Повторять без изменений? | Признак восстановления |
|---|---|---|---|---|
400 INVALID_ARGUMENT | Итоговый URL, API version, model, обезличенный body, details | Исправить контракт запроса | Нет | Минимальный запрос больше не даёт 400 |
429 RESOURCE_EXHAUSTED | Status/details, текущие лимиты, время и номер попытки | Найти ограничение и выровнять нагрузку | Только ограниченно | Стабильный успех при контролируемой параллельности |
503 UNAVAILABLE | Request ID, route, model, временной интервал | Проверить состояние сервиса и отступить | Только ограниченно | Минимальный запрос на том же маршруте проходит |
finishReason=NO_IMAGE | Candidate, finishMessage, parts, response ID | Проверить модальность и минимальный запрос | После классификации | Новый ответ содержит декодируемую часть с изображением |
Тайм-аут или 504 | Deadline каждого слоя, длительность, наличие body, request ID | Найти первый закрывший слой и проверить неизвестный результат | Лишь для временной ветки | Маршрут завершает запрос без дубля результата |
В официальном руководстве Gemini API Google рекомендует ограниченный exponential backoff для временных 408, 429 и 5xx, но не советует слепо повторять 400/403. Справочник ошибок API содержит no_image как ошибку генерации, а GenerateContent определяет NO_IMAGE как FinishReason: изображение ожидалось, но не было создано. Это официальный результат, но не HTTP-статус и не доказательство safety, перегрузки или списания средств.
Сначала соберите один диагностический пакет
Логируйте данные до того, как адаптер заменит ответ общей фразой. Промпт, входные изображения и ключи следует обезличить.
timestamp_utc, provider, base_url, api_version, endpoint, model
http_status, canonical_status, request_id, upstream_request_id
attempt, started_at, latency_ms, client_timeout_ms
candidate_count, prompt_block_reason, finish_reason
part_mime_types, has_text_part, has_image_part, error_body_summaryЗаписывайте реальный маршрут, а не только название Nano Banana. Gemini Developer API, Vertex AI, внешний gateway и ваш reverse proxy могут показать одинаковый текст пользователю, но иметь разные квоты и идентификаторы. Ниже используется нативная форма Gemini Developer API models/{model}:generateContent; пути Vertex с project и region сюда подставлять нельзя.
Минимальный пробник должен идти через тот же account, base URL и model, что и рабочая нагрузка. Значение GEMINI_MODEL берите из актуального официального списка моделей, а не из старого примера с preview-моделью.
API_VERSION="${GEMINI_API_VERSION:-v1beta}"
MODEL="${GEMINI_MODEL:?set GEMINI_MODEL}"
curl --fail-with-body --silent --show-error \
-X POST \
"https://generativelanguage.googleapis.com/${API_VERSION}/models/${MODEL}:generateContent" \
-H "x-goog-api-key: ${GEMINI_API_KEY:?set GEMINI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"contents": [{"parts": [{"text": "Создай предметное фото синей керамической кружки на белом фоне."}]}],
"generationConfig": {"responseModalities": ["IMAGE"]}
}'Если пробник проходит, а исходный запрос — нет, исследуйте входные изображения, контекст и дополнительные параметры. Если не проходит и он, переходите к account/route/capacity, не меняя всё одновременно.

Ошибка 400: ищите различие в запросе
400 INVALID_ARGUMENT означает, что API отклонил структуру или параметры. Повтор того же body ситуацию не изменит. Сравните итоговый URL, API version, model и responseModalities с актуальной документацией генерации изображений.
Сведите запрос к одному текстовому part и выходу IMAGE. Затем возвращайте по одному полю за тест. Для входного файла проверьте фактический MIME, способ передачи и размер байтов. Если минимальный запрос успешен, а после добавления одного параметра появляется 400, вы получили воспроизводимое доказательство вместо предположения.
Не смешивайте нативный Gemini endpoint с Vertex-путём. У Vertex есть собственные project, location и IAM-контракты; совет «сменить region» не является универсальным исправлением для Developer API.
429 и 503: общая техника повторов, разные причины
429 RESOURCE_EXHAUSTED указывает на превышение применимого ограничения. Оно может относиться к RPM, TPM, RPD, расходам или конкретной модели/ступени. Значения меняются, поэтому проверяйте их в текущем интерфейсе Google AI Studio rate limits, а не фиксируйте число из статьи.
Сначала уменьшите всплески: ограничьте worker concurrency, поставьте задания в очередь и распределите их по времени. Если все процессы повторят запрос через одну и ту же задержку, возникнет retry storm.
503 UNAVAILABLE означает временную недоступность или перегрузку. Это не доказательство неверного промпта и не обещание восстановления через определённое число минут. Проверьте официальный статус, минимальный запрос на том же model/route и сохраните request ID. После исчерпания бюджета попыток автоматический цикл должен остановиться.
Для собственного REST-клиента ограничьте попытки, timeout каждой попытки и общее время. Не добавляйте второй цикл поверх SDK, пока не проверите его встроенные повторы.
import random
import time
import requests
TRANSIENT = {408, 429, 500, 502, 503, 504}
def post_with_retry(
url, headers, payload, *, max_attempts=4, total_budget_s=120
):
deadline = time.monotonic() + total_budget_s
for attempt in range(1, max_attempts + 1):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("общий лимит retry исчерпан")
try:
response = requests.post(
url, headers=headers, json=payload,
timeout=(10, min(60, remaining))
)
except (requests.Timeout, requests.ConnectionError):
if attempt == max_attempts:
raise
else:
if response.status_code == 400:
raise ValueError(f"исправьте запрос: {response.text[:500]}")
if response.status_code not in TRANSIENT:
response.raise_for_status()
return response.json()
if attempt == max_attempts:
response.raise_for_status()
base = min(2 ** (attempt - 1), 30)
delay = base + random.uniform(0, base * 0.5)
if delay >= deadline - time.monotonic():
raise TimeoutError("новая попытка не помещается в общий лимит")
time.sleep(delay)NO_IMAGE: официальный результат генерации, а не HTTP-ошибка
Не смешивайте официальный finishReason с общей надписью приложения «нет изображения». В текущем контракте Google NO_IMAGE относится к candidate; gateway может отдельно преобразовать его в no_image. Сохраните upstream body и проверьте его по порядку:
- Нет
candidates: изучитеpromptFeedback; проблема промпта может остановить ответ до кандидатов. finishReason=NO_IMAGE: сохранитеfinishMessage,responseIdиmodelVersion, проверьте модальность и выполните минимальный запрос по тому же маршруту. Не переименовывайте результат в safety или 503 без доказательств.IMAGE_SAFETY,IMAGE_PROHIBITED_CONTENTили другая policy-причина: уточните законный запрос либо примите границу; retry не должен быть обходом.- Есть текст, но нет image part: прочитайте ответ и проверьте модальность. Text-only — не то же доказательство, что
NO_IMAGE. - Есть
inlineData, но приложение всё равно пишет «нет изображения»: проверяйте parser, MIME, base64 и запись в хранилище.
def inspect_result(body):
candidates = body.get("candidates") or []
if not candidates:
return {"kind": "no_candidates", "feedback": body.get("promptFeedback")}
candidate = candidates[0]
finish_reason = candidate.get("finishReason")
parts = ((candidate.get("content") or {}).get("parts") or [])
images = [p["inlineData"] for p in parts if p.get("inlineData")]
texts = [p["text"] for p in parts if p.get("text")]
if images:
kind = "image"
elif finish_reason == "NO_IMAGE":
kind = "no_image"
elif finish_reason in {
"SAFETY", "IMAGE_SAFETY", "PROHIBITED_CONTENT",
"IMAGE_PROHIBITED_CONTENT", "IMAGE_RECITATION",
}:
kind = "blocked_or_policy"
else:
kind = "candidate_without_image"
return {
"kind": kind,
"finishReason": finish_reason,
"finishMessage": candidate.get("finishMessage"),
"imageCount": len(images),
"textCount": len(texts),
"responseId": body.get("responseId"),
"modelVersion": body.get("modelVersion"),
}Для отдельной работы с фильтрацией используйте руководство по отсутствию изображения и IMAGE_SAFETY. Если в инциденте есть и другие HTTP-ошибки Gemini, сверяйтесь с общим руководством по ошибкам Gemini API.
Тайм-аут: кто первым закрыл соединение
Типичная цепочка выглядит так:
client/SDK → application → reverse proxy/API gateway → provider → modelКлиент может отменить запрос до получения любого HTTP body, а reverse proxy — создать собственный 504. Записывайте 504 провайдера только тогда, когда он пришёл в upstream-ответе. Важно и обратное: тайм-аут клиента не доказывает остановку сервера. Перед повтором операции, создающей изображение или запись, проверьте task/artifact по request или response ID, чтобы не получить дубль. Измеряйте старт, первый байт, завершение и deadline на каждом контролируемом участке.
Сравните два запроса по одному маршруту: минимальный безопасный и исходный сложный. Если первый стабилен, а второй всегда достигает client deadline, увеличивайте именно правильный timeout или уменьшайте вход. Если оба падают в одном окне с 503/504, новые тяжёлые попытки почти ничего не добавят к диагнозу.

Когда остановиться и что передать поддержке
Остановите повторы при 400 и других известных client errors; если NO_IMAGE ещё не проверен минимальным запросом и сырой структурой ответа; при safety/policy finishReason; после исчерпания попыток или общего времени; при постоянном 5xx для одного fingerprint; либо когда результат уже не нужен вызывающей стороне.
В пакет для поддержки включите UTC-интервал, provider/base URL, API version, endpoint/model, полный обезличенный error body, request и upstream request ID, задержку каждой попытки, timeout каждого слоя, минимальный промпт, MIME входа, candidate_count, promptFeedback, finishReason, finishMessage, responseId, modelVersion, список MIME частей и результат проверки артефактов после тайм-аута.
Исправление доказано только тогда, когда минимальный запрос возвращает декодируемую часть с изображением по реально используемому маршруту, а исходная нагрузка затем завершается при контролируемой параллельности. Одного HTTP 200, статуса задачи completed или исчезновения метки NO_IMAGE недостаточно.



