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

失敗した AI API 生成のデバッグ方法

CometAPI
AnnaJun 4, 2026
失敗した AI API 生成のデバッグ方法

AI APIの障害は、通常のAPIの障害とは異なります。200が返ってきても、生成が成功したとは限りません。content フィールドが null でも、必ずしもエラーではありません。そして、昨日うまくいった同じプロンプトが、プロバイダーのコンテンツポリシー更新により今日は失敗することもあります。

このガイドでは、AI APIのエラーの読み方、各失敗モードの意味、そして「何が壊れたのか」を特定できるエラーハンドリングの作り方を解説します。「何かが壊れた」だけで終わらせないために。

Note: この記事で使用している gpt-5.4gpt-5.4-mini のようなモデル名は、CometAPI のプラットフォーム識別子です。これらは https://api.cometapi.com/v1 経由でのみ動作し、OpenAI や Anthropic のAPIを直接叩いても動作しません。詳しくは モデル一覧 を参照してください。

なぜAI APIのデバッグは通常のAPIのデバッグより難しいのか

一般的なREST APIでは、200は成功、4xxはクライアントの誤りを意味します。AI APIには第3のカテゴリ、つまりHTTP 200を返すものの、使えるコンテンツが含まれていない「ソフトフェイル」があります。

起こりうる問題は3つあります:

  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 を確認
401UnauthorizedAPIキーが不正または欠落Authorization: Bearer 形式を確認
429Rate limitedリクエスト過多指数バックオフ(手順4参照)
500Server errorプロバイダー側の問題、または不正なリクエストボディバックオフつきで再試行;リクエスト形式確認
504Gateway timeoutプロバイダーが時間内に応答できず再試行;より高速なモデルを検討

出典**:** CometAPI chat completions docs

リトライ戦略では、400500 の違いが重要です。400 はリクエストが誤っていることを意味し、同じリクエストを再試行しても状況は改善しません。500504 はサーバー側の問題を示すため、再試行に意味があります。

finish_reason を確認する — 最も見落とされがちなフィールド

finish_reason: "content_filter" でHTTP 200のレスポンスは、生成がブロックされたことを意味します。content フィールドは null または空になります。これを確認しないと、アプリは何も返さずに黙って終わってしまいます。

finish_reasonMeaningWhat to doFix
stop正常終了何もしない — 成功ですレスポンスの error.param を確認
lengthトークン上限に到達max_tokens を増やすか、プロンプトを短くするAuthorization: Bearer 形式を確認
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")

サイレントフェイルの主因はたいてい次の3つです。プロンプトの曖昧さ、モデルがフォーマット指示を無視した、またはタスクに対して入力が短すぎる/長すぎる。検証に失敗したときに出力全体をログに残すのが、どれに該当するかを最速で診断する方法です。

レート制限に指数バックオフを追加する

レート制限エラー(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

400401 では再試行しないでください — これらはクライアントエラーであり、放置しても解決しません。

画像生成の失敗をデバッグする

画像生成には、標準的な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 を確認;言い換える
GPT Image 2 での response_format エラー未対応のパラメータoutput_format を使用
Qwen Image で n > 1 エラーモデルの制約ループして個別にリクエストする
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}%"    )

動画特有の問題:

SymptomCauseFix
queued のまま10分以上進まないサーバー負荷別モデルで再試行
エラー詳細なしで failedプロンプトがフィルタリング、またはモデルエラープロンプトを言い換える
動画URLが403を返すURLの有効期限切れすぐにダウンロードする
Runway の最初のポーリングで task_not_existタスク初期化中(CometAPIのドキュメント化された挙動)5秒待って再試行
Kling が "succeed" を返すKling のAPIは非標準のステータス文字列を使用ポーリングで両方を扱う

出典**:** 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_reasonstop 以外になっていないか?
  • content が null か? finish_reasontool_calls かどうかを確認
  • 出力が切り詰められていないか? finish_reason: "length"usage.completion_tokens を確認
  • エラーは4xx(リクエスト修正)か5xx(再試行)か?
  • 出力はアプリケーション層の検証(サイレントフェイル検出)を通過しているか?

画像生成:

  • data 配列が空か?(コンテンツフィルター)
  • GPT Image 2 で response_format を使っていないか?(未対応 — output_format を使用)
  • Qwen Image で n > 1 を指定していないか?(未対応)
  • URL の有効期限切れ前に画像をダウンロードしたか?

動画生成:

  • タスクが queued に留まっていないか?(別モデルを試す)
  • 失敗したタスクのレスポンスで error フィールドを確認したか?
  • URL の有効期限切れ前に動画をダウンロードしたか?
  • "succeed"(Kling)と "succeeded"(Veo, Runway)の両方を扱っているか?

FAQ

Q: 200が返ってくるのにコンテンツがありません。何が起きていますか?

finish_reason を確認してください。content_filter なら生成がブロックされています — リクエスト自体は成功していますが出力が抑止されました。tool_calls なら、テキストの代わりにツールを呼び出しており、設計上 content は null です。finish_reasonstop なのに内容が空ならサイレントフェイルです — レスポンス全体をログに残し、プロンプトを確認してください。

Q: プロンプトがフィルタリングされているかどうかを知るには?

テキスト: finish_reason === "content_filter" を確認。画像: data 配列が空かを確認。動画: 送信直後に failed になりエラー詳細がない場合があります。いずれも、より中立的な表現に言い換えてみてください。

Q: いつ失敗したリクエストを再試行すべきですか?

4295xx では指数バックオフで再試行します。4xx では再試行しないでください — 不正なリクエストは自然には直りません。例外はAPIキーをローテーションする場合の 401 です。

Q: 指数バックオフとは何で、なぜ重要ですか?

すぐに再試行するのではなく、1秒、2秒、4秒…のように待機時間を徐々に延ばします。ランダムジッター(+ random.random())を加えると、多数のクライアントが同時に再試行するのを防げます。これはCometAPI 特有ではなく、レート制限のあるAPI全般の標準手法です。

Q: 動画タスクが queued のまま10分間止まっています。失敗ですか?

必ずしもそうではありません — 負荷によってキューが滞留することがあります。max_wait のしきい値までは待機し、その後は TimeoutError を投げて別モデルで再試行してください。手動で確認できるよう、タスクIDをログに残しておきましょう。

Q: サイレントフェイルはどうやって検出しますか?

サイレントフェイルはアプリケーション層の検証が必要です — APIは意味的な誤りを教えてくれません。期待する形式に合致しているか(有効なJSON、期待ラベル、最小長など)を確認します。検証失敗時は出力全体をログに残してください。最も一般的な原因は、曖昧なプロンプト、無視されたフォーマット指示、またはタスクに対して入力が短すぎる/長すぎることです。

AI開発コストを20%削減する準備はできていますか?

数分で無料スタート。無料トライアルクレジット付き。クレジットカード不要。

もっと読む