stop_reasonとエラーの違い — Claude APIの成功と失敗を混同しない設計
stop_reasonは常にHTTP 200の一部で、4xx/5xxのエラーとは別レイヤーです。両者を同じ監視ロジックで扱うと、refusalのような重要な停止理由を見落とします。
Claude APIのレスポンスを監視する仕組みを作るとき、エラーレートや5xxだけで異常系を監視していると、stop_reason の異常を拾えません。stop_reason はHTTP 200の成功レスポンスの一部であり、4xxや5xxのエラーとはそもそも別のレイヤーの情報だからです。
stop_reasonとエラーは別レイヤーの情報
stop_reason はMessages APIのすべての成功レスポンスに含まれるフィールドで、Claudeが生成をなぜ止めたかを示します。エラーは、リクエストの処理自体が失敗したことを示すHTTPステータスコード(4xx・5xx)です。
どちらの層の情報か
stop_reason
HTTP 200で返るレスポンス本体のフィールドです。生成が止まった理由を示し、refusal もここに入ります。判断するのはアプリケーションの仕事です。
エラー
4xx・5xxのステータスコードと、error オブジェクトを持つJSONで返ります。content は存在しません。
content が空になる成功レスポンスもあります。出力が始まる前に拒否されたrefusalは、content が [] のままHTTP 200で届きます。content の有無だけでエラーかどうかを判定すると、この応答を取りこぼします。
7つのstop_reason値と対応表
Messages APIが返す stop_reason は次の7種類です。
| stop_reason | 発生する場面 | 取るべき対応 |
|---|---|---|
end_turn | 発生する場面Claudeが自然に応答を終えた | 取るべき対応そのまま使う |
max_tokens | 発生する場面max_tokens の上限に達した | 取るべき対応上限を上げるか続きを取得する |
stop_sequence | 発生する場面指定した stop_sequences を出力した | 取るべき対応stop_sequence フィールドでどれが発火したか確認 |
tool_use | 発生する場面ツールを呼び出している | 取るべき対応ツールを実行し結果を返す |
pause_turn | 発生する場面サーバーツールのループが反復上限に達した | 取るべき対応アシスタントの内容をそのまま送り返して継続 |
refusal | 発生する場面Claudeが応答を拒否した | 取るべき対応stop_details を確認しフォールバックモデルへリトライ |
model_context_window_exceeded | 発生する場面コンテキストウィンドウの上限に達した | 取るべき対応レスポンスを打ち切りとして扱う |
stop_sequence と max_tokens はどちらも「打ち切り」に見えますが、意味が違います。stop_sequence は指定した文字列が出力された時点での意図的な停止で、応答は完結した内容として扱えます。max_tokens はトークン数の上限による強制的な打ち切りで、応答が文の途中で切れている可能性があります。この2つを同じ「打ち切り」として一括処理すると、stop_sequence で正常に終わった応答まで「続きを取得する」処理に回してしまう実装ミスにつながります。
tool_use と pause_turn は混同しやすい組み合わせです。クライアント側の tool_use ブロックが応答の中で待っているときの stop_reason は tool_use で、pause_turn にはなりません。pause_turn が返るのは、Web検索のようなサーバーツールの反復ループがデフォルト10回の上限に達したときです。継続のために送り返すのは、tool_use なら tool_result、pause_turn ならレスポンスそのものです。
model_context_window_exceeded は、入力サイズが事前にわからないまま最大限のトークンを引き出したいときに使う値です。Sonnet 4.5以降は追加ヘッダーなしで有効になり、それより前のモデルでは model-context-window-exceeded-2025-08-26 ベータヘッダーが必要です。SDKの型定義では、この値は beta 名前空間側にだけ置かれています。この値を使った実装パターンは入力サイズ不明でも最大出力を引き出す実装パターンで扱っています。
エラーは何が違うのか — HTTPステータスコード一覧
主なステータスコードと error.type の対応は次の通りです。
| コード | type | 意味 |
|---|---|---|
| 400 | typeinvalid_request_error | 意味リクエストの形式・内容の問題、または支出上限の超過 |
| 401 | typeauthentication_error | 意味APIキーが不正・失効・期限切れ |
| 402 | typebilling_error | 意味支払い情報の問題 |
| 403 | typepermission_error | 意味APIキーに権限がない |
| 404 | typenot_found_error | 意味リソースが見つからない |
| 409 | typeconflict_error | 意味リソースの状態と競合している |
| 413 | typerequest_too_large | 意味リクエストサイズの上限を超過 |
| 429 | typerate_limit_error | 意味レート制限または支出上限に到達 |
| 500 | typeapi_error | 意味Anthropic側の予期しない内部エラー |
| 504 | typetimeout_error | 意味処理中にタイムアウト |
| 529 | typeoverloaded_error | 意味APIが一時的に過負荷 |
error.type はこの一覧に限定されません。バージョニングポリシー上、type の値は将来増える可能性があるため、文字列の完全一致だけに頼った分岐は壊れやすくなります。公式SDKはこれらをタイプ付き例外に変換して返すため(Pythonなら anthropic.NotFoundError、Rubyなら Anthropic::Errors::NotFoundError など)、文字列比較よりSDKの例外クラスで捕捉するほうが安全です。エラーコードとSDK例外クラスの言語別対応はClaude APIのエラー形式とSDK例外クラスの言語別対応表にまとめています。
refusalは200で届く — 形・タイミング・課金
refusal は、安全性の分類器がリクエストを拒否したときに返る値です。通常のHTTP 200の成功レスポンスとして届く点で、他のエラーとは扱いが違います。
出力前に拒否されたときのレスポンスは、次の形です。
{
"content": [],
"stop_reason": "refusal",
"stop_details": {
"type": "refusal",
"category": "cyber",
"explanation": "This request was declined because it could enable cyber harm."
},
"usage": { "input_tokens": 412, "output_tokens": 0 }
}stop_details.category は cyber / bio / frontier_llm / reasoning_extraction / general_harms のいずれかで、どの方針領域が発火したかを示します。explanation の文面は安定しないので、画面に出すだけにして分岐には使いません。分類できない拒否では category と explanation がどちらも null になるため、判定には stop_reason === "refusal"(または stop_details.type)を直接使います。stop_details 自体も、refusal以外の stop_reason では null です。
拒否が届くタイミングで、扱いが変わります。
出力前の拒否と途中の拒否
出力前
output_tokens は0です。課金されるかどうかはカテゴリー次第で、bio・frontier_llm・reasoning_extraction は課金され、cyber・general_harms・null は課金されません。どちらの場合もレート制限には数えられます。
出力途中
部分的な出力が届いたあとで拒否されることがあります。部分出力は未完了として捨てます。入力トークンとそれまでに流れた出力トークンは通常料金で課金されます。
課金対象のカテゴリーは今後変わりうる、とされています。「HTTP 200だから課金は成功時と同じ」「拒否だから無料」のどちらも成り立たないので、コストの集計でもrefusalを独立した項目にしておくと食い違いに気づけます。
ストリーミング中のrefusal検出はClaude APIのrefusal stop_reasonを検出してリセットする方法に書きました。リセット手順もそちらにあります。
同じモデルに同じリクエストを送り直しても、再び拒否される可能性が高いとされています。リトライ先はフォールバックモデルにします。
最も簡単な構成は、Claude API上のベータ機能であるサーバー側フォールバックです。fallbacks を "default" に設定すると、拒否されたリクエストをAPIが、その拒否カテゴリー向けにAnthropicが推奨するフォールバックモデルで自動的に再試行します。発動するのは安全性の分類器による拒否だけです。リクエスト先モデルでのレート制限・過負荷・サーバーエラーはそのまま呼び出し側に返るので、ここでもエラーとrefusalは別の経路になります。
ストリーミングで判定が通る3つの関門
ストリーミング(SSE)の応答は、次の順に判定が通ります。
ストリーミング応答を判定する順番
- 1
リクエスト自体の4xx・5xx
接続の時点で失敗していれば、通常のHTTPエラーです。SDKなら例外として上がってきます。
- 2
ストリーム内のエラーイベント
200で始まったあとに
event: errorが届きます。過負荷ならoverloaded_errorで、非ストリーミングなら529に対応します。 - 3
message_deltaのstop_reason
message_startではstop_reasonがnullで、message_deltaイベントで初めて値が入ります。ここでrefusalやmax_tokensを判定します。
ストリーミングの途中切断からの復旧は、Claude 4.5以前とClaude 4.6以降で手順が違います。違いはClaude APIストリーミングのエラー復旧がClaude 4.6以降で変わった点にあります。
Message Batchesでも境界は同じ位置にある
バッチ内の各リクエストは result.type が succeeded / errored / canceled / expired のいずれかに振り分けられますが、refusalは errored ではなく succeeded です。中身の stop_reason が "refusal" になっているだけで、バッチとしては正常に処理が完了した扱いになります。
エラー件数だけを集計する監視ロジックをバッチに流用すると、errored の件数はゼロなのにrefusalが大量発生している、という状態を見逃します。バッチの結果を集計するときは、result.type の分岐とは別に、succeeded の中身の stop_reason を数える処理が要ります。
Message Batchでは fallbacks パラメータが使えず、含めたリクエストは個別に errored 結果として返ります。拒否されたバッチ項目の再送は、次の手順で自前で組みます。
- 結果から拒否された項目を集める
- Claude Fable 5.1 / Fable 5のthinkingブロックが多ターンの履歴に入っていれば取り除く
- フォールバックモデルに、新しいバッチまたは通常のリクエストとして再送する
バッチAPIのエラー結果全般(errored の中の invalid_request_error 判定や24時間・29日の期限)はClaude APIのエラーハンドリング設計で扱っています。
実装パターン — stop_reasonとエラーを1つの分岐で扱わない
curl --fail-with-body -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello!"}]
}' | jq '.stop_reason'--fail-with-body を付けると、HTTPエラーのときにcurlが非ゼロで終了します。終了コードでエラーを、jq の出力で stop_reason を、それぞれ別に見られます。
Python SDKでは、except でHTTPエラーを処理し、try の後ろで stop_reason を見る形が基本です。
try:
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
except anthropic.APIStatusError as e:
if e.status_code == 429:
print("レート制限に到達")
elif e.status_code == 500:
print("サーバーエラー")
raise
if response.stop_reason == "refusal":
print("拒否されました", response.stop_details)
elif response.stop_reason == "max_tokens":
print("応答が上限で打ち切られました")TypeScriptでも構造は同じで、catch の中に Anthropic.APIError の分岐を、try の後ろに stop_reason の分岐を置きます。
let response;
try {
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
});
} catch (err) {
if (err instanceof Anthropic.APIError) {
if (err.status === 429) console.log("レート制限に到達");
else if (err.status === 500) console.log("サーバーエラー");
}
throw err;
}
if (response.stop_reason === "refusal") {
console.log("拒否されました", response.stop_details);
} else if (response.stop_reason === "max_tokens") {
console.log("応答が上限で打ち切られました");
}崩れやすいのは、stop_reason の分岐を except(または catch)の中に書くケースです。stop_reason は成功レスポンスの一部なので、except / catch には入ってきません。逆に、エラーを stop_reason の分岐として拾おうとするコードも動作しません。2つの分岐は、独立した処理として書きます。
再試行はどちらの層の仕事か
エラー側には、SDKの自動リトライがあります。接続エラー・レート制限・5xxのような一時的な失敗は、既定で2回まで指数バックオフ付きで再試行され、retry-after ヘッダーがあればそれに従います。実装が自分で書く分岐に届く前に、SDK内部で再試行が済んでいる場合があるということです。
ただし429がすべて待てば直るわけではありません。利用ティアの月間支出上限に達した429には retry-after ヘッダーが付かず、アクセスが再開するまで失敗し続けます。400にも、組織やワークスペースに設定した支出上限の超過が含まれます。この2つはSDKの再試行で解決しないので、アラートの対象にします。
stop_reason 側は、SDKのリトライの対象外です。max_tokens はリクエストが成功した結果であり、SDKの再試行が扱うのは通信・サーバー側の失敗だからです。応答の中身がどう打ち切られたかは、呼び出し側のアプリケーションロジックの責任範囲になります。
エラーは「インフラ・入力・課金の問題」として、リトライやアラートの対象になります。refusalを含む stop_reason は「モデルの応答判断」として、フォールバックやプロンプトの調整の対象です。1本のエラーレートに混ぜると、インフラは正常なのにrefusalだけが急増している変化が埋もれます。
まとめ
refusalは1件ごとにイベントを出し、フォールバックで救われた応答(usage.iterations の fallback_message)の件数との差を警報にする、という運用があります。エラーレートとは別のダッシュボードに置くと、両者が混ざりません。