Claude Media
Claude APIのrefusal stop_reasonを検出してリセットする方法

Claude APIのrefusal stop_reasonを検出してリセットする方法

stop_reason: refusalはHTTP 200で返るためエラー監視では検知できません。検出方法とコンテキストのリセット、fallback creditでの二重課金回避までを扱います。

Claude APIのストリーミングを運用しているなら、先に押さえたいのは次の3点です。refusalはHTTP 200で届くのでエラー監視には映りません。検出したら会話をリセットするか、別のモデルへリトライします。そして課金は拒否のカテゴリと発生タイミングで変わります。

Claudeのrefusalは、ネットワーク異常でも400系の入力エラーでもなく、安全性の分類器が出した判定です。見分ける鍵は stop_reason が "refusal" になることで、理由は stop_details.category に入ります。reasoning_extraction や general_harms といった値もここに入ります。入口が違うので、見るべき場所も対処も通常のエラーとは別になります。

refusalがエラー監視に映らない理由

Claude Fable 5.1・Claude Fable 5・Claude Opus 5.5・Claude Opus 5・Claude Sonnet 5.5には安全性の分類器が入っています。分類器が止めると、レスポンスは stop_reason: "refusal" の成功レスポンスになります。ステータスコードやSDKの例外で異常を拾う監視は、これを一件も数えません。

Message Batchesでも同じです。拒否されたリクエストは errored ではなく succeeded の結果として返り、中身の stop_reason が "refusal" になっています。バッチの集計でエラー件数だけを見ていると、拒否は素通りします。

「拒否」と呼ばれるものは、実は返り方の違う3系統です。

種類返り方発生タイミング
ストリーミング分類器の拒否返り方stop_reason: "refusal"発生タイミングコンテンツが方針に抵触したとき
入力・著作権の検証返り方400エラー発生タイミングリクエストの検証で弾かれたとき
モデル自身の拒否返り方通常のテキスト応答発生タイミングモデルが自ら回答を断ったとき

400は通常のエラー監視で見えます。分類器の拒否はHTTP 200に隠れ、モデル自身の拒否は本文を読まないと分かりません。3つを同じ「エラー」として数えると、分類器の拒否とモデル自身の拒否は取りこぼします。

監視の組み方として、拒否1件につき1イベント、フォールバックで答えた応答1件につき1イベントを出し、両者の差にアラートを置く方法があります。差が開いたら、フォールバックが効かない経路があるということです。サーバー側フォールバックを持たないClaude Haiku 5.5の拒否対応は別に書いています。

レスポンスの形と判定に使うフィールド

拒否が起きると、ストリームの message_delta イベントに stop_reason と stop_details が乗ります。stop_details は拒否のとき必ず存在します。ただし中の category(方針領域)と explanation(利用者に見せられる説明文)は、名前付きの分類に当てはまらない拒否では null になります。

{
  "stop_reason": "refusal",
  "stop_details": {
    "type": "refusal",
    "category": "cyber",
    "explanation": "This request was declined because it could enable cyber harm."
  }
}

この null は欠損ではなく、恒久的な正規の値です。判定条件は category の有無ではなく、stop_reason か stop_details.type に置きます。explanation の文面は安定が保証されていないため、表示には使えても解析には使えません。

fallbacks を付けたリクエストの拒否には、stop_details.recommended_model も入ります。APIがフォールバックを見送ったとき(たとえばフォールバック先がレート制限中のとき)に、直接リトライする先のモデルを示すフィールドです。

検出したら会話をリセットする

stop_reason: "refusal" を受け取ったら、同じ文脈のまま送り直しても拒否が続きます。拒否を引き起こしたターンを取り除くか言い換えるか、会話全体を初期化してから続けます。

messages = []
 
def reset_conversation():
    global messages
    messages = []
 
with client.messages.stream(
    max_tokens=1024,
    messages=messages + [{"role": "user", "content": user_input}],
    model="claude-opus-5-5",
) as stream:
    for event in stream:
        if event.type == "message_delta" and event.delta.stop_reason == "refusal":
            reset_conversation()
            break

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-opus-5-5",
  max_tokens: 1024
});
 
for await (const event of stream) {
  if (event.type === "message_delta" && event.delta.stop_reason === "refusal") {
    resetConversation();
    break;
  }
}

