AI-API-fouten verschillen van reguliere API-fouten. Een 200-response betekent niet dat je generatie is geslaagd. Een veld content met waarde null is niet altijd een fout. En dezelfde prompt die gisteren werkte, kan vandaag falen omdat een provider zijn inhoudsbeleid heeft bijgewerkt.
Deze gids behandelt hoe je AI-API-fouten leest, wat elke faalmodus werkelijk betekent, en hoe je foutafhandeling bouwt die vertelt wat er stukging in plaats van alleen dát er iets stuk is.
Opmerking: Modelnamen zoals gpt-5.4 en gpt-5.4-mini die in dit artikel worden gebruikt, zijn platformidentifiers van CometAPI. Ze werken alleen via https://api.cometapi.com/v1 — niet rechtstreeks via de API’s van OpenAI of Anthropic. Zie de full model list.
Waarom debuggen van AI-API’s lastiger is dan bij gewone API’s
Bij een typische REST-API betekent 200 succes en 4xx dat jij iets fout deed. AI-API’s voegen een derde categorie toe: zachte fouten — responses die 200 teruggeven maar geen bruikbare inhoud bevatten.
Er kunnen drie dingen misgaan:
- Harde fout — HTTP-fout (4xx, 5xx). Het verzoek is niet voltooid.
- Zachte fout — HTTP 200, maar
finish_reasoniscontent_filteroflength, ofcontentisnull. - Stille fout — HTTP 200, inhoud lijkt oké, maar de output is fout op een manier die je pas op de applicatielaag vangt.
De meeste foutafhandeling dekt alleen geval 1. Gevallen 2 en 3 zijn waar de meeste bugs in productie zitten.
Begrijp het foutresponsformaat
De text completions-endpoint retourneert een consistent foutobject:
{ "error": { "message": "human-readable description (often includes request id)", "type": "comet_api_error", "param": "the_problematic_parameter_or_null", "code": "error_code_or_null" }}
Image- en video-endpoints geven andere foutformaten terug — parse altijd de ruwe response-body in plaats van uit te gaan van een vaste structuur over endpoints heen.
Het veld message vertelt meestal precies wat er mis is. Het veld param vertelt welk parameter dit veroorzaakte. Log ze altijd allebei.
Weet wat elke HTTP-statuscode betekent
| Status | Betekenis | Veelvoorkomende oorzaak | Oplossing |
|---|---|---|---|
| 400 | Ongeldig verzoek | Ontbrekend model, verkeerde parameter voor dit model | Controleer error.param in de response |
| 401 | Niet geautoriseerd | Verkeerde of ontbrekende API-sleutel | Verifieer Authorization: Bearer |
| 429 | Rate limit bereikt | Te veel verzoeken | Exponentiële backoff (zie Stap 4) |
| 500 | Serverfout | Probleem aan providerzijde of ongeldig request body | Opnieuw proberen met backoff; controleer requestformaat |
| 504 | Gateway-time-out | Provider deed er te lang over | Opnieuw proberen; overweeg een sneller model |
Bron**: CometAPI chat completions docs
Het onderscheid 400 vs 500 is belangrijk voor retrylogica. Een 400 betekent dat je verzoek fout is — dezelfde aanvraag opnieuw proberen helpt niet. Een 500 of 504 betekent dat de server een probleem had — opnieuw proberen is logisch.
Controleer finish_reason — het meest over het hoofd geziene veld
Een 200-response met finish_reason: "content_filter" betekent dat je generatie is geblokkeerd. Het veld content is null of leeg. Als je dit niet controleert, geeft je app stilzwijgend niets terug.
| finish_reason | Betekenis | Wat te doen | Oplossing |
|---|---|---|---|
| stop | Normale voltooiing | Niets — dit is succes | Controleer error.param in de response |
| length | Tokengrens bereikt | Verhoog max_tokens of verkort de prompt | Verifieer Authorization: Bearer <key>-formaat |
| content_filter | Geblokkeerd door veiligheidsbeleid | Herschrijf de prompt; vermijd specifieke namen/onderwerpen | Exponentiële backoff (zie Stap 4) |
| tool_calls | Model riep een tool aan i.p.v. tekst te geven | Verwerk de tool-aanroep; content is null | Opnieuw proberen met backoff; controleer requestformaat |
| 504 | Gateway-time-out | Provider deed er te lang over | Opnieuw proberen; overweeg een sneller model |
Bron**: 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"])
Detecteer stille fouten op de applicatielaag
Stille fouten zijn het lastigst te vangen. De API retourneert 200, finish_reason is stop, maar de output is semantisch onjuist. Je kunt dit alleen op de applicatielaag detecteren.
Veelvoorkomende patronen:
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")
Stille fouten komen meestal uit een van drie bronnen: de prompt is ambigu, het model negeerde je formaat-instructies, of de input was te kort/te lang voor de taak. Het loggen van de volledige output wanneer validatie faalt is de snelste manier om te diagnosticeren welke van de drie het is.
Voeg exponentiële backoff toe voor rate limits
Rate-limitfouten (429) zijn tijdelijk. De juiste reactie is wachten en opnieuw proberen met oplopende vertragingen — een standaardpraktijk voor elke API met rate limits:
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
Herhaal niet bij 400 of 401 — dat zijn clientfouten die niet vanzelf verdwijnen.
Debuggen van afbeeldingsgeneratiefouten
Afbeeldingsgeneratie heeft eigen faalmodi bovenop de standaard HTTP-fouten:
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 }
Afbeeldingsspecifieke aandachtspunten:
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
| Lege data-array | Prompt gefilterd | Controleer revised_prompt; herformuleer |
| response_format-fout op GPT Image 2 | Parameter niet ondersteund | Gebruik in plaats daarvan output_format |
| n > 1-fout op Qwen Image | Modellimiteit | In plaats daarvan verzoeken loopen |
| URL geeft later 403 | URL verlopen | Meteen na generatie downloaden |
Bron**: CometAPI image generation docs
Debuggen van videogeneratiefouten
Videogeneratie faalt anders omdat het asynchroon is. Initialiseer statusvariabelen vóór de lus zodat de time-outerrorboodschap altijd goed gevormd is:
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}%" )
Videospecifieke aandachtspunten:
| Symptoom | Oorzaak | Oplossing |
|---|---|---|
| Taak blijft 10+ min in queued hangen | Serverbelasting | Probeer een ander model |
| failed zonder errordetail | Prompt gefilterd of modelfout | Herformuleer prompt |
| Video-URL geeft 403 | URL verlopen | Meteen downloaden |
| task_not_exist bij eerste poll Runway | Taak initialiseert nog (gedocumenteerd gedrag bij CometAPI) | Wacht 5s en probeer opnieuw |
| Kling retourneert "succeed" i.p.v. "succeeded" | API van Kling gebruikt niet-standaard statusstring | Verwerk beide in je polllogica |
Bron**: CometAPI video generation docs**, Kling Video docs
Node.js-versie
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);}
Een debugging-checklist
Wanneer een generatie faalt en je niet zeker weet waar te beginnen:
Voor tekstgeneratie:
- Is de API-sleutel gezet en in het formaat
Authorization: Bearer <key>? - Is
finish_reasoniets anders danstop? - Is
contentnull? Controleer offinish_reasontool_callsis - Is de output afgekapt? Controleer
finish_reason: "length"enusage.completion_tokens - Is de fout een 4xx (repareer het verzoek) of 5xx (opnieuw proberen)?
- Slaagt de output voor je validatie op applicatielaag? (stille fout)
Voor afbeeldingsgeneratie:
- Is de
data-array leeg? (contentfilter) - Heb je
response_formatgebruikt op GPT Image 2? (niet ondersteund — gebruikoutput_format) - Heb je
n > 1ingesteld op Qwen Image? (niet ondersteund) - Heb je de afbeelding gedownload voordat de URL verliep?
Voor videogeneratie:
- Zit de taak vast in
queued? (probeer een ander model) - Heb je het
error-veld gecontroleerd in de mislukte taakresponse? - Heb je de video gedownload voordat de URL verliep?
- Verwerk je zowel
"succeed"(Kling) als"succeeded"(Veo, Runway)?
FAQ
Q: Mijn request geeft 200 terug maar er is geen content. Wat is er gebeurd?
Controleer finish_reason. Als dit content_filter is, is de generatie geblokkeerd — het verzoek is geslaagd maar de output is onderdrukt. Als het tool_calls is, heeft het model een tool aangeroepen in plaats van tekst terug te geven, en content is bij ontwerp null. Als finish_reason stop is maar de content nog steeds leeg is, is dat een stille fout — log de volledige response en controleer je prompt.
Q: Hoe weet ik of mijn prompt wordt gefilterd?
Voor tekst: controleer finish_reason === "content_filter". Voor afbeeldingen: controleer of de data-array leeg is. Voor video: controleer of de taak kort na indienen de status failed bereikt zonder errordetail. Probeer in alle gevallen de prompt neutraler te formuleren.
Q: Wanneer moet ik een mislukt verzoek opnieuw proberen?
Probeer opnieuw bij 429 en 5xx met exponentiële backoff. Niet opnieuw proberen bij 4xx — een ongeldig verzoek lost zichzelf niet op. De uitzondering is 401 als je API-sleutels roteert.
Q: Wat is exponentiële backoff en waarom is het belangrijk?
In plaats van onmiddellijk opnieuw te proberen, wacht je steeds langer: 1s, 2s, 4s. Willekeurige jitter (+ random.random()) voorkomt dat meerdere clients synchroon opnieuw proberen. Dit is een standaardpraktijk voor elke API met rate limits — niet specifiek voor CometAPI.
Q: De videotask blijft 10 minuten in queued. Is deze mislukt?
Niet per se — wachtrijen kunnen vollopen bij belasting. Wacht tot je max_wait-drempel, gooi dan een TimeoutError en probeer een ander model. Log de taak-ID zodat je de status handmatig kunt controleren indien nodig.
Q: Hoe vang ik stille fouten?
Stille fouten vereisen validatie op applicatielaag — de API vertelt je niet dat de output semantisch onjuist is. Controleer of de output aan het verwachte formaat voldoet (geldige JSON, verwacht label, minimumlengte). Log de volledige output wanneer validatie faalt. De meest voorkomende oorzaken zijn ambigue prompts, genegeerde formateringseisen of inputs die te kort of te lang zijn voor de taak.
