При сбое 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, перегрузки или списания средств.
Сначала соберите один диагностический пакет
Логируйте данные до того, как адаптер заменит ответ общей фразой. Промпт, входные изображения и ключи следует обезличить.
texttimestamp_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-моделью.
bashAPI_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, пока не проверите его встроенные повторы.
pythonimport 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 и запись в хранилище.
pythondef 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.
Тайм-аут: кто первым закрыл соединение
Типичная цепочка выглядит так:
textclient/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 недостаточно.