拒否は出力の途中でも起きます。途中で止まった場合、それまでに受け取ったテキストは不完全なものとして捨てます。

拒否の理由別に、次の一手を変える

stop_details.category には、どの方針領域が引っかかったかが入ります。カテゴリによって、リトライが効くかどうか、出力前でも課金されるかどうかが変わります。

category内容出力前の拒否の課金
cyber内容マルウェアや攻撃コードの開発など、サイバー攻撃に転用されうる要求。無害なセキュリティ業務でも該当することがある出力前の拒否の課金されない
bio内容危険な実験手法など、生物学的な危害につながりうる要求。無害なライフサイエンス業務でも該当することがある出力前の拒否の課金される
frontier_llm内容競合するAIモデルの開発を助ける要求。商用利用規約で制限されている領域。無害な機械学習の業務でも該当することがある出力前の拒否の課金される
reasoning_extraction内容モデル自身の推論を応答本文に再現させる要求出力前の拒否の課金される
general_harms内容上の4分類に当てはまらない、利用ポリシー上の懸念領域出力前の拒否の課金されない

category が null の拒否は、出力前なら課金されません。課金の対象になる3分類は、誤検知の量が少ないと計測されているカテゴリで、Anthropicは2026年9月の計測としてこの扱いを示しています。対象のカテゴリは今後変わる可能性があります。

出力の途中で拒否された場合は、カテゴリを問わず、入力トークンと配信済みの出力が通常の料金で課金されます。

reasoning_extractionは、プロンプトを直すしかない

reasoning_extraction には、推奨のフォールバック先がありません。モデルを替えて再送するのではなく、プロンプトを書き換えます。原因になりやすい書き方は次のとおりです。

  • 答える前に埋めさせる <thinking> や <reasoning>、スクラッチパッド用の節
  • JSON出力やツール入力にある reasoning、thinking、trace のフィールド
  • モデルの私的なメモや、推論の経過ログ
  • 推論を一字一句そのまま、あるいは全部出すよう求める指示

該当の文言はシステムプロンプトだけでなく、スキルやツールの説明文に紛れていることもあります。答えの理由を短く説明させたり、根拠や実行した操作の要約を求めたりするのは問題ありません。推論の中身を見たいときは、display: "summarized" を指定して thinking ブロックを読みます。言い換えても拒否が続くときは、リクエストIDを添えてサポートに連絡します。

別モデルへ再送する3つの方法

リセットだけが回復手段ではありません。拒否されたリクエストを別のClaudeモデルへ回すと、多くの場合は答えが返ります。同じモデルへの再送は、通常は再び拒否されます。

選び方

リトライの3方式

  • サーバーサイドフォールバック

    Claude APIのベータ機能です。fallbacks: "default" と server-side-fallback-2026-07-01 ヘッダーを送ると、拒否のカテゴリに応じた推奨モデルで、同じAPI呼び出しの中で再試行されます。Message Batches APIには非対応で、Amazon Bedrock・Google Cloud・Microsoft Foundryでも使えません。

  • SDKミドルウェア

    クライアントの初期化時にフォールバック先のモデルを並べておくと、client.beta.messages 経由の呼び出しで拒否のたびに順番に試されます。どのプラットフォームでも使えます。

  • 自前のリトライ

    生のHTTPや独自のリトライ処理を組むときの方式です。

サーバーサイドフォールバックとSDKミドルウェアは、fallback creditの適用も自動で済ませます。自前で組むのは、この2つが要件に合わないときだけです。

サーバーサイドフォールバックで返る応答には、モデルが切り替わった位置に fallback ブロックが入り、最上位の model が実際に答えたモデルを示します。usage.iterations には試行ごとの使用量が並びます。断ったモデルは message、答えたモデルは fallback_message として出ます。

同じ会話で一度フォールバックすると、約1時間は、fallbacks を付けた後続のリクエストが最初からフォールバック先へ送られます。この経路では拒否したモデルが無いので、fallback ブロックは付きません。見分けるには usage.iterations の fallback_message を見ます。この記録はベストエフォートなので、元のモデルが再び試されることはありえます。

