Las fallas de las API de IA son diferentes de las fallas de las API normales. Una respuesta 200 no significa que tu generación haya tenido éxito. Un campo de contenido null no siempre es un error. Y el mismo prompt que funcionó ayer puede fallar hoy porque un proveedor actualizó su política de contenido.
Esta guía explica cómo leer los errores de las API de IA, qué significa realmente cada modo de falla y cómo crear un manejo de errores que te diga qué se rompió en lugar de solo que algo se rompió.
Nota: Los nombres de modelos como gpt-5.4 y gpt-5.4-mini utilizados en este artículo son identificadores de plataforma de CometAPI. Funcionan únicamente a través de https://api.cometapi.com/v1, no directamente a través de las API de OpenAI o Anthropic. Consulta la lista completa de modelos.
Por qué la depuración de APIs de IA es más difícil que la de APIs normales
Con una API REST típica, un 200 significa éxito y un 4xx significa que hiciste algo mal. Las APIs de IA agregan una tercera categoría: fallos suaves — respuestas que devuelven 200 pero no contienen contenido utilizable.
Tres cosas pueden salir mal:
- Fallo duro — error HTTP (4xx, 5xx). La solicitud no se completó.
- Fallo suave — HTTP 200, pero
finish_reasonescontent_filterolength, ocontentesnull. - Fallo silencioso — HTTP 200, el contenido parece correcto, pero la salida es errónea de una forma que solo detectas en la capa de aplicación.
La mayoría del manejo de errores solo cubre el caso 1. Los casos 2 y 3 son donde viven la mayoría de los bugs en producción.
Comprende el formato de respuesta de error
El endpoint de completado de texto devuelve una estructura de error consistente:
{ "error": { "message": "human-readable description (often includes request id)", "type": "comet_api_error", "param": "the_problematic_parameter_or_null", "code": "error_code_or_null" }}
Los endpoints de imagen y video devuelven formatos de error diferentes — analiza siempre el cuerpo bruto de la respuesta en lugar de asumir una estructura fija entre endpoints.
El campo message normalmente te dice exactamente qué está mal. El campo param te dice qué parámetro lo causó. Registra siempre ambos.
Conoce lo que significa cada código de estado HTTP
| Estado | Significado | Causa común | Solución |
|---|---|---|---|
| 400 | Solicitud incorrecta | Falta el modelo, parámetro incorrecto para este modelo | Revisa error.param en la respuesta |
| 401 | No autorizado | Clave de API incorrecta o ausente | Verifica el formato Authorization: Bearer <key> |
| 429 | Con límite de tasa | Demasiadas solicitudes | Reintentos exponenciales (consulta el Paso 4) |
| 500 | Error del servidor | Problema del proveedor o cuerpo de solicitud mal formado | Vuelve a intentar con backoff; verifica el formato de la solicitud |
| 504 | Tiempo de espera de la puerta de enlace | El proveedor tardó demasiado | Reintenta; considera un modelo más rápido |
Fuente**: CometAPI chat completions docs
La distinción 400 vs 500 importa para la lógica de reintentos. Un 400 significa que tu solicitud está mal — reintentar la misma solicitud no ayudará. Un 500 o 504 significa que el servidor tuvo un problema — reintentar tiene sentido.
Revisa finish_reason — el campo más pasado por alto
Una respuesta 200 con finish_reason: "content_filter" significa que tu generación fue bloqueada. El campo content estará null o vacío. Si no revisas esto, tu app devolverá silenciosamente nada.
| finish_reason | Significado | Qué hacer | Solución |
|---|---|---|---|
| stop | Finalización normal | Nada — es un éxito | Revisa error.param en la respuesta |
| length | Se alcanzó el límite de tokens | Aumenta max_tokens o acorta el prompt | Verifica el formato Authorization: Bearer <key> |
| content_filter | Bloqueado por la política de seguridad | Reformula el prompt; evita nombres/temas específicos | Reintentos exponenciales (consulta el Paso 4) |
| tool_calls | El modelo llamó a una herramienta en lugar de devolver texto | Gestiona la llamada a la herramienta; el contenido será null | Vuelve a intentar con backoff; verifica el formato de la solicitud |
| 504 | Tiempo de espera de la puerta de enlace | El proveedor tardó demasiado | Reintenta; considera un modelo más rápido |
Fuente**: CometAPI chat completions docs
import osimport loggingfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("COMETAPI_KEY")if not api_key: raise ValueError("COMETAPI_KEY is not set")client = OpenAI( base_url="https://api.cometapi.com/v1", api_key=api_key,)def safe_complete(messages: list, model: str = "gpt-5.4-mini", **kwargs) -> dict: """ Complete a chat request with full error and finish_reason handling. Returns {"content": str, "finish_reason": str, "tool_calls": list | None} Raises on API errors. """ try: response = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: error_body = {} try: error_body = e.response.json().get("error", {}) except Exception: pass logging.error( f"API error status={e.status_code} " f"message={error_body.get('message')} " f"param={error_body.get('param')}" ) raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network/timeout error: {e}") raise choice = response.choices[0] finish_reason = choice.finish_reason if finish_reason == "content_filter": raise ValueError( f"Generation blocked by content filter. " f"Model: {model}. Rephrase the prompt." ) if finish_reason == "length": used = response.usage.completion_tokens if response.usage else "unknown" logging.warning(f"Output truncated at token limit. Used {used} tokens.") # Return structured result so callers can handle tool_calls explicitly return { "content": choice.message.content or "", "finish_reason": finish_reason, "tool_calls": choice.message.tool_calls, }# Usageresult = safe_complete( messages=[{"role": "user", "content": "Summarize this article: [text]"}], model="gpt-5.4-mini")if result["finish_reason"] == "tool_calls": # Handle tool call — content will be empty print("Model wants to call a tool:", result["tool_calls"])else: print(result["content"])
Detecta fallos silenciosos en la capa de aplicación
Los fallos silenciosos son los más difíciles de detectar. La API devuelve 200, finish_reason es stop, pero la salida es semánticamente incorrecta. Solo puedes detectarlos en la capa de aplicación.
Patrones comunes:
def validate_completion(result: dict, task: str) -> str: """ Application-layer validation for silent failures. Raises ValueError if the output doesn't meet basic expectations. """ content = result["content"].strip() # Empty output that isn't a tool call if not content and result["finish_reason"] != "tool_calls": raise ValueError(f"Empty output for task '{task}' with finish_reason='{result['finish_reason']}'") # Task-specific checks if task == "classify": valid_labels = {"positive", "negative", "neutral"} if content.lower() not in valid_labels: logging.warning( f"Unexpected classification output: '{content}'. " f"Expected one of {valid_labels}. " f"Model may have returned explanation instead of label." ) if task == "json_extract": import json try: json.loads(content) except json.JSONDecodeError: raise ValueError( f"Expected JSON output but got: '{content[:100]}...'. " f"Try adding 'respond with valid JSON only' to the prompt, " f"or use response_format={{\"type\": \"json_object\"}}." ) if task == "summarize" and len(content.split()) < 10: logging.warning( f"Suspiciously short summary ({len(content.split())} words). " f"Check if the input was too short or the model misunderstood the task." ) return content# Full flow with silent failure detectionresult = safe_complete( messages=[{"role": "user", "content": "Classify as positive/negative/neutral: 'Great product!'"}], model="claude-haiku-4-5")label = validate_completion(result, task="classify")
Los fallos silenciosos suelen provenir de una de tres fuentes: el prompt es ambiguo, el modelo ignoró tus instrucciones de formato o la entrada era demasiado corta/larga para la tarea. Registrar la salida completa cuando falle la validación es la forma más rápida de diagnosticar cuál es el caso.
Añade reintentos exponenciales para límites de tasa
Los errores de límite de tasa (429) son temporales. La respuesta correcta es esperar y reintentar con demoras crecientes — una práctica estándar para cualquier API con límites de tasa:
import timeimport randomfrom openai import RateLimitErrordef complete_with_retry( messages: list, model: str = "gpt-5.4-mini", max_retries: int = 3, **kwargs) -> dict: """Retry on rate limits and server errors with exponential backoff.""" last_error = None for attempt in range(max_retries): try: return safe_complete(messages, model=model, **kwargs) except APIStatusError as e: if e.status_code < 500: raise # 4xx: don't retry, request is wrong last_error = e except RateLimitError as e: last_error = e except (APIConnectionError, APITimeoutError) as e: last_error = e if attempt < max_retries - 1: wait = (2 ** attempt) + random.random() # jitter prevents thundering herd logging.warning(f"Attempt {attempt + 1} failed. Waiting {wait:.1f}s before retry.") time.sleep(wait) raise RuntimeError(f"All {max_retries} attempts failed") from last_error
No reintentes en 400 o 401 — esos son errores del cliente que no se resolverán por sí solos.
Depura fallos de generación de imágenes
La generación de imágenes tiene sus propios modos de falla además de los errores HTTP estándar:
import base64import requestsdef generate_image_safe(prompt: str, model: str = "dall-e-3") -> dict: """ Generate an image with full error handling. Returns {"url": str | None, "bytes": bytes | None, "blocked": bool} """ api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") BASE64_MODELS = {"gpt-image-2", "qwen-image"} headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = {"model": model, "prompt": prompt, "size": "1024x1024"} if model in BASE64_MODELS: payload["output_format"] = "png" else: payload["response_format"] = "url" try: response = requests.post( "https://api.cometapi.com/v1/images/generations", json=payload, headers=headers, timeout=60 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Image generation HTTP error: {e.response.status_code} {e.response.text}") raise except requests.exceptions.Timeout: logging.error("Image generation timed out after 60s") raise data = response.json().get("data", []) if not data: logging.warning("Image generation returned empty data — prompt may have been filtered.") return {"url": None, "bytes": None, "blocked": True} item = data[0] if "revised_prompt" in item: logging.info(f"Provider revised prompt to: {item['revised_prompt']}") if "url" in item: return {"url": item["url"], "bytes": None, "blocked": False} return { "url": None, "bytes": base64.b64decode(item["b64_json"]), "blocked": False }
Problemas específicos de imagen a tener en cuenta:
| Síntoma | Causa | Solución |
|---|---|---|
| Matriz de datos vacía | Prompt filtrado | Revisa revised_prompt; reformula |
| error de response_format en GPT Image 2 | Parámetro no admitido | Usa output_format en su lugar |
| error de n > 1 en Qwen Image | Limitación del modelo | Haz solicitudes en bucle |
| La URL devuelve 403 más tarde | URL expirada | Descarga inmediatamente tras la generación |
Fuente**: CometAPI image generation docs
Depura fallos de generación de video
La generación de video falla de forma diferente porque es asíncrona. Inicializa variables de estado antes del bucle para que el mensaje de error por tiempo de espera siempre esté bien formado:
def submit_and_poll_video( prompt: str, model: str = "veo3-fast", max_wait: int = 600) -> str: """Submit video task and poll to completion. Returns video URL.""" api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") headers = {"Authorization": f"Bearer {api_key}"} try: response = requests.post( "https://api.cometapi.com/v1/videos", headers=headers, files={ "prompt": (None, prompt), "model": (None, model), "size": (None, "16x9") }, timeout=30 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Video submit failed: {e.response.status_code} {e.response.text}") raise task_id = response.json()["id"] logging.info(f"Video task submitted: {task_id}") poll_url = f"https://api.cometapi.com/v1/videos/{task_id}" elapsed = 0 interval = 10 status = "unknown" # initialize before loop progress = 0 # initialize before loop while elapsed < max_wait: try: poll_response = requests.get(poll_url, headers=headers, timeout=30) poll_response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Poll request failed: {e.response.status_code}") raise result = poll_response.json() status = result.get("status", "unknown") progress = result.get("progress", 0) logging.info(f"Task {task_id}: status={status} progress={progress}%") if status == "succeeded": return result["output"][0] elif status in ("failed", "cancelled"): error_detail = result.get("error", "no error detail returned") raise RuntimeError(f"Video task {task_id} failed: {error_detail}") time.sleep(interval) elapsed += interval raise TimeoutError( f"Video task {task_id} did not complete within {max_wait}s. " f"Last status: {status}, progress: {progress}%" )
Problemas específicos de video:
| Síntoma | Causa | Solución |
|---|---|---|
| Tarea atascada en queued más de 10 min | Carga del servidor | Reintenta con un modelo diferente |
| failed sin detalle de error | Prompt filtrado o error del modelo | Reformula el prompt |
| La URL del video devuelve 403 | URL expirada | Descarga de inmediato |
| task_not_exist en la primera consulta de Runway | La tarea aún se está inicializando (comportamiento documentado por CometAPI) | Espera 5 s y reintenta |
| Kling devuelve "succeed" y no "succeeded" | La API de Kling usa una cadena de estado no estándar | Gestiona ambas en la lógica de sondeo |
Fuente**: CometAPI video generation docs**, Kling Video docs
Versión en Node.js
import OpenAI from 'openai';const apiKey = process.env.COMETAPI_KEY;if (!apiKey) throw new Error('COMETAPI_KEY is not set');const client = new OpenAI({ baseURL: 'https://api.cometapi.com/v1', apiKey,});async function safeComplete(messages, model = 'gpt-5.4-mini', options = {}) { let response; try { response = await client.chat.completions.create({ model, messages, ...options }); } catch (err) { if (err.status && err.status < 500) { console.error(`Client error ${err.status}: ${err.message}`); } else { console.error(`Server/network error: ${err.message}`); } throw err; } const choice = response.choices[0]; const finishReason = choice.finish_reason; if (finishReason === 'content_filter') { throw new Error(`Generation blocked by content filter. Model: ${model}`); } if (finishReason === 'length') { console.warn(`Output truncated. Used ${response.usage?.completion_tokens ?? 'unknown'} tokens.`); } return { content: choice.message.content ?? '', finishReason, toolCalls: choice.message.tool_calls ?? null, };}async function completeWithRetry(messages, model = 'gpt-5.4-mini', maxRetries = 3) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await safeComplete(messages, model); } catch (err) { // Don't retry 4xx client errors if (err.status && err.status < 500) throw err; lastError = err; if (attempt < maxRetries - 1) { const wait = (2 ** attempt + Math.random()) * 1000; console.warn(`Attempt ${attempt + 1} failed. Retrying in ${(wait / 1000).toFixed(1)}s`); await new Promise(r => setTimeout(r, wait)); } } } throw new Error(`All ${maxRetries} attempts failed: ${lastError?.message}`);}// Usageconst result = await safeComplete( [{ role: 'user', content: 'Classify as positive/negative/neutral: "Great product!"' }], 'claude-haiku-4-5');if (result.finishReason === 'tool_calls') { console.log('Tool call requested:', result.toolCalls);} else { console.log(result.content);}
Una lista de verificación de depuración
Cuando una generación falla y no sabes por dónde empezar:
Para generación de texto:
- ¿La clave de API está configurada y en formato
Authorization: Bearer <key>? - ¿
finish_reasones algo distinto destop? - ¿
contentesnull? Comprueba sifinish_reasonestool_calls - ¿La salida se truncó? Revisa
finish_reason: "length"yusage.completion_tokens - ¿El error es un 4xx (corrige la solicitud) o un 5xx (reintenta)?
- ¿La salida pasa tu validación en la capa de aplicación? (fallo silencioso)
Para generación de imágenes:
- ¿El arreglo
dataestá vacío? (filtro de contenido) - ¿Usaste
response_formaten GPT Image 2? (no admitido — usaoutput_format) - ¿Configuraste
n > 1en Qwen Image? (no admitido) - ¿Descargaste la imagen antes de que caducara la URL?
Para generación de video:
- ¿La tarea está atascada en
queued? (prueba un modelo diferente) - ¿Revisaste el campo
erroren la respuesta de la tarea fallida? - ¿Descargaste el video antes de que caducara la URL?
- ¿Estás manejando tanto
"succeed"(Kling) como"succeeded"(Veo, Runway)?
Preguntas frecuentes
P: Mi solicitud devuelve 200 pero no hay contenido. ¿Qué pasó?
Revisa finish_reason. Si es content_filter, la generación fue bloqueada — la solicitud tuvo éxito pero la salida fue suprimida. Si es tool_calls, el modelo llamó a una herramienta en lugar de devolver texto, y content es null por diseño. Si finish_reason es stop pero el contenido sigue vacío, es un fallo silencioso — registra la respuesta completa y revisa tu prompt.
P: ¿Cómo sé si mi prompt está siendo filtrado?
Para texto: comprueba finish_reason === "content_filter". Para imágenes: verifica si el arreglo data está vacío. Para video: comprueba si la tarea llega a estado failed poco después del envío sin detalle de error. En todos los casos, intenta reformular el prompt para que sea más neutral.
P: ¿Cuándo debo reintentar una solicitud fallida?
Reintenta en 429 y 5xx utilizando backoff exponencial. No reintentes en 4xx — una solicitud incorrecta no se arreglará sola. La excepción es 401 si estás rotando claves de API.
P: ¿Qué es el backoff exponencial y por qué importa?
En lugar de reintentar inmediatamente, esperas progresivamente más: 1s, 2s, 4s. Agregar jitter aleatorio (+ random.random()) evita que múltiples clientes reintenten a la vez. Es una práctica estándar para cualquier API con límites de tasa — no específica de CometAPI.
P: La tarea de video está atascada en queued durante 10 minutos. ¿Está fallida?
No necesariamente — las colas pueden saturarse bajo carga. Espera hasta tu umbral max_wait, luego lanza un TimeoutError y reintenta con un modelo diferente. Registra el ID de la tarea para poder comprobar el estado manualmente si es necesario.
P: ¿Cómo detecto fallos silenciosos?
Los fallos silenciosos requieren validación en la capa de aplicación — la API no te dirá que la salida es semánticamente incorrecta. Comprueba que la salida coincide con el formato esperado (JSON válido, etiqueta esperada, longitud mínima). Registra la salida completa cuando falle la validación. Las causas más comunes son prompts ambiguos, instrucciones de formato ignoradas o entradas demasiado cortas o largas para la tarea.
