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

Jak debugować nieudane generacje w API AI

CometAPI
AnnaJun 4, 2026
Jak debugować nieudane generacje w API AI

Awaria AI API różni się od zwykłej awarii API. Odpowiedź 200 nie oznacza, że generowanie się powiodło. Pole content równe null nie zawsze jest błędem. A ten sam prompt, który działał wczoraj, dziś może się wyłożyć, bo dostawca zaktualizował politykę treści.

Ten przewodnik omawia, jak czytać błędy AI API, co oznacza każda forma niepowodzenia i jak zbudować obsługę błędów, która powie ci, co się zepsuło — zamiast tylko tego, że coś się zepsuło.

Uwaga: Nazwy modeli takie jak gpt-5.4 i gpt-5.4-mini użyte w tym artykule to identyfikatory platformy CometAPI. Działają tylko przez https://api.cometapi.com/v1 — nie bezpośrednio przez API OpenAI ani Anthropic. Zobacz pełną listę modeli.

Dlaczego debugowanie AI API jest trudniejsze niż debugowanie zwykłego API

W typowym REST API, 200 oznacza sukces, a 4xx oznacza, że to po twojej stronie. AI API dodają trzecią kategorię: miękkie błędy — odpowiedzi, które zwracają 200, ale nie zawierają użytecznej treści.

Mogą pójść źle trzy rzeczy:

  1. twarde niepowodzenie — błąd HTTP (4xx, 5xx). Żądanie nie zostało zrealizowane.
  2. miękkie niepowodzenie — HTTP 200, ale finish_reason to content_filter lub length, albo content jest null.
  3. ciche niepowodnienie — HTTP 200, treść wygląda dobrze, ale wynik jest błędny w sposób wykrywalny dopiero na poziomie aplikacji.

Większość obsługi błędów obejmuje tylko przypadek 1. Przypadki 2 i 3 to miejsca, gdzie pojawia się większość problemów produkcyjnych.

Zrozum format odpowiedzi błędu

Endpoint uzupełniania tekstu zwraca spójny format błędu:

{  "error": {    "message": "human-readable description (often includes request id)",    "type": "comet_api_error",    "param": "the_problematic_parameter_or_null",    "code": "error_code_or_null"  }}

Endpointy obrazu i wideo zwracają różne formaty błędów — zawsze parsuj surowe body odpowiedzi zamiast zakładać stały format między endpointami.

Pole message zazwyczaj dokładnie wskazuje, co jest nie tak. Pole param mówi, który parametr to spowodował. Zawsze loguj oba.

Poznaj znaczenie każdego kodu statusu HTTP

StatusZnaczenieCzęsta przyczynaNaprawa
400Bad requestBrak modelu, błędny parametr dla tego modeluSprawdź error.param w odpowiedzi
401UnauthorizedBłędny lub brakujący klucz APIZweryfikuj format Authorization: Bearer <key>
429Rate limitedZbyt wiele żądańExponential backoff (zob. Krok 4)
500Server errorProblem po stronie dostawcy lub błędne bodyPonów z backoffem; sprawdź format żądania
504Gateway timeoutDostawca odpowiadał zbyt długoPonów; rozważ szybszy model

*Źródło**: CometAPI chat completions docs

Różnica między 400 a 500 ma znaczenie dla strategii ponawiania. 400 oznacza, że żądanie jest błędne — ponowienie tego samego nie pomoże. 500 lub 504 oznacza problem serwera — ponawianie ma sens.

Sprawdź finish_reason — najczęściej pomijane pole

Odpowiedź 200 z finish_reason: "content_filter" oznacza, że generowanie zostało zablokowane. Pole content będzie null lub puste. Jeśli tego nie sprawdzisz, twoja aplikacja po cichu zwróci pustkę.

finish_reasonZnaczenieCo zrobićNaprawa
stopNormalne zakończenieNic — to sukcesSprawdź error.param w odpowiedzi
lengthOsiągnięto limit tokenówZwiększ max_tokens lub skróć promptZweryfikuj format Authorization: Bearer <key>
content_filterZablokowane przez politykę bezpieczeństwaPrzeformułuj prompt; unikaj konkretnych nazw/tematówExponential backoff (zob. Krok 4)
tool_callsModel wywołał narzędzie zamiast zwrócić tekstObsłuż wywołanie narzędzia; content będzie nullPonów z backoffem; sprawdź format żądania
504Gateway timeoutDostawca odpowiadał zbyt długoPonów; rozważ szybszy model

*Źródło**: CometAPI chat completions docs

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"])

Wykrywaj ciche niepowodzenia na poziomie aplikacji

Ciche niepowodzenia są najtrudniejsze do złapania. API zwraca 200, finish_reason to stop, ale wyjście jest semantycznie błędne. Możesz je wykryć tylko na poziomie aplikacji.

Typowe wzorce:

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")

Ciche niepowodzenia zazwyczaj wynikają z jednego z trzech źródeł: prompt jest niejednoznaczny, model zignorował instrukcje formatowania, albo wejście było zbyt krótkie/długie do zadania. Logowanie pełnego wyjścia, gdy walidacja nie przejdzie, najszybciej pokaże, która to przyczyna.

Dodaj exponential backoff dla limitów zapytań

Błędy limitu zapytań (429) są tymczasowe. Właściwą reakcją jest odczekanie i ponowienie z rosnącymi opóźnieniami — standardowa praktyka dla każdego API z limitami:

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

Nie ponawiaj przy 400 ani 401 — to błędy po stronie klienta, które same się nie rozwiążą.

Debugowanie błędów generowania obrazów

Generowanie obrazów ma własne tryby niepowodzeń, oprócz standardowych błędów HTTP:

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    }

