エラーボディの形式と読み取り順序
すべての失敗レスポンスは同じ構造を持っています:
{"error":{"code":"...","message":"..."}}
読み取り順序は以下の 3 段階です:
- HTTP ステータスコード:大カテゴリを決定します。
error.code:具体的な原因を決定し、プログラムで分岐に使用します。error.message:人間用に表示し、ログに記録します。文字列マッチングには使用しないでください。
まず明確にすべき境界があります:再試行で解決できるもの(429、503、ネットワークタイムアウト)と、コード修正が必要なもの(その他)です。これらを混同することは、402に対して無効なリクエストを毎秒数十回送信するなど、本番環境での事故の最も一般的な原因です。
ログには必ず以下の4フィールドを記録すること:時刻、ステータスコード、error.code、今回のリクエストにおけるprompt_tokensの推定値。問題発生時に、特定のエラーが急増したのか、それとも全体が失敗しているのかを一目で判断できる。また、アラートルールを設定すること:402が1回でも発生したら即座に通知(サービスがユーザーに利用不可であることを意味するため)。429は割合を見る:偶発的なら無視、継続的なら同時実行設計に問題がある。
ステータスコード早見表
| ステータスコード | error.code | 意味 | 再試行? |
|---|---|---|---|
| 400 | — | リクエストが不正:promptとmax_tokensの合計が64kを超えている | 否 |
| 401 | — | キーが無効または欠落 | 否 |
| 402 | no_credit | 残高枯渇または試用期間終了 | 否 |
| 403 | content_blocked | コンテンツがブロックされた | 否 |
| 404 | — | エンドポイントが存在しない | 否 |
| 429 | — | レート制限に到達 | はい、バックオフ |
| 503 | upstream_busy | サービスが一時的にビジー | はい、数秒後 |
表中の「—」は、特別な処理が必要な固定の code 文字列がないことを意味し、ステータスコードで分岐してください。
4xx:リクエストを修正し、再試行しない
400 リクエストが不正
- 原因 1:JSON 形式エラー。手動で文字列を構築する際に引用符のエスケープを忘れることが一般的です。
- 原因2:promptとmax_tokensの合計が100,000を超えている。長い会話で最も発生しやすい。
- 原因 3:リクエストボディが 8 MB を超える。
- 修正:JSON ライブラリでシリアライズする;送信前にトークンを推定し、超過時は履歴を切り捨てるか max_tokens を小さくする;長文脈の実践を参照。
401 キーの問題
- Header が欠落している、または
Bearerプレフィックスが抜けている。 - キーを再生成した場合、旧キーは直ちに無効になるが、あるマシンが旧値を使い続けている。
- 環境変数がコンテナまたは定期タスクに引き継がれていない。
402 no_credit
残高が枯渇、または7日間の試用期間が終了。アカウントページで前払いクレジットをチャージすれば利用を再開できます。サービス側で残高を監視し、ユーザーからエラー報告が来てから気づかないようにしてください。
403 content_blocked
コンテンツがブロックされました。合法的なアダルトコンテンツ、フィクション、論争のあるトピックは拒否されませんが、未成年者関連の性的コンテンツは小説やロールプレイも含め一律でブロックされます。403エラーが発生した場合は、入力履歴に該当コンテンツがないか確認し、単に表現を変えて再試行するのではなく対処してください。
404 エンドポイントが存在しません
利用可能なエンドポイントは2つだけです:POST /v1/chat/completions と GET /v1/models です。パスに /v1 が抜けている、余分なスラッシュが含まれている、スペルミスがある、またはサポートされていない embeddings や画像などのエンドポイントをリクエストすると404になります。
400エラーで誤解されやすいケースがあります:会話のトークン数は会話の進行に伴って線形に増加します。日中のテストでは正常でも、数十回の会話以降に突然400エラーが発生することがあります。これはAPIの不安定さではなく、コンテキストウィンドウの上限に達したためです。解決策は、履歴に上限を設定し、閾値を超えた場合は最も古い会話を破棄するか、古いコンテンツを要約に圧縮することです。エラー発生後に処理するのではなく、リクエスト送信前に見積もっておいてください。
401エラーのトラブルシューティングにはコツがあります:API キーの前後4文字をログに出力し、完全な値を出力しないようにします。そしてアカウントページに表示される値と比較します。これにより、プロセス内で読み込まれている値が本当に想定通りのものかどうかを確認できます。特にコンテナや定期タスクの環境では、環境変数の欠落や古い値の読み込みが最も一般的な原因です。
429 と 503:再試行すべき2つのケース
429 レート制限
APIキーごとに毎分300リクエスト。バッチタスク、複数インスタンスで1つのキーを共有、再試行の嵐などがトリガーになる。修正は2段階:まずクライアント側でレート制限を行い(後述コード参照)、その後429に対してバックオフ再試行を行う。
503 upstream_busy
サービスが一時的に混雑しています。数秒待ってから再試行してください。直ちに連続して送信したり、1秒以内に10回再試行したりすると、状況が悪化するだけです。
ネットワークタイムアウト
長文の生成には時間がかかるため、timeout は短すぎない設定にしてください。例では120秒を使用します。タイムアウト後の再試行には注意が必要です:前のリクエストがサーバー側で既に実行されている可能性があり、重複呼び出しにより課金が重複する恐れがあるためです。生成系のリクエストでは再試行回数を抑え、ストリーミング出力を併用して単回の待機時間を短くすることをお勧めします。
import os
import random
import time
import requests
URL = "https://api.wushenchaapi.com/v1/chat/completions"
HEADERS = {
"Authorization": "Bearer " + os.environ["API_KEY"],
"Content-Type": "application/json",
}
RETRYABLE = {429, 503} # 只重试这两类
FATAL = {400, 401, 402, 403, 404}
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def call(payload, max_retries=5, base=1.0, cap=30.0):
for attempt in range(max_retries + 1):
try:
r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
except (requests.ConnectionError, requests.Timeout):
r = None # 网络层失败,按可重试处理
if r is not None and r.ok:
return r.json()
if r is not None:
try:
err = r.json().get("error", {})
except ValueError:
err = {}
if r.status_code in FATAL or r.status_code not in RETRYABLE:
raise ApiError(r.status_code, err.get("code"), err.get("message"))
if attempt == max_retries:
raise ApiError(r.status_code if r is not None else 0, "retry_exhausted", "重试次数用尽")
delay = min(cap, base * (2 ** attempt)) * random.uniform(0.5, 1.0)
time.sleep(delay)
if __name__ == "__main__":
out = call({"model": "uncensored", "max_tokens": 100,
"messages": [{"role": "user", "content": "回复一个字:好"}]})
print(out["choices"][0]["message"]["content"])要点:バックオフの公式は min(cap, base × 2^n) × 乱数係数 です。ランダムなジッター(揺らぎ)により、複数のクライアントが同時に再試行して衝突するのを防ぎます。FATAL 集合に含まれるステータスコードは再試行しません。
クライアント側のレート制限と診断リクエスト
429が発生してからバックオフするのではなく、事前にレート制限を設けるべきです。以下のツールは、1分あたりのリクエスト数を設定値以下に保つことを保証し、スレッドセーフです:
import threading
import time
class RateGate:
"""简单的客户端限速:保证一分钟内请求数不超过 limit。"""
def __init__(self, limit=240): # 留出余量,低于 300/分钟
self.limit, self.stamps, self.lock = limit, [], threading.Lock()
def wait(self):
while True:
with self.lock:
now = time.time()
self.stamps = [t for t in self.stamps if now - t < 60]
if len(self.stamps) < self.limit:
self.stamps.append(now)
return
sleep_for = 60 - (now - self.stamps[0])
time.sleep(max(sleep_for, 0.05))トラブルシューティングにおいて最もクリーンな方法は、ビジネスロジックから離れ、curl で最小限のリクエストを送信することです。-i オプションを指定すると、ステータス行とレスポンスヘッダーの両方を確認できます:
curl -i https://api.wushenchaapi.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"uncensored","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'curl は通るがビジネスロジックが通らない場合、問題はコードまたは環境(プロキシ、環境変数、エンコーディング)にあります。curl も通らない場合は、API キー、残高、ネットワークを確認します。
なぜ制限値を300ではなく240に設定するか:マルチインスタンス展開時、各インスタンスが個別にレート制限を行うため、合計が総額制限を超える可能性がある。さらに再試行で追加のクォータを消費するため、20%の余裕を持たせるのが安全策である。複数のインスタンスがある場合は、各インスタンスの制限を総額で均等に分けるか、共有カウンターで一元管理せよ。
トラブルシューティングチェックリスト
- ステータスコードだけでなく、body 内の
error.codeを確認してください。 - 401:API キーのプレフィックス、環境変数、再生成の有無。
- 400:JSONが有効か、promptとmax_tokensの合計が100,000を超えていないか、リクエストボディが8 MBを超えていないかを確認。
- 402:残高と試用期間の有効期限。
- 403:入力と履歴に禁止コンテンツが含まれていないか。
- 404:パスが /v1/chat/completions または /v1/models であるか。
- 429:複数のインスタンスで1つのAPI キーを共有していないか、クライアント側のレート制限を実施しているか。
- 503:バックオフ再試行を実施しているか、間隔が少なくとも数秒あるか。
- タイムアウト:timeout が十分か、ストリーミングに変更できるか。
- 上記すべてを除外:curlで最小限のリクエストを再現。
接続直後なら、まず接続チュートリアルを参照。その他のパラメータはドキュメントに記載。
チェックリストの使い方:問題発生時は上から順に除外し、飛ばして見ないこと。「奇妙な」障害の多くは最初の4項目に原因がある。
経験則として1つ追加します:トラブルシューティングの結論をチームのドキュメントに書き戻してください。次回同じエラーが発生した際に、すぐに原因を特定できます。