Kimi K2.7 Code is now on CometAPI — Kimi's most intelligent coding model to date, reliably follows instructions in long contexts and completes programming tasks with a higher success rate. Try it now

Comment déboguer les générations échouées de l'API d'IA

CometAPI
AnnaJun 4, 2026
Comment déboguer les générations échouées de l'API d'IA

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:

  1. Échec dur — erreur HTTP (4xx, 5xx). La requête ne s’est pas terminée.
  2. Échec souple — HTTP 200, mais finish_reason vaut content_filter ou length, ou content est null.
  3. É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

StatusMeaningCommon causeFix
400Requête invalideModèle manquant, mauvais paramètre pour ce modèleVérifier error.param dans la réponse
401Non autoriséClé d’API erronée ou manquanteVérifier le format Authorization: Bearer
429Limitation de débitTrop de requêtesBackoff exponentiel (voir Étape 4)
500Erreur serveurProblème côté fournisseur ou corps de requête mal forméRetenter avec backoff; vérifier le format de requête
504Délai d’attente de la passerelleLe fournisseur a mis trop de tempsRetenter; 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_reasonMeaningWhat to doFix
stopAchèvement normalRien — c’est un succèsCheck error.param in the response
lengthLimite de jetons atteinteAugmenter max_tokens ou raccourcir l’inviteVerify Authorization: Bearer format
content_filterBloqué par une politique de sécuritéReformuler l’invite; éviter des noms/sujets précisExponential backoff (see Step 4)
tool_callsLe modèle a appelé un outil au lieu de retourner du texteGérer l’appel d’outil; content sera nullRetry with backoff; check request format
504Délai d’attente de la passerelleLe fournisseur a mis trop de tempsRetry; consider a faster model

Source*:* Documentation des complétions de chat CometAPI

import osimport loggingfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom dotenv import load_dotenv​load_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 RateLimitError​def 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 requests​def 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:

SymptomCauseFix
Tableau data videInvite filtréeVérifier revised_prompt; reformuler
erreur response_format sur GPT Image 2Paramètre non pris en chargeUtiliser output_format à la place
erreur n > 1 sur Qwen ImageLimitation du modèleBoucler les requêtes à la place
L’URL renvoie 403 plus tardURL expiréeTé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:

SymptomCauseFix
Tâche bloquée en queued plus de 10 minCharge serveurRetenter avec un autre modèle
failed sans détail d’erreurInvite filtrée ou erreur du modèleReformuler l’invite
L’URL de la vidéo renvoie 403URL expiréeTélécharger immédiatement
task_not_exist lors du premier poll RunwayLa 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 standardGé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.

Prêt à réduire vos coûts de développement IA de 20 % ?

Démarrez gratuitement en quelques minutes. Crédits d'essai offerts. Aucune carte bancaire requise.

En savoir plus