ストリーミングでは、出力前の拒否なら message_start の時点でフォールバック先のモデル名が入ります。ただし最初のバイトが届くのは、拒否された試行が終わってからです。出力の途中で拒否されると、message_start は元のモデル名のままなので、答えたモデルは fallback ブロックの to.model から読みます。

非ストリーミングで途中拒否が起きたときは、拒否したモデルの途中出力は返らず、フォールバック先が最初から答えます。

フォールバック先は、"default" の代わりに自分で最大3つまで指定することもできます。指定したモデルがすべて拒否した場合は、最後の拒否が返ります。

次のターンへ会話を戻すときは、fallback ブロックを元の位置のまま残します。境界より前の thinking・redacted_thinking・connector_text と、クライアント側の tool_use は落とします。拒否された試行もレート制限に数えられる点は、容量計画に入れておきます。

自前のリトライで二重課金を避ける

プロンプトキャッシュはモデルごとに独立しています。拒否されたあとにモデルだけ替えて再送すると、会話の前半をフォールバック先のキャッシュへ書き直すことになり、書き込みは読み取りより高くつきます。これを消すのがfallback creditです。拒否に付いてくるトークンを再送に添えると、会話が最初からフォールバック先で行われていたものとして請求されます。

手順

fallback creditを使った再送

  1. 1

    最初のリクエストにベータヘッダーを付ける

    anthropic-beta: fallback-credit-2026-07-01 を付けます。server-side-fallback-2026-07-01 でも同じフィールドが付き、旧称の fallback-credit-2026-06-01 も引き続き受け付けられます。

  2. 2

    拒否から2つのフィールドを読む

    stop_details.fallback_credit_token(クレジットを表す不透明な文字列)と、stop_details.fallback_has_prefill_claim(再送の本文の形を示す真偽値)です。クレジットが無い拒否では、どちらも null です。

  3. 3

    再送の本文を組む

    拒否されたリクエストを土台に、model をフォールバック先へ替え、最上位のパラメーター fallback_credit_token を足します。

  4. 4

    同じヘッダーで送る

    引き換えにもヘッダーが要るので、再送にも同じ fallback-credit-2026-07-01 を付けます。

fallback_has_prefill_claim が true なら、拒否されたリクエストの本文に、拒否された応答の content をそのまま写したassistantメッセージを1つ足して送ります。フォールバック先は、拒否されたモデルが止まった位置から続きを書き、完了済みのサーバー側ツール呼び出しは再実行されません。false なら、拒否されたリクエストの本文を変えずに送ります。写すassistantメッセージでは、末尾のテキストの末尾空白を取り除き、対応する tool_result が無いクライアント側の tool_use は除きます。

token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}")
 
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 "anthropic-beta: fallback-credit-2026-07-01" \
  -H "content-type: application/json" \
  -d "$(jq -n --arg token "${token}" '{
    model: "claude-opus-4-8",
    max_tokens: 1024,
    messages: [{"role": "user", "content": "Hello, Claude"}],
    fallback_credit_token: $token
  }')"

トークンの寿命は拒否から5分で、発行した組織とワークスペースでしか引き換えられません。有効期限が切れたら、トークン無しで送り直します。トークンはサーバー側に状態を持たないので、確認や失効のためのエンドポイントはありません。

再送先のモデルは、拒否したモデルに許可されたフォールバック先でなければなりません。Claude Fable 5.1とClaude Fable 5なら、Claude Opus 4.8かClaude Opus 5です。許可先はModels APIの allowed_fallback_models で取れます(server-side-fallback-2026-07-01 ヘッダーを付けて呼びます)。

本文が一致しないと、引き換えは400で落ちる

引き換え時には、再送が拒否されたリクエストと照合されます。プロンプトの形を決めるフィールドは完全に一致している必要があります。

扱いフィールド
完全一致が必要フィールドsystem、messages、tools、tool_choice、thinking、cache_control。使っている場合は output_config、mcp_servers、context_management、container も
再送で変えてよいフィールドmodel、max_tokens、stop_sequences、temperature、top_p、top_k、stream、metadata、service_tier

前のターンの thinking や redacted_thinking ブロックを削ってはいけません。トークン無しの再送では削るのが普通ですが、引き換えでは本文が一致している必要があり、これらのブロックはサーバーが自分で扱います。