Problemy specyficzne dla obrazów, na które warto uważać:

ObjawPrzyczynaNaprawa
Pusta tablica dataPrzefiltrowany promptSprawdź revised_prompt; przeformułuj
błąd response_format na GPT Image 2Parametr nieobsługiwanyUżyj output_format zamiast
n > 1 błąd na Qwen ImageOgraniczenie modeluZastąp pętlą wielu żądań
URL zwraca 403 późniejURL wygasłPobierz od razu po wygenerowaniu

*Źródło**: CometAPI image generation docs

Debugowanie błędów generowania wideo

Generowanie wideo psuje się inaczej, bo jest asynchroniczne. Zainicjalizuj zmienne statusu przed pętlą, aby komunikat o timeout zawsze był poprawnie sformatowany:

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}%"    )

Problemy specyficzne dla wideo:

ObjawPrzyczynaNaprawa
Zadanie utknęło w queued 10+ minObciążenie serweraPonów na innym modelu
failed bez szczegółu błęduPrompt przefiltrowany lub błąd modeluPrzeformułuj prompt
URL wideo zwraca 403URL wygasłPobierz natychmiast
task_not_exist przy pierwszym pollu RunwayZadanie wciąż się inicjalizuje (zachowanie CometAPI)Poczekaj 5s i ponów
Kling zwraca "succeed" zamiast "succeeded"API Kling używa niestandardowego statusuObsłuż oba w logice pollingu

Źródło**: CometAPI video generation docs**,* Kling Video docs

Wersja 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);}

Lista kontrolna debugowania

Gdy generowanie się nie powiedzie i nie wiesz, od czego zacząć:

Dla generowania tekstu:

  • Czy klucz API jest ustawiony i w formacie Authorization: Bearer <key>?
  • Czy finish_reason ma wartość inną niż stop?
  • Czy content jest null? Sprawdź, czy finish_reason to tool_calls
  • Czy wyjście zostało ucięte? Sprawdź finish_reason: "length" i usage.completion_tokens
  • Czy błąd to 4xx (napraw żądanie) czy 5xx (ponów)?
  • Czy wyjście przechodzi walidację na poziomie aplikacji? (ciche niepowodnienie)

Dla generowania obrazów:

  • Czy tablica data jest pusta? (filtr treści)
  • Czy użyłeś response_format na GPT Image 2? (nieobsługiwane — użyj output_format)
  • Czy ustawiłeś n > 1 na Qwen Image? (nieobsługiwane)
  • Czy pobrałeś obraz, zanim URL wygasł?

Dla generowania wideo:

  • Czy zadanie utknęło w queued? (spróbuj innego modelu)
  • Czy sprawdziłeś pole error w odpowiedzi nieudanego zadania?
  • Czy pobrałeś wideo, zanim URL wygasł?
  • Czy obsługujesz zarówno "succeed" (Kling), jak i "succeeded" (Veo, Runway)?

FAQ

P: Moje żądanie zwraca 200, ale nie ma treści. Co się stało?

Sprawdź finish_reason. Jeśli to content_filter, generowanie zostało zablokowane — żądanie się powiodło, ale wyjście zostało wyciszone. Jeśli to tool_calls, model wywołał narzędzie zamiast zwrócić tekst i content z definicji jest null. Jeśli finish_reason to stop, ale treść wciąż jest pusta, to ciche niepowodnienie — zaloguj całą odpowiedź i sprawdź prompt.

P: Skąd wiem, że mój prompt jest filtrowany?

Dla tekstu: sprawdź finish_reason === "content_filter". Dla obrazów: sprawdź, czy tablica data jest pusta. Dla wideo: sprawdź, czy zadanie przechodzi w status failed krótko po wysłaniu bez szczegółów błędu. W każdym przypadku spróbuj przeformułować prompt na bardziej neutralny.

P: Kiedy powinienem ponawiać nieudane żądanie?

Ponawiaj przy 429 i 5xx używając exponential backoff. Nie ponawiaj przy 4xx — błędne żądanie samo się nie naprawi. Wyjątkiem jest 401, jeśli rotujesz klucze API.

P: Czym jest exponential backoff i dlaczego ma znaczenie?

Zamiast ponawiać natychmiast, czekasz coraz dłużej: 1 s, 2 s, 4 s. Dodanie losowego jittera (+ random.random()) zapobiega zsynchronizowanemu „stadnemu” ponawianiu wielu klientów. To standardowa praktyka dla każdego API z limitami — nie specyficzna dla CometAPI.

P: Zadanie wideo utknęło w queued przez 10 minut. Czy to porażka?

Niekoniecznie — kolejki mogą się zapchać przy obciążeniu. Poczekaj do progu max_wait, potem rzuć TimeoutError i spróbuj innego modelu. Zaloguj ID zadania, aby w razie potrzeby ręcznie sprawdzić status.

P: Jak łapać ciche niepowodzenia?

Ciche niepowodzenia wymagają walidacji na poziomie aplikacji — API nie powie ci, że wynik jest semantycznie błędny. Sprawdź, czy wyjście spełnia oczekiwany format (poprawny JSON, oczekiwana etykieta, minimalna długość). Loguj pełne wyjście, gdy walidacja zawiedzie. Najczęstsze przyczyny to niejednoznaczne prompty, zignorowane instrukcje formatu lub wejścia zbyt krótkie albo zbyt długie do zadania.

Gotowy na obniżenie kosztów rozwoju AI o 20%?

Zacznij za darmo w kilka minut. Dołączone kredyty na bezpłatny okres próbny. Karta kredytowa nie jest wymagana.

Czytaj więcej