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

Как отлаживать неудавшиеся генерации в API ИИ

CometAPI
AnnaJun 4, 2026
Как отлаживать неудавшиеся генерации в API ИИ

Сбои в AI API отличаются от обычных сбоев API. Ответ 200 не означает, что генерация удалась. Поле null в контенте — не всегда ошибка. И один и тот же запрос, который работал вчера, сегодня может не сработать, потому что провайдер обновил политику модерации контента.

В этом руководстве разобрано, как читать ошибки AI API, что на самом деле означает каждый режим отказа, и как выстроить обработку ошибок, которая подскажет, что именно сломалось, а не просто что «что‑то» сломалось.

Примечание: Названия моделей, такие как gpt-5.4 и gpt-5.4-mini, используемые в этой статье, — это идентификаторы платформы CometAPI. Они работают только через https://api.cometapi.com/v1 — не напрямую через API OpenAI или Anthropic. См. полный список моделей.

Почему отладка AI API сложнее, чем обычных API

В типичном REST API код 200 означает успех, а 4xx — что вы что‑то сделали неправильно. AI API добавляют третью категорию: «мягкие отказы» — ответы с 200, но без пригодного к использованию контента.

Три вещи могут пойти не так:

  1. Жесткий отказ — HTTP‑ошибка (4xx, 5xx). Запрос не завершился.
  2. Мягкий отказ — HTTP 200, но finish_reasoncontent_filter или length, либо contentnull.
  3. Тихий отказ — HTTP 200, контент выглядит нормально, но вывод неверен по смыслу, и это можно поймать только на уровне приложения.

Большинство обработчиков ошибок покрывают только случай 1. Случаи 2 и 3 — там, где живет большинство продакшн‑багов.

Понимайте формат ответа с ошибкой

Эндпоинт текстовых дополнений возвращает единый формат ошибки:

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

Эндпоинты изображений и видео возвращают другие форматы ошибок — всегда парсите «сырое» тело ответа, а не предполагайте единый формат для всех эндпоинтов.

Поле message обычно прямо указывает, что не так. Поле param говорит, какой параметр вызвал проблему. Всегда логируйте оба.

Знайте, что означает каждый HTTP‑статус

StatusMeaningCommon causeFix
400Bad requestОтсутствует модель, неверный параметр для этой моделиПроверьте error.param в ответе
401UnauthorizedНеверный или отсутствующий API‑ключПроверьте формат Authorization: Bearer <key>
429Rate limitedСлишком много запросовЭкспоненциальная задержка (см. Шаг 4)
500Server errorПроблема на стороне провайдера или некорректное тело запросаПовтор с задержкой; проверьте формат запроса
504Gateway timeoutПровайдер слишком долго отвечалПовторите; рассмотрите более быструю модель

Источник**: CometAPI chat completions docs

Различие между 400 и 500 важно для логики повторов. 400 означает, что ваш запрос неверен — повтор того же запроса не поможет. 500 или 504 означает проблему на стороне сервера — повтор имеет смысл.

Проверяйте finish_reason — самое недооцененное поле

Ответ 200 с finish_reason: "content_filter" означает, что генерация была заблокирована. Поле content будет null или пустым. Если вы это не проверите, ваше приложение молча вернет пустоту.

finish_reasonMeaningWhat to doFix
stopНормальное завершениеНичего — это успехПроверьте error.param в ответе
lengthДостигнут лимит токеновУвеличьте max_tokens или сократите подсказкуПроверьте формат Authorization: Bearer <key>
content_filterЗаблокировано политикой безопасностиПереформулируйте подсказку; избегайте конкретных имен/темЭкспоненциальная задержка (см. Шаг 4)
tool_callsМодель вызвала инструмент вместо текстаОбработайте вызов инструмента; content будет nullПовтор с задержкой; проверьте формат запроса
504Gateway timeoutПровайдер слишком долго отвечалПовторите; рассмотрите более быструю модель

Источник**: 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"])

Обнаруживайте тихие отказы на уровне приложения

Тихие отказы — самые трудные. API возвращает 200, finish_reasonstop, но содержательно вывод неверен. Поймать их можно только на уровне приложения.

Распространенные паттерны:

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

Тихие отказы обычно возникают из‑за одного из трех источников: двусмысленная подсказка, модель игнорирует ваши инструкции по формату или вход слишком короткий/длинный для задачи. Логирование полного вывода при провале валидации — самый быстрый способ понять, что именно произошло.

Добавьте экспоненциальную задержку при лимитах скорости

Ошибки лимитов (429) — временные. Правильная реакция — подождать и повторить с увеличивающимися задержками — стандартная практика для любого API с ограничениями по скорости:

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

Не повторяйте при 400 или 401 — это клиентские ошибки, которые сами не исчезнут.

Отладка сбоев генерации изображений

