Falhas em APIs de IA são diferentes de falhas em APIs comuns. Uma resposta 200 não significa que sua geração foi bem-sucedida. Um campo de conteúdo null nem sempre é um erro. E o mesmo prompt que funcionou ontem pode falhar hoje porque um provedor atualizou sua política de conteúdo.
Este guia explica como ler erros de APIs de IA, o que cada modo de falha realmente significa e como criar tratativas de erro que informem o que quebrou em vez de apenas que algo quebrou.
Nota: Nomes de modelos como gpt-5.4 e gpt-5.4-mini usados neste artigo são identificadores da plataforma da CometAPI. Eles funcionam somente via https://api.cometapi.com/v1 — não diretamente pelas APIs da OpenAI ou Anthropic. Veja a lista completa de modelos.
Por que depurar APIs de IA é mais difícil do que depurar APIs comuns
Em uma API REST típica, um 200 significa sucesso e um 4xx significa que você fez algo errado. APIs de IA adicionam uma terceira categoria: falhas suaves — respostas que retornam 200 mas não contêm conteúdo utilizável.
Três coisas podem dar errado:
- Falha grave — erro HTTP (4xx, 5xx). A requisição não foi concluída.
- Falha suave — HTTP 200, mas
finish_reasonécontent_filteroulength, oucontenténull. - Falha silenciosa — HTTP 200, o conteúdo parece ok, mas a saída está errada de um modo que você só detecta na camada da aplicação.
A maior parte do tratamento de erros cobre apenas o caso 1. Os casos 2 e 3 são onde vivem a maioria dos bugs em produção.
Entenda o formato de resposta de erro
O endpoint de conclusões de texto retorna uma estrutura de erro 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" }}
Os endpoints de imagem e vídeo retornam formatos de erro diferentes — sempre analise o corpo bruto da resposta em vez de assumir uma estrutura fixa entre endpoints.
O campo message geralmente diz exatamente o que está errado. O campo param informa qual parâmetro causou o problema. Sempre registre ambos.
Saiba o que cada código de status HTTP significa
| Status | Significado | Causa comum | Correção |
|---|---|---|---|
| 400 | Requisição inválida | Falta modelo, parâmetro errado para este modelo | Verifique error.param na resposta |
| 401 | Não autorizado | Chave de API errada ou ausente | Verifique o formato Authorization: Bearer |
| 429 | Limitado por taxa | Muitas requisições | Backoff exponencial (veja a Etapa 4) |
| 500 | Erro do servidor | Problema no provedor ou corpo da requisição malformado | Tente novamente com backoff; verifique o formato da requisição |
| 504 | Tempo limite do gateway | O provedor demorou demais | Tente novamente; considere um modelo mais rápido |
Fonte**: CometAPI chat completions docs
A distinção entre 400 e 500 é importante para a lógica de retry. Um 400 significa que sua requisição está errada — repetir a mesma requisição não ajudará. Um 500 ou 504 significa que o servidor teve um problema — faz sentido tentar novamente.
Verifique finish_reason — o campo mais negligenciado
Uma resposta 200 com finish_reason: "content_filter" significa que sua geração foi bloqueada. O campo content estará null ou vazio. Se você não verificar isso, seu app retorna silenciosamente nada.
| finish_reason | Significado | O que fazer | Correção |
|---|---|---|---|
| stop | Conclusão normal | Nada — isto é sucesso | Verifique error.param na resposta |
| length | Atingiu o limite de tokens | Aumente max_tokens ou encurte o prompt | Verifique o formato Authorization: Bearer |
| content_filter | Bloqueado pela política de segurança | Reformule o prompt; evite nomes/tópicos específicos | Backoff exponencial (veja a Etapa 4) |
| tool_calls | O modelo chamou uma ferramenta em vez de retornar texto | Trate a chamada de ferramenta; content será null | Tente novamente com backoff; verifique o formato da requisição |
| 504 | Tempo limite do gateway | O provedor demorou demais | Tente novamente; considere um modelo mais rápido |
Fonte**: 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"])
Detecte falhas silenciosas na camada da aplicação
Falhas silenciosas são as mais difíceis de capturar. A API retorna 200, finish_reason é stop, mas a saída está semanticamente errada. Você só consegue capturá-las na camada da aplicação.
Padrões comuns:
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")
Falhas silenciosas geralmente têm três origens: o prompt é ambíguo, o modelo ignorou suas instruções de formato, ou a entrada era curta/demorada demais para a tarefa. Registrar toda a saída quando a validação falhar é a maneira mais rápida de diagnosticar qual delas é.
Adicione backoff exponencial para limites de taxa
Erros de limite de taxa (429) são temporários. A resposta correta é esperar e tentar novamente com atrasos crescentes — uma prática padrão para qualquer API com limites de taxa:
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
Não tente novamente em 400 ou 401 — são erros do cliente que não se resolvem sozinhos.
Depure falhas de geração de imagens
A geração de imagens tem seus próprios modos de falha além dos erros HTTP padrão:
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 imagem a observar:
| Sintoma | Causa | Correção |
|---|---|---|
| Array data vazio | Prompt filtrado | Verifique revised_prompt; reformule |
| erro de response_format no GPT Image 2 | Parâmetro não suportado | Use output_format em vez de |
| n > 1 erro no Qwen Image | Limitação do modelo | Faça requisições em loop |
| URL retorna 403 mais tarde | URL expirada | Baixe imediatamente após a geração |
Fonte**: CometAPI image generation docs
Depure falhas de geração de vídeo
A geração de vídeo falha de maneira diferente porque é assíncrona. Inicialize variáveis de status antes do loop para que a mensagem de timeout seja sempre bem formada:
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 vídeo:
| Sintoma | Causa | Correção |
|---|---|---|
| Tarefa presa em queued por 10+ min | Carga do servidor | Tente novamente com outro modelo |
| failed sem detalhe de erro | Prompt filtrado ou erro do modelo | Reformule o prompt |
| URL do vídeo retorna 403 | URL expirada | Baixe imediatamente |
| task_not_exist no primeiro poll do Runway | A tarefa ainda está inicializando (comportamento documentado da CometAPI) | Aguarde 5s e tente novamente |
| Kling retorna "succeed" não "succeeded" | A API do Kling usa string de status não padronizada | Trate ambas no polling |
Fonte**: CometAPI video generation docs**, Kling Video docs
Versão em 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);}
Uma lista de verificação de depuração
Quando uma geração falhar e você não souber por onde começar:
Para geração de texto:
- A chave de API está definida e no formato
Authorization: Bearer <key>? finish_reasoné algo diferente destop?contentestá nulo? Verifique sefinish_reasonétool_calls- A saída foi truncada? Verifique
finish_reason: "length"eusage.completion_tokens - O erro é um 4xx (corrija a requisição) ou 5xx (tente novamente)?
- A saída passa sua validação na camada da aplicação? (falha silenciosa)
Para geração de imagens:
- O array
dataestá vazio? (filtro de conteúdo) - Você usou
response_formatno GPT Image 2? (não é suportado — useoutput_format) - Você definiu
n > 1no Qwen Image? (não é suportado) - Você baixou a imagem antes que a URL expirasse?
Para geração de vídeo:
- A tarefa está presa em
queued? (tente um modelo diferente) - Você verificou o campo
errorna resposta da tarefa que falhou? - Você baixou o vídeo antes que a URL expirasse?
- Você está tratando tanto
"succeed"(Kling) quanto"succeeded"(Veo, Runway)?
FAQ
P: Minha requisição retorna 200 mas não há conteúdo. O que aconteceu?
Verifique finish_reason. Se for content_filter, a geração foi bloqueada — a requisição foi bem-sucedida, mas a saída foi suprimida. Se for tool_calls, o modelo chamou uma ferramenta em vez de retornar texto, e content é nulo por design. Se finish_reason for stop mas o conteúdo ainda estiver vazio, isso é uma falha silenciosa — registre a resposta completa e verifique seu prompt.
P: Como sei se meu prompt está sendo filtrado?
Para texto: verifique se finish_reason === "content_filter". Para imagens: verifique se o array data está vazio. Para vídeo: verifique se a tarefa chega a failed pouco após o envio, sem detalhes de erro. Em todos os casos, tente reformular o prompt para ser mais neutro.
P: Quando devo tentar novamente uma requisição que falhou?
Tente novamente em 429 e 5xx usando backoff exponencial. Não tente novamente em 4xx — uma requisição inválida não se corrige sozinha. A exceção é 401 se você estiver rotacionando chaves de API.
P: O que é backoff exponencial e por que isso importa?
Em vez de tentar novamente imediatamente, você espera progressivamente mais: 1s, 2s, 4s. Adicionar jitter aleatório (+ random.random()) evita que vários clientes tentem novamente em sincronia. Isso é prática padrão para qualquer API com limites de taxa — não é específico da CometAPI.
P: A tarefa de vídeo está presa em queued por 10 minutos. Falhou?
Não necessariamente — as filas podem se acumular sob carga. Aguarde até seu limite max_wait, então lance um TimeoutError e tente novamente com um modelo diferente. Registre o ID da tarefa para poder verificar o status manualmente, se necessário.
P: Como capturar falhas silenciosas?
Falhas silenciosas requerem validação na camada da aplicação — a API não vai informar que a saída está semanticamente errada. Verifique se a saída corresponde ao formato esperado (JSON válido, rótulo esperado, comprimento mínimo). Registre toda a saída quando a validação falhar. As causas mais comuns são prompts ambíguos, instruções de formato ignoradas ou entradas curtas ou longas demais para a tarefa.
