Fehler bei KI-APIs unterscheiden sich von regulären API-Fehlern. Eine 200-Antwort bedeutet nicht, dass Ihre Generierung erfolgreich war. Ein Feld content mit null ist nicht immer ein Fehler. Und derselbe Prompt, der gestern noch funktioniert hat, kann heute scheitern, weil ein Anbieter seine Inhaltsrichtlinie aktualisiert hat.
Dieser Leitfaden erklärt, wie man KI-API-Fehler liest, was die einzelnen Fehlermodi tatsächlich bedeuten und wie man ein Fehlermanagement aufbaut, das Ihnen sagt, was kaputtging — statt nur, dass etwas kaputtging.
Hinweis: Modellnamen wie gpt-5.4 und gpt-5.4-mini sind Plattformbezeichner von CometAPI. Sie funktionieren nur über https://api.cometapi.com/v1 — nicht direkt über die APIs von OpenAI oder Anthropic. Siehe die vollständige Modellliste.
Warum Debugging von KI-APIs schwieriger ist als bei regulären APIs
Bei einer typischen REST-API bedeutet 200 Erfolg und 4xx, dass Sie etwas falsch gemacht haben. KI-APIs fügen eine dritte Kategorie hinzu: Soft-Fails — Antworten, die 200 zurückgeben, aber keinen nutzbaren Inhalt enthalten.
Drei Dinge können schiefgehen:
- Harte Fehler — HTTP-Fehler (4xx, 5xx). Die Anfrage wurde nicht abgeschlossen.
- Soft-Fehler — HTTP 200, aber
finish_reasonistcontent_filteroderlength, odercontentistnull. - Stiller Fehler — HTTP 200, der Inhalt sieht gut aus, aber die Ausgabe ist auf eine Weise falsch, die Sie erst in der Anwendungsschicht erkennen.
Die meisten Fehlerbehandlungen decken nur Fall 1 ab. Fälle 2 und 3 sind die Ursache der meisten Produktionsbugs.
Das Fehlerrückgabeformat verstehen
Der Text-Completions-Endpunkt gibt eine konsistente Fehlerstruktur zurück:
{ "error": { "message": "menschenlesbare Beschreibung (enthält oft die Request-ID)", "type": "comet_api_error", "param": "der problematische Parameter oder null", "code": "Fehlercode oder null" }}
Bild- und Video-Endpunkte liefern andere Fehlerformate — parsen Sie immer den Roh-Response-Body, anstatt eine feste Struktur über alle Endpunkte hinweg anzunehmen.
Das Feld message sagt Ihnen normalerweise genau, was falsch ist. Das Feld param sagt Ihnen, welcher Parameter es verursacht hat. Loggen Sie immer beide.
Die Bedeutung der einzelnen HTTP-Statuscodes kennen
| Status | Bedeutung | Häufige Ursache | Maßnahme |
|---|---|---|---|
| 400 | Bad Request | Modell fehlt, falscher Parameter für dieses Modell | Prüfen Sie error.param in der Antwort |
| 401 | Unauthorized | Falscher oder fehlender API-Schlüssel | Format Authorization: Bearer <key> prüfen |
| 429 | Rate Limited | Zu viele Anfragen | Exponentielles Backoff (siehe Schritt 4) |
| 500 | Serverfehler | Anbieterproblem oder fehlerhafter Request-Body | Mit Backoff erneut versuchen; Request-Format prüfen |
| 504 | Gateway Timeout | Anbieter brauchte zu lange | Erneut versuchen; schnelleres Modell erwägen |
Quelle**:* CometAPI chat completions docs
Die Unterscheidung 400 vs. 500 ist wichtig für die Retry-Logik. Ein 400 bedeutet, Ihre Anfrage ist falsch — erneutes Senden derselben Anfrage hilft nicht. 500 oder 504 bedeutet ein Serverproblem — erneute Versuche sind sinnvoll.
finish_reason prüfen — das am häufigsten übersehene Feld
Eine 200-Antwort mit finish_reason: "content_filter" bedeutet, dass Ihre Generierung blockiert wurde. Das Feld content ist null oder leer. Wenn Sie das nicht prüfen, gibt Ihre App stillschweigend nichts zurück.
| finish_reason | Bedeutung | Vorgehen | Maßnahme |
|---|---|---|---|
| stop | Normale Beendigung | Nichts — das ist Erfolg | Prüfen Sie error.param in der Antwort |
| length | Tokenlimit erreicht | max_tokens erhöhen oder Prompt kürzen | Format Authorization: Bearer <key> prüfen |
| content_filter | Durch Sicherheitsrichtlinie blockiert | Prompt umformulieren; bestimmte Namen/Themen meiden | Exponentielles Backoff (siehe Schritt 4) |
| tool_calls | Modell hat ein Tool aufgerufen statt Text zu liefern | Tool-Aufruf behandeln; content ist null | Mit Backoff erneut versuchen; Request-Format prüfen |
| 504 | Gateway-Zeitüberschreitung | Anbieter brauchte zu lange | Erneut versuchen; schnelleres Modell erwägen |
Quelle**:* 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 ist nicht gesetzt")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: """ Führt eine Chat-Anfrage mit vollständiger Fehler- und finish_reason-Behandlung aus. Gibt {"content": str, "finish_reason": str, "tool_calls": list | None} zurück Löst bei API-Fehlern Ausnahmen aus. """ 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-Fehler 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"Netzwerk-/Timeout-Fehler: {e}") raise choice = response.choices[0] finish_reason = choice.finish_reason if finish_reason == "content_filter": raise ValueError( f"Generierung durch Inhaltsfilter blockiert. " f"Modell: {model}. Prompt umformulieren." ) if finish_reason == "length": used = response.usage.completion_tokens if response.usage else "unknown" logging.warning(f"Ausgabe am Tokenlimit abgeschnitten. Verwendet {used} Token.") # Strukturierte Rückgabe, damit Aufrufer tool_calls explizit behandeln können return { "content": choice.message.content or "", "finish_reason": finish_reason, "tool_calls": choice.message.tool_calls, }# Verwendungresult = safe_complete( messages=[{"role": "user", "content": "Fassen Sie diesen Artikel zusammen: [Text]"}], model="gpt-5.4-mini")if result["finish_reason"] == "tool_calls": # Tool-Aufruf behandeln — content ist leer print("Modell möchte ein Tool aufrufen:", result["tool_calls"])else: print(result["content"])
Stille Fehler auf der Anwendungsebene erkennen
Stille Fehler sind am schwersten zu finden. Die API liefert 200, finish_reason ist stop, aber die Ausgabe ist semantisch falsch. Sie können sie nur auf der Anwendungsebene erkennen.
Häufige Muster:
def validate_completion(result: dict, task: str) -> str: """ Validierung auf Anwendungsebene für stille Fehler. Löst ValueError aus, wenn die Ausgabe grundlegenden Erwartungen nicht entspricht. """ content = result["content"].strip() # Leere Ausgabe, die kein Tool-Aufruf ist if not content and result["finish_reason"] != "tool_calls": raise ValueError(f"Leere Ausgabe für Aufgabe '{task}' mit finish_reason='{result['finish_reason']}'") # Aufgabenspezifische Prüfungen if task == "classify": valid_labels = {"positive", "negative", "neutral"} if content.lower() not in valid_labels: logging.warning( f"Unerwartete Klassifikationsausgabe: '{content}'. " f"Erwartet eine aus {valid_labels}. " f"Das Modell hat möglicherweise eine Erklärung statt eines Labels zurückgegeben." ) if task == "json_extract": import json try: json.loads(content) except json.JSONDecodeError: raise ValueError( f"JSON-Ausgabe erwartet, erhalten: '{content[:100]}...'. " f"Versuchen Sie, 'Antwort nur als gültiges JSON' zum Prompt hinzuzufügen, " f"oder verwenden Sie response_format={{\"type\": \"json_object\"}}." ) if task == "summarize" and len(content.split()) < 10: logging.warning( f"Auffällig kurze Zusammenfassung ({len(content.split())} Wörter). " f"Prüfen Sie, ob der Input zu kurz war oder das Modell die Aufgabe missverstanden hat." ) return content# Gesamtablauf mit Erkennung stiller Fehlerresult = safe_complete( messages=[{"role": "user", "content": "Als positiv/negativ/neutral klassifizieren: 'Great product!'"}], model="claude-haiku-4-5")label = validate_completion(result, task="classify")
Stille Fehler entstehen meist aus drei Ursachen: Der Prompt ist mehrdeutig, das Modell ignoriert Ihre Formatvorgaben oder der Input war für die Aufgabe zu kurz/zu lang. Das vollständige Loggen der Ausgabe bei Validierungsfehlern ist der schnellste Weg, herauszufinden, welche Ursache vorliegt.
Exponentielles Backoff für Ratenbegrenzungen hinzufügen
Rate-Limit-Fehler (429) sind temporär. Die richtige Reaktion ist, mit zunehmenden Verzögerungen zu warten und erneut zu versuchen — eine Standardpraxis für jede API mit Ratenbegrenzungen:
import timeimport randomfrom openai import RateLimitErrordef complete_with_retry( messages: list, model: str = "gpt-5.4-mini", max_retries: int = 3, **kwargs) -> dict: """Erneut versuchen bei Rate-Limits und Serverfehlern mit exponentiellem 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: nicht erneut versuchen, Anfrage ist falsch 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 verhindert Gleichlauf beim Retry logging.warning(f"Versuch {attempt + 1} fehlgeschlagen. Warte {wait:.1f}s vor erneutem Versuch.") time.sleep(wait) raise RuntimeError(f"Alle {max_retries} Versuche fehlgeschlagen") from last_error
Versuchen Sie es nicht erneut bei 400 oder 401 — das sind Client-Fehler, die sich nicht von selbst lösen.
Fehler bei der Bildgenerierung debuggen
Die Bildgenerierung hat eigene Fehlermodi zusätzlich zu den Standard-HTTP-Fehlern:
import base64import requestsdef generate_image_safe(prompt: str, model: str = "dall-e-3") -> dict: """ Generiert ein Bild mit vollständiger Fehlerbehandlung. Gibt {"url": str | None, "bytes": bytes | None, "blocked": bool} zurück """ api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY ist nicht gesetzt") 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"HTTP-Fehler bei der Bildgenerierung: {e.response.status_code} {e.response.text}") raise except requests.exceptions.Timeout: logging.error("Bildgenerierung nach 60s zeitüberschritten") raise data = response.json().get("data", []) if not data: logging.warning("Bildgenerierung lieferte leere Daten — Prompt wurde ggf. gefiltert.") return {"url": None, "bytes": None, "blocked": True} item = data[0] if "revised_prompt" in item: logging.info(f"Anbieter hat Prompt überarbeitet zu: {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 }
Bildspezifische Punkte, auf die Sie achten sollten:
| Symptom | Ursache | Maßnahme |
|---|---|---|
| Leeres Daten-Array | Prompt gefiltert | revised_prompt prüfen; umformulieren |
| response_format-Fehler bei GPT Image 2 | Parameter nicht unterstützt | Stattdessen output_format verwenden |
| n > 1 Fehler bei Qwen Image | Modellbeschränkung | Anfragen in einer Schleife ausführen |
| URL liefert später 403 | URL abgelaufen | Sofort nach Generierung herunterladen |
Quelle**:* CometAPI image generation docs
Fehler bei der Videogenerierung debuggen
Videogenerierung schlägt anders fehl, weil sie asynchron ist. Initialisieren Sie Statusvariablen vor der Schleife, damit die Timeout-Fehlermeldung immer korrekt formatiert ist:
def submit_and_poll_video( prompt: str, model: str = "veo3-fast", max_wait: int = 600) -> str: """Video-Aufgabe einreichen und bis zum Abschluss pollen. Gibt Video-URL zurück.""" api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY ist nicht gesetzt") 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-Einreichung fehlgeschlagen: {e.response.status_code} {e.response.text}") raise task_id = response.json()["id"] logging.info(f"Video-Task eingereicht: {task_id}") poll_url = f"https://api.cometapi.com/v1/videos/{task_id}" elapsed = 0 interval = 10 status = "unknown" # vor der Schleife initialisieren progress = 0 # vor der Schleife initialisieren 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 fehlgeschlagen: {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", "keine Fehlerdetails zurückgegeben") raise RuntimeError(f"Video-Task {task_id} fehlgeschlagen: {error_detail}") time.sleep(interval) elapsed += interval raise TimeoutError( f"Video-Task {task_id} wurde nicht innerhalb von {max_wait}s abgeschlossen. " f"Letzter Status: {status}, Fortschritt: {progress}%" )
Videospezifische Probleme:
| Symptom | Ursache | Maßnahme |
|---|---|---|
| Aufgabe 10+ Min in queued | Serverauslastung | Mit anderem Modell versuchen |
| failed ohne Fehlerdetail | Prompt gefiltert oder Modellfehler | Prompt umformulieren |
| Video-URL liefert 403 | URL abgelaufen | Sofort herunterladen |
| task_not_exist beim ersten Runway-Poll | Aufgabe initialisiert noch (CometAPI-dokumentiertes Verhalten) | 5s warten und erneut versuchen |
| Kling liefert "succeed" statt "succeeded" | Die Kling-API nutzt einen nicht standardisierten Status-String | Beide im Polling behandeln |
Quelle**:* CometAPI video generation docs**, Kling Video docs
Node.js-Version
import OpenAI from 'openai';const apiKey = process.env.COMETAPI_KEY;if (!apiKey) throw new Error('COMETAPI_KEY ist nicht gesetzt');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-Fehler ${err.status}: ${err.message}`); } else { console.error(`Server-/Netzwerkfehler: ${err.message}`); } throw err; } const choice = response.choices[0]; const finishReason = choice.finish_reason; if (finishReason === 'content_filter') { throw new Error(`Generierung durch Inhaltsfilter blockiert. Modell: ${model}`); } if (finishReason === 'length') { console.warn(`Ausgabe abgeschnitten. Verwendet ${response.usage?.completion_tokens ?? 'unknown'} Token.`); } 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) { // 4xx-Clientfehler nicht erneut versuchen if (err.status && err.status < 500) throw err; lastError = err; if (attempt < maxRetries - 1) { const wait = (2 ** attempt + Math.random()) * 1000; console.warn(`Versuch ${attempt + 1} fehlgeschlagen. Neuer Versuch in ${(wait / 1000).toFixed(1)}s`); await new Promise(r => setTimeout(r, wait)); } } } throw new Error(`Alle ${maxRetries} Versuche fehlgeschlagen: ${lastError?.message}`);}// Verwendungconst result = await safeComplete( [{ role: 'user', content: 'Als positiv/negativ/neutral klassifizieren: "Great product!"' }], 'claude-haiku-4-5');if (result.finishReason === 'tool_calls') { console.log('Tool-Aufruf angefordert:', result.toolCalls);} else { console.log(result.content);}
Eine Debugging-Checkliste
Wenn eine Generierung fehlschlägt und Sie nicht wissen, wo Sie anfangen sollen:
Für Textgenerierung:
- Ist der API-Schlüssel gesetzt und im Format
Authorization: Bearer <key>? - Ist
finish_reasonetwas anderes alsstop? - Ist
contentnull? Prüfen Sie, obfinish_reasontool_callsist. - Wurde die Ausgabe abgeschnitten? Prüfen Sie
finish_reason: "length"undusage.completion_tokens. - Handelt es sich um einen 4xx-Fehler (Request korrigieren) oder 5xx (erneut versuchen)?
- Besteht die Ausgabe Ihre Validierung auf Anwendungsebene? (stiller Fehler)
Für Bildgenerierung:
- Ist das Array
dataleer? (Inhaltsfilter) - Haben Sie
response_formatbei GPT Image 2 verwendet? (nicht unterstützt — verwenden Sieoutput_format) - Haben Sie
n > 1bei Qwen Image gesetzt? (nicht unterstützt) - Haben Sie das Bild heruntergeladen, bevor die URL abgelaufen ist?
Für Videogenerierung:
- Steckt die Aufgabe in
queuedfest? (anderes Modell versuchen) - Haben Sie das Feld
errorin der fehlgeschlagenen Task-Antwort geprüft? - Haben Sie das Video heruntergeladen, bevor die URL abgelaufen ist?
- Behandeln Sie sowohl
"succeed"(Kling) als auch"succeeded"(Veo, Runway)?
FAQ
F: Meine Anfrage gibt 200 zurück, aber es gibt keinen Inhalt. Was ist passiert?
Prüfen Sie finish_reason. Wenn es content_filter ist, wurde die Generierung blockiert — die Anfrage war erfolgreich, aber die Ausgabe wurde unterdrückt. Wenn es tool_calls ist, hat das Modell ein Tool aufgerufen statt Text zurückzugeben, und content ist absichtlich null. Wenn finish_reason stop ist, der Inhalt aber trotzdem leer ist, ist das ein stiller Fehler — loggen Sie die vollständige Antwort und prüfen Sie Ihren Prompt.
F: Woher weiß ich, ob mein Prompt gefiltert wird?
Für Text: Prüfen Sie finish_reason === "content_filter". Für Bilder: Prüfen Sie, ob das Array data leer ist. Für Videos: Prüfen Sie, ob die Aufgabe kurz nach dem Absenden den Status failed erreicht, ohne Fehlerdetails. In allen Fällen versuchen Sie, den Prompt neutraler zu formulieren.
F: Wann sollte ich eine fehlgeschlagene Anfrage erneut versuchen?
Bei 429 und 5xx mit exponentiellem Backoff erneut versuchen. Nicht bei 4xx — eine fehlerhafte Anfrage behebt sich nicht von selbst. Die Ausnahme ist 401, wenn Sie API-Schlüssel rotieren.
F: Was ist exponentielles Backoff und warum ist das wichtig?
Anstatt sofort erneut zu versuchen, warten Sie zunehmend länger: 1s, 2s, 4s. Das Hinzufügen von Jitter (+ random.random()) verhindert, dass mehrere Clients synchron erneut versuchen. Das ist eine Standardpraxis für jede API mit Ratenbegrenzungen — nicht spezifisch für CometAPI.
F: Die Videoaufgabe steckt seit 10 Minuten in queued. Ist sie fehlgeschlagen?
Nicht unbedingt — unter Last können sich Warteschlangen aufbauen. Warten Sie bis zu Ihrer max_wait-Schwelle, dann lösen Sie einen TimeoutError aus und versuchen es mit einem anderen Modell erneut. Loggen Sie die Task-ID, damit Sie den Status bei Bedarf manuell prüfen können.
F: Wie fange ich stille Fehler ab?
Stille Fehler erfordern Validierung auf Anwendungsebene — die API sagt Ihnen nicht, dass die Ausgabe semantisch falsch ist. Prüfen Sie, dass die Ausgabe dem erwarteten Format entspricht (gültiges JSON, erwartetes Label, Mindestlänge). Loggen Sie die vollständige Ausgabe, wenn die Validierung fehlschlägt. Die häufigsten Ursachen sind mehrdeutige Prompts, ignorierte Formatvorgaben oder Inputs, die für die Aufgabe zu kurz oder zu lang sind.