Генерация изображений имеет свои режимы отказов сверх стандартных 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    }

Специфические проблемы для изображений:

SymptomCauseFix
Пустой массив dataПодсказка отфильтрованаПроверьте revised_prompt; переформулируйте
Ошибка response_format на GPT Image 2Параметр не поддерживаетсяИспользуйте output_format
n > 1 ошибка на Qwen ImageОграничение моделиДелайте запросы в цикле
URL позже возвращает 403URL истекСкачайте сразу после генерации

Источник**: CometAPI image generation docs

Отладка сбоев генерации видео

Генерация видео падает иначе, потому что она асинхронная. Инициализируйте переменные статуса перед циклом, чтобы сообщение об ошибке таймаута всегда было корректно сформировано:

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

Проблемы, специфичные для видео:

SymptomCauseFix
Задача застряла в queued 10+ минутНагрузка на серверПовторите с другой моделью
failed без деталей ошибкиПодсказка отфильтрована или ошибка моделиПереформулируйте подсказку
URL видео возвращает 403URL истекСкачайте немедленно
task_not_exist на Runway при первом опросеЗадача еще инициализируется (поведение, задокументированное CometAPI)Подождите 5 с и повторите
Kling возвращает "succeed", а не "succeeded"В API Kling нестандартная строка статусаОбрабатывайте оба варианта в логике опроса

Источник**: CometAPI video generation docs**, Kling Video docs

Версия для 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);}

Чек‑лист по отладке

Когда генерация падает, и вы не знаете, с чего начать:

Для генерации текста:

  • Установлен ли API‑ключ и используется ли формат Authorization: Bearer <key>?
  • finish_reason отличается от stop?
  • Поле content равно null? Проверьте, не finish_reason ли это tool_calls
  • Был ли вывод обрезан? Проверьте finish_reason: "length" и usage.completion_tokens
  • Это ошибка 4xx (исправьте запрос) или 5xx (повторите)?
  • Проходит ли вывод вашу проверку на уровне приложения? (тихий отказ)

Для генерации изображений:

  • Пустой массив data? (контент отфильтрован)
  • Использовали response_format с GPT Image 2? (не поддерживается — используйте output_format)
  • Установили n > 1 с Qwen Image? (не поддерживается)
  • Скачали изображение до истечения срока действия URL?

Для генерации видео:

  • Задача застряла в queued? (попробуйте другую модель)
  • Проверили поле error в ответе для упавшей задачи?
  • Скачали видео до истечения срока действия URL?
  • Обрабатываете и "succeed" (Kling), и "succeeded" (Veo, Runway)?

FAQ

Вопрос: Мой запрос возвращает 200, но нет содержимого. Что случилось?

Проверьте finish_reason. Если это content_filter, генерация была заблокирована — запрос прошел, но вывод подавлен. Если это tool_calls, модель вызвала инструмент вместо текста, и content — null по задумке. Если finish_reasonstop, но контент все равно пуст, это тихий отказ — залогируйте полный ответ и проверьте вашу подсказку.

Вопрос: Как узнать, что моя подсказка фильтруется?

Для текста: проверьте finish_reason === "content_filter". Для изображений: проверьте, пустой ли массив data. Для видео: проверьте, не уходит ли задача в статус failed вскоре после отправки без деталей ошибки. Во всех случаях попробуйте переформулировать подсказку более нейтрально.

Вопрос: Когда стоит повторять упавший запрос?

Повторяйте на 429 и 5xx, используя экспоненциальную задержку. Не повторяйте на 4xx — плохой запрос сам не исправится. Исключение — 401, если вы ротируете API‑ключи.

Вопрос: Что такое экспоненциальная задержка и почему это важно?

Вместо немедленных повторов вы ждете все дольше: 1 с, 2 с, 4 с. Добавление случайного джиттера (+ random.random()) предотвращает синхронные повторы множеством клиентов. Это стандартная практика для любого API с лимитами — не специфично для CometAPI.

Вопрос: Задача видео висит в queued 10 минут. Это сбой?

Не обязательно — очереди могут расти под нагрузкой. Ждите до порога max_wait, затем бросайте TimeoutError и пробуйте другую модель. Логируйте ID задачи, чтобы при необходимости проверить статус вручную.

Вопрос: Как ловить тихие отказы?

Тихие отказы требуют проверки на уровне приложения — API не скажет, что вывод семантически неверен. Проверьте, что вывод соответствует ожидаемому формату (валидный JSON, ожидаемая метка, минимальная длина). Логируйте полный вывод при провале валидации. Самые частые причины — двусмысленные подсказки, игнорирование инструкций по формату или слишком короткий/длинный ввод для задачи.

Готовы сократить затраты на AI-разработку на 20%?

Начните бесплатно за несколько минут. Пробные кредиты включены. Карта не нужна.

Читать далее