stop_reasonとエラーの違い — Claude APIの成功と失敗を混同しない設計
stop_reasonは常にHTTP 200の一部で、4xx/5xxのエラーとは別レイヤーです。両者を同じ監視ロジックで扱うと、refusalのような重要な停止理由を見落とします。
Claude APIのレスポンスを監視する仕組みを作るとき、多くの実装が「異常系はステータスコードで判定する」という前提から出発します。この前提のまま stop_reason を扱うと、監視のすき間が生まれます。stop_reason はHTTP 200の成功レスポンスの一部であり、4xxや5xxのエラーとはそもそも別のレイヤーの情報だからです。
stop_reasonとエラーは別レイヤーの情報
stop_reason はMessages APIのすべての成功レスポンスに含まれるフィールドで、Claudeが生成をなぜ止めたかを示します。一方でエラーは、リクエストの処理自体が失敗したことを示すHTTPステータスコード(4xx・5xx)です。両者は発生する層が違います。
- stop_reason: レスポンス本体の一部。生成は正常に完了しており、
contentに有効な中身が入っている - エラー: リクエスト処理の失敗。
errorオブジェクトが返り、contentは存在しない
この違いを踏まえると、「レスポンスをそのまま使ってよいか、続きを送るべきか、リトライすべきか、フォールバックすべきか」を判断する材料は stop_reason にあり、「リクエスト自体をやり直すべきか」の判断材料はステータスコードにある、と役割が分かれます。
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 が返るのは、Web検索のようなサーバーツールの反復ループがデフォルト10回の上限に達したときだけで、継続のために送り返すものが「ツール結果」か「レスポンスそのもの」かが変わります。
model_context_window_exceeded は、入力サイズが事前にわからないまま最大限のトークンを引き出したいときに使うベータ機能です。Sonnet 4.5以降は追加ヘッダーなしで有効になり、それより前のモデルでは model-context-window-exceeded-2025-08-26 ベータヘッダーが必要です。この値を使った実装パターンは入力サイズ不明でも最大出力を引き出す実装パターンで扱っています。
エラーは何が違うのか — HTTPステータスコード一覧
エラーは常にJSON形式の error オブジェクトとして返り、content は存在しません。主なステータスコードは次の通りです。
| コード | 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なのに拒否
stop_reason の7種類の中で最も誤解されやすいのが refusal です。安全性の分類器がリクエストを拒否したときに返る値ですが、これは通常のHTTP 200の成功レスポンスとして届きます。エラーレートやステータスコードを軸にした監視は、refusalを一切検知しません。
refusalはエラーではありません。コンテンツの安全性判定という、リクエスト処理の成否とは別のレイヤーの結果です。stop_details.category にどの方針領域が発火したかが入りますが、category と explanation は分類できないケースで null になることがあるため、判定条件には stop_reason === "refusal"(または stop_details.type)を直接使います。ストリーミング中のrefusal検出とコンテキストのリセット手順はClaude APIのrefusal stop_reasonを検出してリセットする方法に詳しく書きました。
ストリーミングでは監視の分岐点がもう一段複雑になる
ストリーミング(SSE)を使うと、200応答が返ったあとにエラーが発生することがあります。この場合のエラーは標準のHTTPステータスコードでは表現されず、ストリーム内のイベントとして届きます。つまりストリーミング実装では、「リクエスト自体の4xx/5xx」「ストリーム内のエラーイベント」「成功レスポンス内のstop_reason」という3層を分けて扱う必要があります。v4.6でこのストリーミングエラー復旧の挙動が変わった経緯はClaude APIストリーミングのエラー復旧がv4.6で変わったを参照してください。
Message Batchesでも境界は同じ位置にある
この区別はストリーミングだけでなく、Message Batchesの結果判定にも同じ形で現れます。バッチ内の各リクエストは result.type が succeeded / errored / canceled / expired のいずれかに振り分けられますが、refusalは errored ではなく succeeded です。中身の stop_reason が "refusal" になっているだけで、バッチとしては正常に処理が完了した扱いになります。
エラー件数だけを集計する監視ロジックをバッチに流用すると、errored の件数はゼロなのにrefusalが大量発生している、という状態を見逃します。バッチの結果を集計するときは、result.type の分岐とは別に、succeeded の中身の stop_reason を数える処理が要ります。もう1点、Message Batchではfallbacks パラメータ自体が使えず、含めたリクエストは個別に errored 結果として返ります。バッチのrefusalをリトライするなら、フォールバックモデルへの再送を自分で組む必要があります。バッチ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",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello!"}]
}' | jq '.stop_reason'Python SDKでは、try/except の外で stop_reason を見て、except の中でHTTPエラーを見る、という2段構えの分岐が基本形になります。
try:
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
if response.stop_reason == "max_tokens":
print("応答が上限で打ち切られました")
except anthropic.APIStatusError as e:
if e.status_code == 429:
print("レート制限に到達")
elif e.status_code == 500:
print("サーバーエラー")TypeScriptでも構造は同じで、try/catch の外に stop_reason の分岐、catch の中に Anthropic.APIError の分岐を置きます。
try {
const response = await client.messages.create({
model: "claude-opus-5",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }]
});
if (response.stop_reason === "max_tokens") {
console.log("応答が上限で打ち切られました");
}
} catch (err) {
if (err instanceof Anthropic.APIError) {
if (err.status === 429) console.log("レート制限に到達");
else if (err.status === 500) console.log("サーバーエラー");
} else {
throw err;
}
}この形が崩れやすいのは、stop_reason の分岐をエラーハンドリングの except(または catch)ブロックの中に書いてしまうケースです。stop_reason は成功レスポンスの一部なので、except / catch には絶対に入ってきません。逆に、エラーを stop_reason の分岐として拾おうとするコードも動作しません。2つの分岐は独立した2つの if / try として書く必要があります。
Anthropicはなぜこの2レイヤーを分離したのか
この設計は、監視の責務を分けるための意図的な選択と見ることもできます。エラーは「インフラ・入力・課金の問題」を表し、リトライやアラートの対象です。一方でrefusalを含む stop_reason は「モデルの応答判断」を表し、コンテンツ面の対応(フォールバックモデルへの切り替えや、プロンプトの調整)を必要とします。この2つを1本のエラーレートに混ぜてしまうと、インフラは正常なのにrefusalだけが急増している、といった変化が埋もれます。refusal率を専用の指標として監視に加えるのは、この設計を踏まえた対応です。
ただし、この分離を厳密に維持できるかは実装側の規律に依存します。except ブロックの中で stop_reason を読むコードは、テスト環境の正常系リクエストでは決して顕在化しません。本番でrefusalやmax_tokensが実際に発生して初めて、分岐が動いていないことに気づく形になります。
もう1つの見落としが、429と529です。この2つはインフラ側の一時的な過負荷に近く、公式SDKが既定で2回まで自動リトライします。つまり実装が自分で書く分岐に届く前に、SDK内部で一度リトライが完了しています。ここに stop_reason の分岐を混ぜて考えると、「なぜmax_tokensだけ自分でリトライを書く必要があるのか」が見えにくくなります。答えは単純で、max_tokens はリクエストが成功した結果であり、SDKのリトライ機構が対象にしているのは通信・サーバー側の失敗だけだからです。応答の中身がどう打ち切られたかは、呼び出し側のアプリケーションロジックの責任範囲になります。
まとめ
stop_reason は成功レスポンスの一部、エラーはリクエスト処理の失敗という、発生するレイヤーが異なる2つの情報です。実装では両者を1つの分岐にまとめず、try/except(またはステータスコード判定)の外側で stop_reason を、内側でHTTPエラーを扱います。とくに refusal はHTTP 200で返るため、エラーレートを軸にした監視では検知できません。
この境界は同期のMessages APIに限らず、ストリーミングのイベント分離やMessage Batchesの result.type 判定にも同じ形で貫かれています。プロトコル全体を通して「リトライで直る層(エラー)」と「アプリケーション側の判断が要る層(stop_reason)」が一貫して分けられている、という前提で監視とハンドリングを設計すると、実装のたびに同じ分岐ロジックを再発明せずに済みます。refusal率とmax_tokens率をエラーレートとは別の指標として持てば、インフラが正常なままrefusalだけが増えた変化を検知できます。