Сбои в 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, но без пригодного к использованию контента.
Три вещи могут пойти не так:
- Жесткий отказ — HTTP‑ошибка (4xx, 5xx). Запрос не завершился.
- Мягкий отказ — HTTP 200, но
finish_reason—content_filterилиlength, либоcontent—null. - Тихий отказ — 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‑статус
| Status | Meaning | Common cause | Fix |
|---|---|---|---|
| 400 | Bad request | Отсутствует модель, неверный параметр для этой модели | Проверьте error.param в ответе |
| 401 | Unauthorized | Неверный или отсутствующий API‑ключ | Проверьте формат Authorization: Bearer <key> |
| 429 | Rate limited | Слишком много запросов | Экспоненциальная задержка (см. Шаг 4) |
| 500 | Server error | Проблема на стороне провайдера или некорректное тело запроса | Повтор с задержкой; проверьте формат запроса |
| 504 | Gateway timeout | Провайдер слишком долго отвечал | Повторите; рассмотрите более быструю модель |
Источник**: CometAPI chat completions docs
Различие между 400 и 500 важно для логики повторов. 400 означает, что ваш запрос неверен — повтор того же запроса не поможет. 500 или 504 означает проблему на стороне сервера — повтор имеет смысл.
Проверяйте finish_reason — самое недооцененное поле
Ответ 200 с finish_reason: "content_filter" означает, что генерация была заблокирована. Поле content будет null или пустым. Если вы это не проверите, ваше приложение молча вернет пустоту.
| finish_reason | Meaning | What to do | Fix |
|---|---|---|---|
| stop | Нормальное завершение | Ничего — это успех | Проверьте error.param в ответе |
| length | Достигнут лимит токенов | Увеличьте max_tokens или сократите подсказку | Проверьте формат Authorization: Bearer <key> |
| content_filter | Заблокировано политикой безопасности | Переформулируйте подсказку; избегайте конкретных имен/тем | Экспоненциальная задержка (см. Шаг 4) |
| tool_calls | Модель вызвала инструмент вместо текста | Обработайте вызов инструмента; content будет null | Повтор с задержкой; проверьте формат запроса |
| 504 | Gateway timeout | Провайдер слишком долго отвечал | Повторите; рассмотрите более быструю модель |
Источник**: 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"])
Обнаруживайте тихие отказы на уровне приложения
Тихие отказы — самые трудные. API возвращает 200, finish_reason — stop, но содержательно вывод неверен. Поймать их можно только на уровне приложения.
Распространенные паттерны:
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 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
Не повторяйте при 400 или 401 — это клиентские ошибки, которые сами не исчезнут.
Отладка сбоев генерации изображений
Генерация изображений имеет свои режимы отказов сверх стандартных HTTP‑ошибок:
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 }
Специфические проблемы для изображений:
| Symptom | Cause | Fix |
|---|---|---|
| Пустой массив data | Подсказка отфильтрована | Проверьте revised_prompt; переформулируйте |
| Ошибка response_format на GPT Image 2 | Параметр не поддерживается | Используйте output_format |
| n > 1 ошибка на Qwen Image | Ограничение модели | Делайте запросы в цикле |
| URL позже возвращает 403 | URL истек | Скачайте сразу после генерации |
Источник**: 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}%" )
Проблемы, специфичные для видео:
| Symptom | Cause | Fix |
|---|---|---|
| Задача застряла в queued 10+ минут | Нагрузка на сервер | Повторите с другой моделью |
| failed без деталей ошибки | Подсказка отфильтрована или ошибка модели | Переформулируйте подсказку |
| URL видео возвращает 403 | URL истек | Скачайте немедленно |
| 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_reason — stop, но контент все равно пуст, это тихий отказ — залогируйте полный ответ и проверьте вашу подсказку.
Вопрос: Как узнать, что моя подсказка фильтруется?
Для текста: проверьте 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, ожидаемая метка, минимальная длина). Логируйте полный вывод при провале валидации. Самые частые причины — двусмысленные подсказки, игнорирование инструкций по формату или слишком короткий/длинный ввод для задачи.
