Les défaillances des API d’IA sont différentes de celles des API classiques. Une réponse 200 ne signifie pas que votre génération a réussi. Un champ content null n’est pas toujours une erreur. Et la même invite qui fonctionnait hier peut échouer aujourd’hui parce qu’un fournisseur a mis à jour sa politique de contenu.
Ce guide explique comment lire les erreurs des API d’IA, ce que signifie réellement chaque mode de défaillance, et comment construire une gestion des erreurs qui vous indique ce qui a cassé plutôt que simplement que quelque chose a cassé.
Note: Les noms de modèles comme gpt-5.4 et gpt-5.4-mini utilisés dans cet article sont les identifiants de plateforme de CometAPI. Ils ne fonctionnent qu’à travers https://api.cometapi.com/v1 — pas directement via les API d’OpenAI ou d’Anthropic. Voir la liste complète des modèles: https://www.cometapi.com/models.
Pourquoi le débogage des API d’IA est plus difficile que celui des API classiques
Avec une API REST classique, un 200 signifie succès et un 4xx signifie que vous avez fait une erreur. Les API d’IA ajoutent une troisième catégorie: échecs souples — des réponses qui renvoient 200 mais ne contiennent aucun contenu exploitable.
Trois choses peuvent mal tourner:
- Échec dur — erreur HTTP (4xx, 5xx). La requête ne s’est pas terminée.
- Échec souple — HTTP 200, mais finish_reason vaut content_filter ou length, ou content est null.
- Échec silencieux — HTTP 200, le contenu a l’air correct, mais la sortie est erronée d’une manière que vous ne détectez qu’au niveau applicatif.
La plupart des gestions d’erreurs ne couvrent que le cas 1. Les cas 2 et 3 sont là où vivent la plupart des bugs en production.
Comprendre le format de réponse d’erreur
Le point de terminaison des complétions de texte renvoie une structure d’erreur cohérente:
{ "error": { "message": "human-readable description (often includes request id)", "type": "comet_api_error", "param": "the_problematic_parameter_or_null", "code": "error_code_or_null" }}
Les points de terminaison image et vidéo renvoient des formats d’erreur différents — analysez toujours le corps brut de la réponse plutôt que de supposer une structure fixe entre les endpoints.
Le champ message vous dit généralement exactement ce qui ne va pas. Le champ param indique quel paramètre en est la cause. Journalisez toujours les deux.
Connaître la signification de chaque code d’état HTTP
| Status | Meaning | Common cause | Fix |
|---|---|---|---|
| 400 | Requête invalide | Modèle manquant, mauvais paramètre pour ce modèle | Vérifier error.param dans la réponse |
| 401 | Non autorisé | Clé d’API erronée ou manquante | Vérifier le format Authorization: Bearer |
| 429 | Limitation de débit | Trop de requêtes | Backoff exponentiel (voir Étape 4) |
| 500 | Erreur serveur | Problème côté fournisseur ou corps de requête mal formé | Retenter avec backoff; vérifier le format de requête |
| 504 | Délai d’attente de la passerelle | Le fournisseur a mis trop de temps | Retenter; envisager un modèle plus rapide |
Source*:* Documentation des complétions de chat CometAPI
La distinction 400 vs 500 est importante pour la logique de retry. Un 400 signifie que votre requête est incorrecte — retenter la même requête ne servira à rien. Un 500 ou 504 signifie que le serveur a eu un problème — retenter a du sens.
Vérifiez finish_reason — le champ le plus négligé
Une réponse 200 avec finish_reason: "content_filter" signifie que votre génération a été bloquée. Le champ content sera null ou vide. Si vous ne vérifiez pas cela, votre application renverra silencieusement rien.
| finish_reason | Meaning | What to do | Fix |
|---|---|---|---|
| stop | Achèvement normal | Rien — c’est un succès | Check error.param in the response |
| length | Limite de jetons atteinte | Augmenter max_tokens ou raccourcir l’invite | Verify Authorization: Bearer |
| content_filter | Bloqué par une politique de sécurité | Reformuler l’invite; éviter des noms/sujets précis | Exponential backoff (see Step 4) |
| tool_calls | Le modèle a appelé un outil au lieu de retourner du texte | Gérer l’appel d’outil; content sera null | Retry with backoff; check request format |
| 504 | Délai d’attente de la passerelle | Le fournisseur a mis trop de temps | Retry; consider a faster model |
Source*:* Documentation des complétions de chat CometAPI
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"])
Détecter les échecs silencieux au niveau applicatif
Les échecs silencieux sont les plus difficiles à attraper. L’API renvoie 200, finish_reason vaut stop, mais la sortie est sémantiquement incorrecte. Vous ne pouvez les détecter qu’au niveau applicatif.
Schémas courants:
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")
Les échecs silencieux proviennent généralement de l’une de trois sources: l’invite est ambiguë, le modèle a ignoré vos instructions de format, ou l’entrée était trop courte/longue pour la tâche. Journaliser la sortie complète lorsque la validation échoue est le moyen le plus rapide de diagnostiquer laquelle.
Ajouter un backoff exponentiel pour les limites de débit
Les erreurs de limitation de débit (429) sont temporaires. La bonne réponse est d’attendre et de retenter avec des délais croissants — une pratique standard pour toute API avec des limites de débit:
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
Ne retentez pas sur 400 ou 401 — ce sont des erreurs côté client qui ne se résoudront pas toutes seules.
Déboguer les échecs de génération d’images
La génération d’images a ses propres modes de défaillance en plus des erreurs HTTP standard:
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 }
Problèmes spécifiques aux images à surveiller:
| Symptom | Cause | Fix |
|---|---|---|
| Tableau data vide | Invite filtrée | Vérifier revised_prompt; reformuler |
| erreur response_format sur GPT Image 2 | Paramètre non pris en charge | Utiliser output_format à la place |
| erreur n > 1 sur Qwen Image | Limitation du modèle | Boucler les requêtes à la place |
| L’URL renvoie 403 plus tard | URL expirée | Télécharger immédiatement après génération |
Source*:* Documentation de génération d’images CometAPI
Déboguer les échecs de génération de vidéos
La génération de vidéos échoue différemment car elle est asynchrone. Initialisez les variables d’état avant la boucle afin que le message d’erreur de délai soit toujours bien formé:
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}%" )
Problèmes spécifiques aux vidéos:
| Symptom | Cause | Fix |
|---|---|---|
| Tâche bloquée en queued plus de 10 min | Charge serveur | Retenter avec un autre modèle |
| failed sans détail d’erreur | Invite filtrée ou erreur du modèle | Reformuler l’invite |
| L’URL de la vidéo renvoie 403 | URL expirée | Télécharger immédiatement |
| task_not_exist lors du premier poll Runway | La tâche est encore en initialisation (comportement documenté par CometAPI) | Attendre 5 s et retenter |
| Kling renvoie "succeed" et non "succeeded" | L’API de Kling utilise une chaîne de statut non standard | Gérer les deux dans la logique de polling |
Source*:* Documentation de génération vidéo CometAPI (Veo3)**, Documentation vidéo Kling
Version 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);}
Une checklist de débogage
Quand une génération échoue et que vous ne savez pas par où commencer:
Pour la génération de texte:
- La clé d’API est‑elle définie et au format Authorization: Bearer
? - finish_reason est‑il autre chose que stop ?
- content est‑il null ? Vérifier si finish_reason vaut tool_calls
- La sortie a‑t‑elle été tronquée ? Vérifier finish_reason: "length" et usage.completion_tokens
- L’erreur est‑elle un 4xx (corriger la requête) ou un 5xx (retenter) ?
- La sortie passe‑t‑elle votre validation au niveau applicatif ? (échec silencieux)
Pour la génération d’images:
- Le tableau data est‑il vide ? (filtre de contenu)
- Avez‑vous utilisé response_format sur GPT Image 2 ? (non pris en charge — utilisez output_format)
- Avez‑vous défini n > 1 sur Qwen Image ? (non pris en charge)
- Avez‑vous téléchargé l’image avant l’expiration de l’URL ?
Pour la génération de vidéos:
- La tâche est‑elle bloquée en queued ? (essayez un autre modèle)
- Avez‑vous vérifié le champ error dans la réponse de tâche échouée ?
- Avez‑vous téléchargé la vidéo avant l’expiration de l’URL ?
- Gérez‑vous à la fois "succeed" (Kling) et "succeeded" (Veo, Runway) ?
FAQ
Q: Ma requête renvoie 200 mais il n’y a pas de contenu. Que s’est‑il passé ?
Vérifiez finish_reason. Si c’est content_filter, la génération a été bloquée — la requête a réussi mais la sortie a été supprimée. Si c’est tool_calls, le modèle a appelé un outil au lieu de renvoyer du texte, et content est null par conception. Si finish_reason vaut stop mais que le contenu est toujours vide, c’est un échec silencieux — journalisez la réponse complète et vérifiez votre invite.
Q: Comment savoir si mon invite est filtrée ?
Pour le texte: vérifiez finish_reason === "content_filter". Pour les images: vérifiez si le tableau data est vide. Pour la vidéo: vérifiez si la tâche passe à l’état failed peu après la soumission sans détail d’erreur. Dans tous les cas, essayez de reformuler l’invite de manière plus neutre.
Q: Quand dois‑je retenter une requête échouée ?
Retentez sur 429 et 5xx en utilisant un backoff exponentiel. Ne retentez pas sur 4xx — une mauvaise requête ne se corrigera pas toute seule. L’exception est 401 si vous faites une rotation des clés API.
Q: Qu’est‑ce que le backoff exponentiel et pourquoi est‑ce important ?
Au lieu de retenter immédiatement, vous attendez de plus en plus longtemps: 1s, 2s, 4s. Ajouter du jitter aléatoire (+ random.random()) empêche plusieurs clients de retenter en synchronisation. C’est une pratique standard pour toute API avec des limites de débit — ce n’est pas spécifique à CometAPI.
Q: La tâche vidéo est bloquée en queued depuis 10 minutes. Est‑elle en échec ?
Pas nécessairement — les files d’attente peuvent se remplir en période de charge. Attendez jusqu’à votre seuil max_wait, puis levez une TimeoutError et retentez avec un modèle différent. Journalisez l’ID de tâche pour pouvoir vérifier le statut manuellement si nécessaire.
Q: Comment attraper les échecs silencieux ?
Les échecs silencieux nécessitent une validation au niveau applicatif — l’API ne vous dira pas que la sortie est sémantiquement incorrecte. Vérifiez que la sortie correspond au format attendu (JSON valide, étiquette attendue, longueur minimale). Journalisez la sortie complète quand la validation échoue. Les causes les plus courantes sont des invites ambiguës, des instructions de format ignorées, ou des entrées trop courtes ou trop longues pour la tâche.
