Claude APIのrefusal stop_reasonを検出してリセットする方法
stop_reason: refusalはHTTP 200で返るためエラー監視では検知できません。検出方法とコンテキストのリセット、fallback creditでの二重課金回避までを扱います。
Claude APIのストリーミングを既に運用しているなら、エラー監視の設計を見直す前提としてまずこの1点を押さえておきます。refusalはネットワーク層の異常でも400系の入力エラーでもなく、コンテンツの安全性判定という別レイヤーの結果です。
refusalはエラーではなく成功レスポンスとして返る
Claude Fable 5.1 / Fable 5 / Opus 5は、ストリーミング中に安全性の分類器がコンテンツを止めたとき、stop_reason: "refusal" を返します。ここで見落としやすいのが、このレスポンスがHTTP 200の成功として届く点です。ステータスコードやSDKの例外を見て異常を検知する通常のエラー監視は、refusalを一切拾いません。件数を数えるなら stop_reason そのものを専用の指標として監視する必要があります。
Message Batchesの中で発生したrefusalも同様です。バッチ内の各リクエストは成功・失敗を判定されますが、refusalは失敗(errored)ではなく成功(succeeded)結果として扱われ、中身の stop_reason が "refusal" になっているだけです。バッチの結果を集計するときにエラー件数だけを見ていると、refusalは静かに素通りします。
レスポンスの形と判定に使うフィールド
refusalが発生すると、ストリームの message_delta イベントに stop_reason と stop_details が乗って届きます。stop_details には category(方針違反の分類)と explanation(利用者に見せられる説明文)が入りますが、この2つは分類できないケースで null になることがあります。判定条件には category の有無ではなく stop_reason(または stop_details.type)を使い、category / explanation が空のときのメッセージは自前で用意します。
{
"stop_reason": "refusal",
"stop_details": {
"type": "refusal",
"category": "cyber",
"explanation": "This request was declined because it could enable cyber harm."
}
}検出したらコンテキストをリセットする
stop_reason: "refusal" を受け取ったら、そのままリトライしても同じ結果になります。会話履歴の中でrefusalを引き起こしたターンを取り除くか言い換えるか、あるいは会話全体を初期化してから続ける必要があります。リセットせずに同じ文脈で送り直すのは避けます。
messages = []
def reset_conversation():
global messages
messages = []
with client.messages.stream(
max_tokens=1024,
messages=messages + [{"role": "user", "content": user_input}],
model="claude-sonnet-5",
) as stream:
for event in stream:
if event.type == "message_delta" and event.delta.stop_reason == "refusal":
reset_conversation()
break課金の扱いにも注意が必要です。Claudeが出力を何も生成しないままrefusalになった場合は課金されず、レスポンスに含まれる使用量は参考値になります。逆に一部でもテキストを生成したあとでrefusalになった場合は、その分の出力に課金されます。
TypeScriptで同じ処理を書く場合も考え方は同じで、message_delta イベントの stop_reason を見て会話配列を初期化します。
let messages: Anthropic.MessageParam[] = [];
function resetConversation() {
messages = [];
}
const stream = await client.messages.stream({
messages: [...messages, { role: "user", content: userInput }],
model: "claude-sonnet-5",
max_tokens: 1024
});
for await (const event of stream) {
if (event.type === "message_delta" && event.delta.stop_reason === "refusal") {
resetConversation();
break;
}
}fallback creditで二重課金を防ぐ
コンテキストのリセット以外に、拒否されたリクエストを別モデルへリトライする方法もあります。ただしプロンプトキャッシュはモデルごとに独立しているため、単純にモデルを変えて再送すると、最初のモデル用にキャッシュ済みだった会話の前半部分を新しいモデルのキャッシュへ書き込み直すことになります。キャッシュの書き込みは読み取りより高くつくため、リトライのたびにこのコストを払うことになります。
これを避けるのがfallback creditです。refusalの stop_details には fallback_credit_token というトークンが載っており、リトライ時にこのトークンを付けて送ると、会話が最初からリトライ先のモデルで行われていたかのような料金で処理されます。仕組みは次の3手順です。
- 最初のリクエストに
anthropic-beta: fallback-credit-2026-07-01ヘッダーを付ける - refusalの
stop_details.fallback_credit_tokenを読み取る - リトライのリクエストボディに
fallback_credit_tokenを追加し、モデルをフォールバック先に変えて同じベータヘッダーで送る
refusalの stop_details にはもう1つ、fallback_has_prefill_claim という真偽値も含まれます。これは、リトライのリクエストボディをどう組み立てるかを左右します。true の場合は、拒否されたリクエストボディに、拒否されたレスポンスの content をそのまま反映したassistantメッセージを1つ追加して送ります。この場合、リトライ先のモデルは拒否されたモデルが途中まで生成した内容の続きから応答し、すでに完了していたサーバー側ツール呼び出しは再実行されません。false の場合は、拒否されたリクエストボディをそのまま(変更なしで)送るだけです。
自前でリトライを組む場合はこの手順が必要ですが、公式のサーバーサイドフォールバックやSDKミドルウェアを使えばfallback creditの適用は自動で行われます。トークンが null の場合はそのリクエストにクレジットが無いということなので、通常のコンテキストリセットに切り替えます。
方針違反の分類(category)の内訳
stop_details.category には、どの方針領域が引っかかったかが入ります。Claude Fable 5.1・Claude Fable 5・Claude Opus 5は安全性の分類器を内蔵しており、リクエストの内容によってこの5分類のいずれかで拒否します。
| category | 内容 |
|---|---|
cyber | 内容サイバー攻撃に転用されうる要求(マルウェアや攻撃コードの開発等)。無害なセキュリティ業務でも該当することがある |
bio | 内容生物学的な危害につながりうる要求(危険な実験手法等)。無害なライフサイエンス業務でも該当することがある |
frontier_llm | 内容競合するAIモデルの開発を助ける要求。Anthropicの商用利用規約で制限されている領域 |
reasoning_extraction | 内容リクエストがモデル自身の内部推論をレスポンス本文にそのまま再現するよう求めるもの。構造化された形で推論を得たい場合はadaptive thinkingを使う |
general_harms | 内容上記4分類に当てはまらない、利用ポリシー上の他の懸念領域 |
category が上記いずれにも当てはまらないrefusalでは category / explanation とも null になります。この null は値が欠けているのではなく、正規の恒久的な値として扱います。
サーバーサイドフォールバックとSDKミドルウェアで自動化する
コンテキストのリセットを自前で実装する代わりに、拒否されたリクエストを自動的に別モデルへリトライする仕組みも用意されています。
- サーバーサイドフォールバック(Claude APIのベータ機能): リクエストに
fallbacks: "default"を指定しserver-side-fallback-2026-07-01ベータヘッダーを送ると、拒否されたカテゴリに応じてAnthropicが推奨するフォールバックモデルへ、同じAPI呼び出しの中でリトライされます。呼び出し側は1回のリクエストと1回のレスポンスだけで済みます。ただしMessage Batches APIには非対応で、fallbacksを含むバッチ項目はエラー結果として返ります。Amazon Bedrock・Google Cloud・Microsoft Foundryでも使えません - SDKミドルウェア: クライアント初期化時にフォールバック先モデルのリストを設定しておくと、
client.beta.messages経由の呼び出しで拒否が起きるたびに自動でリトライされます。どのプラットフォームでも使え、fallback-credit-2026-07-01ベータヘッダーも自動で付与されるため、fallback creditを個別に実装する必要がありません
どちらの方法もfallback creditの適用を自動で行うため、前述の手動実装が必要になるのは「自前でリトライロジックを組む」場合に限られます。まず自動化の2択を検討し、要件に合わなければ手動実装に進むのが実装コストの面で合理的です。
利用者への見せ方とパターンの追跡
explanation フィールドはそのまま利用者に見せられる文面として設計されていますが、null になるケースがある以上、それだけに頼った画面設計はできません。category の値ごとに自前の日本語メッセージを用意しておき、explanation があればそれを補足として添える、という二段構えにしておくと欠落時にも破綻しません。
もう一つ見落としやすいのが、refusalの発生頻度そのものを継続的に追う運用です。特定のプロンプトテンプレートや特定のユーザー入力パターンでrefusalが増えているなら、プロンプト設計そのものに改善の余地がある合図です。エラー率とは別に「refusal率」を専用の指標としてダッシュボードに乗せておくと、プロンプトの問題を早期に見つけられます。
refusalは3種類ある
「refusal」とひとくくりにされがちですが、実際には返り方が異なる3系統があります。
| refusalの種類 | 返り方 | 発生タイミング |
|---|---|---|
| ストリーミング分類器によるrefusal | 返り方stop_reason: "refusal" | 発生タイミングストリーミング中にコンテンツが方針に抵触したとき |
| APIの入力・著作権バリデーション | 返り方400エラー | 発生タイミングリクエストの入力検証で弾かれたとき |
| モデル自身による拒否 | 返り方通常のテキスト応答 | 発生タイミングモデルが自ら回答を拒んだとき |
監視の設計では、この3つを同じ「エラー」として扱わないことが重要です。400エラーは通常のエラー監視で拾えますが、ストリーミング分類器のrefusalはHTTP 200の中に隠れており、モデル自身の拒否はさらにレスポンス本文を読まないと判別できません。
既存の実装を持っている場合に見直す点
refusalの検出機能をこの仕組みが登場した当初から実装していたり、既存の連携に後付けする場合は、次の3点を見直します。
- refusalはレスポンスであってエラーではないという前提で作られているか。エラー率だけで監視している仕組みは、refusalを専用の指標として別に扱う必要があります
stop_detailsは構造化された詳細を持つ前提になっているか。どのモデルでも、refusalには方針カテゴリを示すstop_detailsオブジェクトが付きます- 拒否されたら別モデルにリトライする設計になっているか。同じモデルに同じリクエストを再送しても、多くの場合は再び拒否されるだけです。コンテキストのリセットだけでなく、フォールバックモデルへのリトライも合わせて用意し、自前で実装する場合は
fallback_credit_tokenを引き継ぎます
ハンドリングはstop_reasonに集約する
refusalの実装で終始意識しておきたいのは、判定条件を stop_reason の値そのものに寄せることです。モデルやAPIのバージョンが変わっても、「refusalかどうか」を判定する軸を1箇所にまとめておけば、個別の分岐が散らばらずに済みます。
実装チェックリスト
message_deltaイベントごとにstop_reason === "refusal"を確認する- refusal検出時に会話コンテキストをリセットする処理を用意する
- Batch APIの結果集計では
succeeded内のstop_reasonもチェックする - リトライを自前実装するなら
fallback_credit_tokenを必ず引き継ぐ category/explanationがnullの場合のフォールバックメッセージを用意する
まとめ
stop_reason: "refusal" はHTTP 200で返るため、既存のエラー監視では検知できません。ストリーム中の message_delta を見て専用に判定し、検出したら会話コンテキストをリセットするか、別モデルへのフォールバックに切り替えます。自前でリトライを組む場合は fallback_credit_token を使わないとプロンプトキャッシュの書き込みコストを二重に払うことになる点に注意します。エラーコード全般の扱いはClaude APIのエラーハンドリング設計、Agent SDK経由でのプロンプトインジェクション拒否はClaudeが間接プロンプトインジェクションを拒否する理由と対策で扱っています。