Cuando falla una solicitud de imagen de Nano Banana, conserva la evidencia cruda antes de reintentar. 400/429/503 son resultados HTTP de la petición, NO_IMAGE es un motivo de finalización oficial en la referencia actual de Google, y un timeout puede significar solo que el cliente o gateway dejó de esperar. No deben entrar en el mismo bucle de retry.
| Síntoma | Evidencia que debes guardar | Primera acción | ¿Repetir igual? | Prueba de recuperación |
|---|---|---|---|---|
400 INVALID_ARGUMENT | URL final, API version, modelo, body anonimizado, details | Corregir el contrato de petición | No | La petición mínima deja de devolver 400 |
429 RESOURCE_EXHAUSTED | Status/details, límites actuales, hora, intento | Identificar el límite y suavizar tráfico | Solo con límite | Éxito estable con concurrencia controlada |
503 UNAVAILABLE | Request ID, ruta, modelo, intervalo | Consultar estado y aplicar backoff | Solo con límite | El probe mínimo funciona en la misma ruta |
finishReason=NO_IMAGE | Candidate, finishMessage, parts, response ID | Revisar modalidad y ejecutar probe mínimo | Tras clasificar | Nueva respuesta con parte de imagen decodificable |
timeout o 504 | Deadlines por capa, duración, presencia de body, request ID | Hallar la primera capa y comprobar resultado incierto | Solo si es transitorio | Finaliza por la misma ruta sin duplicado |
La guía oficial de Gemini API recomienda backoff exponencial limitado para errores transitorios como 408, 429 y 5xx, y no reintentar 400/403 a ciegas. API errors incluye no_image como error de generación; GenerateContent define NO_IMAGE como FinishReason: se esperaba una imagen, pero no se generó. Es un resultado oficial, no un estado HTTP, y por sí solo no demuestra safety, sobrecarga ni facturación.
Conserva la respuesta antes de que el adaptador la simplifique
Registra estos campos antes de convertir el error en un mensaje para el usuario. Anonimiza prompt, imágenes de entrada y credenciales.
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 es un nombre común, no un identificador de protocolo. Guarda la ruta real. Gemini Developer API, Vertex AI, un gateway externo y tu reverse proxy pueden mostrar el mismo síntoma con responsabilidades diferentes. El probe siguiente usa la forma nativa de Gemini Developer API models/{model}:generateContent; no la mezcles con rutas Vertex de project/location.
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": "Genera una foto de producto de una taza de cerámica azul sobre fondo blanco."}]}], "generationConfig": {"responseModalities": ["IMAGE"]} }'
Configura GEMINI_MODEL con un modelo de imagen que la documentación oficial actual muestre como disponible para tu cuenta. Si el probe funciona y la petición original no, el problema se reduce a entrada, contexto o parámetros opcionales. Si ambos fallan, revisa cuenta, ruta y capacidad.