ベータヘッダーも一致が要ります。片方のリクエストにだけあるヘッダーは、本文が同一でも照合に失敗させます。このときの400は、本文の差と同じ request body ... does not match というメッセージで返るので、本文の問題と読み違えやすい点に注意が必要です。モデルごとにヘッダーを足し引きするのも避けます。照合から外れるのは server-side-fallback-* と fallback-credit-* の2系統で、後者は両方のリクエストに残します。

最初から100万トークンのコンテキストを持つモデル(Claude Fable 5.1、Claude Fable 5、Claude Opus 5.5、Claude Opus 5、Claude Opus 4.8、Claude Sonnet 5.5、Claude Sonnet 5)では、context-1m-2025-08-07 ヘッダーは効果がありません。送るなら両方、送らないなら両方で揃えます。

引き換えが400で拒まれたときの降り方

再送が拒まれた場合は、400のメッセージに従って段階的に形を落とします。

手順

拒まれたときの降り方

  1. 1

    続きを書かせる形が拒まれた

    拒否されたリクエストの本文を、変更せず、トークンを付けたまま送り直します。

  2. 2

    本文が変わらない形も、トークン名指しで拒まれた

    トークン無しで再送します。クレジットは放棄されますが、再送そのものは通ります。

メッセージが redemption temporarily unavailable のときは別です。一時的な拒否なので、同じ本文と同じトークンで、5分の期限内に再試行します。上の段階には降りません。

降りてはいけない場面もあります。拒否されたリクエストでサーバーツールが実行済みなら、トークン無しの再送はそのツールを再実行し、再び課金します。この場合は黙って再送せず、400を呼び出し元に返します。

トークンがどちらの形でも引き換えられない組み合わせも1つあります。リクエストが output_config.format か、ツール使用を強制する tool_choice を使っていると、assistantメッセージを足す形が使えません。そのうえで拒否がサーバーツールの実行後に起きると、本文を変えない形も使えません。このときは、続きを書く形で引き換えるよう求める400が返ります。トークンは捨てて再送できますが、完了済みのツールが再実行されて再課金されるため、費用かエラーを呼び出し元へ見せます。

バッチと他の経路で、取りこぼしやすい点

バッチの拒否はクレジットを発行しません。stop_details には fallback_credit_token が入らず、サーバーサイドフォールバックも使えません。拒否された項目は結果から集め、マルチターンの履歴にあるFable系のthinkingブロックを外し、フォールバック先のモデルで新しいバッチか直接のリクエストとして再投入します。

SDKミドルウェアを入れたアプリから、サーバー側の fallbacks を使うリクエストを送るときは、ミドルウェア無しのクライアントを別に用意します。同じリクエストに両方は設定しません。

エージェントがサブエージェントやツールの内部でモデルを呼ぶ構成では、fallbacks パラメーターは伝わりません。サブエージェントの呼び出しごとに、フォールバックを自分で設定します。リトライ処理やバックグラウンドのワーカーも同じで、フォールバック無しで再発行するコードが1本あると、拒否されやすい経路が無防備になります。

リトライの予算は、ターンやセッションではなくリクエスト単位で数えます。エージェントとそのサブエージェントで、1ターンに拒否が何度も起きることがあるためです。

利用者にどう見せるか

explanation は利用者に見せられる文面として設計されています。ただ null の拒否があるので、これだけに頼る画面は欠落時に破綻します。category ごとに自前の日本語メッセージを用意し、explanation があれば補足として添える二段構えなら、どちらが欠けても画面が崩れません。

拒否の発生頻度も、継続して追う価値があります。特定のプロンプトテンプレートや入力パターンで拒否が増えているなら、プロンプトの設計を見直す合図です。エラー率とは別に、拒否率を専用の指標として並べておきます。

エラーコード全般の扱いはClaude APIのエラーハンドリング設計にまとめています。Agent SDK経由のプロンプトインジェクション拒否は仕組みが別で、Claudeが間接プロンプトインジェクションを拒否する理由と対策が扱っています。

まとめ

拒否は成功レスポンスなので、stop_reason を専用の指標にして数えます。回復はリセットか別モデルへの再送で、自前で再送するなら fallback_credit_token を同じヘッダーと同一の本文で添えます。課金はカテゴリで分かれるので、bio などの拒否は出力が空でも費用に入る前提で見積もります。

この記事を共有:XはてブLinkedIn