Un 400 se arregla comparando la petición
400 INVALID_ARGUMENT significa que la API ha rechazado la estructura o los parámetros. Repetir el mismo body no ayuda. Compara URL final, API version, modelo, responseModalities y MIME de entrada con la documentación actual de generación de imágenes.
Elimina campos opcionales hasta dejar una parte de texto y salida IMAGE. Después añade un campo por prueba. Un mínimo que funciona y falla al incorporar un parámetro es una evidencia mucho mejor que un payload grande que “parece correcto”. Si envías una imagen, registra el MIME real, método de transferencia y tamaño en bytes, no solo la extensión del fichero.
429 y 503 comparten backoff, no causa
429 RESOURCE_EXHAUSTED indica que has superado uno de los límites aplicables. Puede ser RPM, TPM, RPD, gasto u otro límite de modelo/nivel. Los valores cambian: compruébalos en Google AI Studio rate limits y no fijes en código un número de una guía antigua.
Reduce picos antes de pedir más cuota. Limita worker concurrency, usa una cola y reparte solicitudes. Si todos los workers despiertan tras la misma pausa, generan otra tormenta de reintentos.
503 UNAVAILABLE es indisponibilidad temporal o sobrecarga. No demuestra que el prompt sea incorrecto ni promete recuperación en un plazo fijo. Compara un probe mínimo en la misma cuenta/modelo/ruta, consulta el estado oficial y guarda request ID. Detén el automatismo al agotar el presupuesto de intentos o tiempo.
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("presupuesto total de retry agotado") 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"corrige la petición: {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("el siguiente retry excede el presupuesto") time.sleep(delay)
Usa una sola capa de retry. Si el SDK ya reintenta, otro bucle exterior puede multiplicar llamadas. Los valores del ejemplo son parámetros de aplicación ajustables, no una promesa del proveedor.
NO_IMAGE es un resultado de generación, no un error HTTP
No confundas el finishReason oficial con un mensaje genérico de la aplicación. En el contrato actual de Google, NO_IMAGE pertenece al candidate; un gateway puede mapearlo aparte a no_image. Conserva el body upstream y recórrelo así:
- Sin
candidates: mirapromptFeedback; un problema del prompt puede detener la respuesta antes de crear candidatos. finishReason=NO_IMAGE: guardafinishMessage,responseIdymodelVersion; revisa la modalidad y ejecuta el probe mínimo por la misma ruta. No lo renombres como safety o 503 sin evidencia.IMAGE_SAFETY,IMAGE_PROHIBITED_CONTENTu otro motivo de policy: revisa una petición legítima o acepta el límite; retry no es una evasión.- Solo partes de texto: lee el texto y verifica la modalidad. Una salida solo-texto no es la misma evidencia que
NO_IMAGE. - Hay
inlineDatapero la app dice que no hay imagen: revisa parser, MIME, base64 y escritura en almacenamiento.
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"), }
Para profundizar en seguridad y respuestas de solo texto, usa la guía de Gemini sin imagen e IMAGE_SAFETY. Si hay también errores de autenticación u otros HTTP, consulta la guía general de errores Gemini API.
Timeout: localiza la primera capa que cerró
textclient/SDK → application → reverse proxy/API gateway → provider → model
Si el cliente cancela primero, quizá no haya body HTTP de Google, y un reverse proxy puede crear su propio 504. Registra un 504 del proveedor solo cuando aparece en la respuesta upstream. Lo contrario también importa: el timeout del cliente no demuestra que el servidor se detuvo. Antes de repetir una operación que crea una imagen o escritura downstream, consulta task o artifact por request/response ID para evitar duplicados. Mide inicio, primer byte, fin, deadline y actor que cerró cada tramo.
Compara por la misma cuenta/ruta/modelo el probe mínimo y la entrada original. Si el primero funciona y la petición compleja termina siempre en el deadline del cliente, ajusta la capa correcta o reduce la entrada. Si ambos fallan en la misma ventana 503/504, repetir la carga pesada aporta poca información.

Cuándo parar y qué enviar a soporte
Detén los reintentos ante 400 u otro client error conocido; si NO_IMAGE aún no está clasificado con probe mínimo y respuesta cruda; ante un finishReason de safety/policy; al agotar intentos/tiempo total; ante 5xx persistente para el mismo fingerprint; o cuando el llamador ya no pueda usar el resultado.
Entrega a soporte: intervalo UTC, provider/base URL, API version, endpoint/modelo, body de error anonimizado, request/upstream request ID, latencia de cada intento, timeout por capa, prompt mínimo, MIME de entrada, número de candidates, promptFeedback, finishReason, finishMessage, responseId, modelVersion, MIME de las partes y resultado de buscar artifacts tras el timeout.
La reparación solo está demostrada cuando la petición mínima devuelve una parte de imagen decodificable por la ruta real y la carga original termina después con concurrencia controlada. HTTP 200, task completed o la desaparición de NO_IMAGE en el adaptador no bastan por sí solos.
