거부와 폴백

거부와 폴백 (Refusals and fallback)

Claude Fable 5.1, Claude Fable 5, Claude Opus 5.5, Claude Opus 5에는 요청을 거부할 수 있는 안전 분류기가 포함돼요. 그럴 때 오류가 아니라 stop_reason: "refusal"을 가진 정상 응답을 받아요. stop_details.category가 정책 영역을 이름 지어주죠(거부 응답의 모습 참조). 보통 같은 요청을 다른 Claude 모델에 보내면 답을 얻을 수 있어요. 이 문서는 거부를 인식하는 방법과 그 재시도를 설정하는 방법을 알려드릴게요.

출처: 문서

본문

Claude Fable 5.1, Claude Fable 5, Claude Opus 5.5, Claude Opus 5에는 요청을 거부할 수 있는 안전 분류기가 포함돼요. 그럴 때 오류가 아니라 stop_reason: "refusal"을 가진 정상 응답을 받아요. 그 stop_details.category가 정책 영역을 이름 지어주죠(거부 응답의 모습 참조). 보통 같은 요청을 다른 Claude 모델에 보내면 여전히 답을 얻을 수 있어요. 이 페이지는 거부를 인식하는 방법과 그 재시도를 설정하는 방법을 보여줘요.

이 모델들 중 하나에 구축하면서 거부된 요청이 자동으로 다른 모델로 내려가길 원한다면 이 페이지를 읽으세요. 응답에서 "refusal"을 봤고 다음에 무엇을 해야 할지 알고 싶을 때도 적용돼요.

관련 페이지:

가장 단순한 설정(Claude API에서 베타)은 fallbacks를 "default"로 설정하면, API가 거부된 요청을 그 거부 범주에 대해 Anthropic이 권장하는 폴백 모델에서 재시도해요. 권장 폴백이 없는 범주에서는 거부가 그대로 서게 돼요.

```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": "default", "messages": [{"role": "user", "content": "Hello, Claude"}] }' ```
ant beta:messages create \
  --model claude-fable-5 \
  --max-tokens 1024 \
  --message '{"role":"user","content":"Hello, Claude"}' \
  --fallbacks default \
  --beta server-side-fallback-2026-07-01
client = Anthropic()

response = client.beta.messages.create(
    model="claude-fable-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
    fallbacks="default",
    betas=["server-side-fallback-2026-07-01"],
)
print(response.model)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude" }],
  fallbacks: "default",
  betas: ["server-side-fallback-2026-07-01"]
});
console.log(response.model);
AnthropicClient client = new();

BetaMessage response = await client.Beta.Messages.Create(
    new()
    {
        Model = Messages::Model.ClaudeFable5,
        MaxTokens = 1024,
        Messages = [new() { Content = "Hello, Claude", Role = Role.User }],
        Fallbacks = new Default(),
        Betas = [AnthropicBeta.ServerSideFallback2026_07_01],
    }
);

Console.WriteLine(response.Model.Raw());
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeFable5,
	MaxTokens: 1024,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")),
	},
	Fallbacks: anthropic.BetaFallbacksParamOfDefault(),
	Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01},
})
if err != nil {
	panic(err)
}

fmt.Println(response.Model)
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

BetaMessage response = client.beta().messages().create(MessageCreateParams.builder()
    .model(Model.CLAUDE_FABLE_5)
    .maxTokens(1024L)
    .addUserMessage("Hello, Claude")
    .fallbacksDefault()
    .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01)
    .build());

IO.println(response.model().asString());
$client = new Client();

$response = $client->beta->messages->create(
    model: 'claude-fable-5',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    fallbacks: 'default',
    betas: ['server-side-fallback-2026-07-01'],
);

echo $response->model, PHP_EOL;
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}],
  fallbacks: :default,
  betas: ["server-side-fallback-2026-07-01"]
)

puts response.model

다음 섹션들은 거부 응답이 무엇을 담는지, 서버 측 또는 클라이언트 측 폴백을 언제 쓸지, 각각 어떻게 과금되는지를 다룹니다.

거부 응답의 모습 (What a refusal looks like)

거부는 stop_reason: "refusal"을 가진 성공적인 HTTP 200 응답이에요:

{
  "id": "msg_01XFUDYJgAACzvnptvVoYEL",
  "type": "message",
  "role": "assistant",
  "model": "claude-fable-5",
  "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: 분류기를 트리거한 정책 영역을 이름 지어요.
  • explanation: 사람이 읽을 수 있는 설명. 텍스트는 안정적이지 않으니 파싱하지 말고 표시하세요.
  • recommended_model: fallbacks를 설정한 요청에서만(서버 측 폴백, 베타) 존재해요. API가 폴백 시도를 건너뛰었을 때(예: 폴백 모델이 레이트 제한됨) 직접 재시도할 모델을 이름 지어요. 그 외에는 null이에요. 힌트이지 보장이 아니에요.
  • 거부가 이름 붙은 범주에 매핑되지 않을 때 category와 explanation 모두 null이에요. 그 null은 정상적이고 영구적인 값이지 플레이스홀더가 아니에요.
  • stop_details 자체는 refusal이 아닌 모든 중지 이유에서 null이에요.
category 의미
"cyber" 요청이 악성코드나 익스플로잇 개발 같은 사이버 해악을 가능하게 할 수 있어요. 무해한 사이버 보안 작업도 이 범주를 트리거할 수 있어요.
"bio" 요청이 위험한 실험실 방법 같은 생물학적 해악을 가능하게 할 수 있어요. 유익한 생명과학 작업도 이 범주를 트리거할 수 있어요.
"frontier_llm" 요청이 Anthropic 상업 약관 아래 제한된 경쟁 AI 모델 개발을 도울 수 있어요. 무해한 머신러닝 작업도 이 범주를 트리거할 수 있어요.
"reasoning_extraction" 요청이 응답 텍스트에 내부 추론을 재현하도록 요구해요. 구조화된 형태로 추론을 얻으려면 적응형 thinking을 사용하세요.
"general_harms" 요청이 네 개의 이름 붙은 범주 밖의 사용 정책 영역에 해당해요. 무해한 작업도 이 범주를 트리거할 수 있어요.

거부는 출력 전에 도착하거나, 부분 출력 후 스트림 중간에 도착할 수 있어요. 어느 경우든 부분 출력은 불완전한 것으로 취급하고 버리세요.

**거부의 과금 방식:** 출력 전에 도착한 거부는 과금되지 않아요. `content`는 비어 있고, 토큰 수는 `usage`에 나타나지만 청구되지 않아요. 요청은 여전히 레이트 한계에 집계돼요. 스트림 중간 거부는 입력 토큰과 이미 스트리밍된 출력을 정상 요율로 청구해요.

폴백 접근 방식 고르기 (Picking a fallback approach)

거부된 요청을 다른 모델에서 재시도하는 세 가지 방법이 있어요. 올바른 것은 어디서 실행하는지와 얼마나 많은 제어가 필요한지에 달려 있어요.

상황 사용 이유
Claude API, 가장 단순한 설정 서버 측 폴백 요청 하나, 응답 하나. API가 재시도를 처리해요.
모든 플랫폼, Anthropic SDK 사용 SDK 미들웨어 클라이언트에서 한 번 구성. 재시도가 자동으로 일어나요.
원시 HTTP 또는 커스텀 재시도 로직 수동 재시도 + 폴백 크레딧 완전한 제어. 폴백 크레딧이 비용을 낮춰요.

서버 측 폴백과 SDK 미들웨어는 폴백 크레딧을 대신 적용해요. 폴백 크레딧 페이지는 재시도를 직접 만들 때만 필요해요.

서버 측 폴백 (Server-side fallback)

서버 측 폴백은 거부된 요청을 단일 API 호출 안에서 재시도해요. 기본 모드에서 기본 모델이 거부하고 거부 범주에 권장 폴백이 있으면, API는 같은 요청을 그 범주에 대해 Anthropic이 권장하는 모델에서 실행해요. 최대 세 개의 자기 폴백 모델을 이름 지어 부를 수도 있어요. 어느 쪽이든 답한 모델을 이름 짓는 응답 하나를 돌려받으므로, 사용자는 한 번의 왕복으로 답을 얻어요.

서버 측 폴백은 Claude API에서 베타예요. `fallbacks` 파라미터는 [Message Batches API](https://platform.claude.com/docs/en/build-with-claude/batch-processing)에서 지원되지 않고(포함한 배치 항목은 에러 결과로 돌아옴) Amazon Bedrock, Google Cloud, Microsoft Foundry에서는 사용할 수 없어요. 그 플랫폼에서는 [SDK 미들웨어로 클라이언트 측 폴백](https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback#client-side-fallback)을 사용하세요.

요청하기 (Making the request)

fallbacks 파라미터를 문자열 "default"로 설정하고 server-side-fallback-2026-07-01 베타 헤더를 보내세요. 그러면 API가 요청된 모델의 서버 정의 기본 라우팅을 적용하는데, 분류기가 보고하는 거부 범주를 바탕으로 권장 폴백 모델을 선택해요. 그래서 권장사항이 바뀌어도 모델 목록을 유지하지 않고 거부된 요청이 서비스돼요.

기본 라우팅은 직접 고르지 않은 모델에 대한 선행 초대형 이미지 거부를 절대 이끌지 않아요. "oversized_image": "error"로 표시된 이미지를 리사이즈할 라우팅 모델은 대신 라우팅에서 빠지므로, 표시된 이미지는 리사이즈되어 서비스되지 않아요.

```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": "default", "messages": [{"role": "user", "content": "Hello, Claude"}] }' | jq -c '{ stop_reason, model, # A fallback_message entry in usage.iterations means a fallback model ran; # pair it with stop_reason to confirm the fallback served the response. served_by_fallback: ( any(.usage.iterations[]?; .type == "fallback_message") and .stop_reason != "refusal" ) }' ```
ant beta:messages create \
  --model claude-fable-5 \
  --max-tokens 1024 \
  --message '{"role":"user","content":"Hello, Claude"}' \
  --fallbacks default \
  --beta server-side-fallback-2026-07-01 \
  --format json |
  jq -c '{
    stop_reason,
    model,
    # A fallback_message entry in usage.iterations means a fallback model ran;
    # pair it with stop_reason to confirm the fallback served the response.
    served_by_fallback: (
      any(.usage.iterations[]?; .type == "fallback_message")
      and .stop_reason != "refusal"
    )
  }'
client = Anthropic()

response = client.beta.messages.create(
    model="claude-fable-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
    fallbacks="default",
    betas=["server-side-fallback-2026-07-01"],
)

# A fallback_message entry in usage.iterations means a fallback model ran;
# pair it with stop_reason to confirm the fallback served the response.
fallback_ran = any(
    iteration.type == "fallback_message"
    for iteration in response.usage.iterations or []
)
served_by_fallback = fallback_ran and response.stop_reason != "refusal"

print(
    json.dumps(
        {
            "stop_reason": response.stop_reason,
            "model": response.model,
            "served_by_fallback": served_by_fallback,
        }
    )
)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude" }],
  fallbacks: "default",
  betas: ["server-side-fallback-2026-07-01"]
});

// A fallback_message entry in usage.iterations means a fallback model ran;
// pair it with stop_reason to confirm the fallback served the response.
const { stop_reason, model, usage } = response;
const servedByFallback =
  (usage.iterations ?? []).some((entry) => entry.type === "fallback_message") &&
  stop_reason !== "refusal";

console.log(
  JSON.stringify({
    stop_reason,
    model,
    served_by_fallback: servedByFallback
  })
);
AnthropicClient client = new();

var response = await client.Beta.Messages.Create(
    new()
    {
        Model = Messages::Model.ClaudeFable5,
        MaxTokens = 1024,
        Messages =
        [
            new() { Content = "Hello, Claude", Role = Role.User },
        ],
        Fallbacks = new Default(),
        Betas = [AnthropicBeta.ServerSideFallback2026_07_01],
    }
);

// A fallback_message entry in usage.iterations means a fallback model ran;
// pair it with stop_reason to confirm the fallback served the response.
bool fallbackRan = (response.Usage.Iterations ?? []).Any(iteration =>
    iteration.TryPickBetaFallbackMessageIterationUsage(out _)
);
bool servedByFallback =
    fallbackRan && response.StopReason?.Value() != BetaStopReason.Refusal;

Console.WriteLine(
    JsonSerializer.Serialize(
        new
        {
            stop_reason = response.StopReason?.Raw(),
            model = response.Model.Raw(),
            served_by_fallback = servedByFallback,
        }
    )
);
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeFable5,
	MaxTokens: 1024,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")),
	},
	Fallbacks: anthropic.BetaFallbacksParamOfDefault(),
	Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01},
})
if err != nil {
	panic(err)
}

// A fallback_message entry in usage.iterations means a fallback model ran;
// pair it with stop_reason to confirm the fallback served the response.
fallbackRan := slices.ContainsFunc(
	response.Usage.Iterations,
	func(iteration anthropic.BetaIterationsUsageItemUnion) bool {
		_, isFallback := iteration.AsAny().(anthropic.BetaFallbackMessageIterationUsage)
		return isFallback
	},
)
servedByFallback := fallbackRan && response.StopReason != anthropic.BetaStopReasonRefusal

summary, err := json.Marshal(struct {
	StopReason       anthropic.BetaStopReason `json:"stop_reason"`
	Model            anthropic.Model          `json:"model"`
	ServedByFallback bool                     `json:"served_by_fallback"`
}{response.StopReason, response.Model, servedByFallback})
if err != nil {
	panic(err)
}
fmt.Println(string(summary))
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

BetaMessage response = client.beta().messages().create(
    MessageCreateParams.builder()
        .model(Model.CLAUDE_FABLE_5)
        .maxTokens(1024L)
        .addUserMessage("Hello, Claude")
        .fallbacksDefault()
        .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01)
        .build()
);

// A fallback_message usage entry means a fallback model produced the
// response; a refusal stop reason means no model served it.
List<BetaUsage.Iteration> iterations =
    response.usage().iterations().orElse(List.of());
boolean servedByFallback =
    iterations.stream().anyMatch(BetaUsage.Iteration::isFallbackMessage)
        && response.stopReason().filter(BetaStopReason.REFUSAL::equals).isEmpty();

IO.println("""
    {"stop_reason":"%s","model":"%s","served_by_fallback":%b}\
    """.formatted(
        response.stopReason().map(BetaStopReason::asString).orElse("null"),
        response.model().asString(),
        servedByFallback));
$client = new Client();

$response = $client->beta->messages->create(
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    model: 'claude-fable-5',
    fallbacks: 'default',
    betas: ['server-side-fallback-2026-07-01'],
);

// A fallback_message entry in usage.iterations means a fallback model ran;
// pair it with stop_reason to confirm the fallback served the response.
$iterations = $response->usage->iterations ?? [];
$servedByFallback = array_any($iterations, fn($entry) => $entry->type === 'fallback_message')
    && $response->stopReason !== 'refusal';

echo json_encode([
    'stop_reason' => $response->stopReason,
    'model' => $response->model,
    'served_by_fallback' => $servedByFallback,
]), PHP_EOL;
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}],
  fallbacks: :default,
  betas: ["server-side-fallback-2026-07-01"]
)

# A fallback_message entry in usage.iterations means a fallback model ran;
# pair it with stop_reason to confirm the fallback served the response.
iterations = response.usage.iterations || []
served_by_fallback = iterations.any? { it.type == :fallback_message } &&
  response.stop_reason != :refusal

stop_reason = response.stop_reason
model = response.model
puts JSON.generate({stop_reason:, model:, served_by_fallback:})

Anthropic은 모델의 능력에 맞춰 각 모델과 각 정책 범주별로 안전장치를 설정해요. 범주에 따라 플래그된 요청이 덜 유능한 모델로 폴백되거나 거부될 수 있어요. "default" 모드는 이러한 모델별·범주별 권장사항을 인코딩해 주므로, 거부된 요청은 그 범주에 대해 Anthropic이 권장하는 모델에서 재시도돼요. 폴백은 어느 쪽이든 보여요. 응답이 서비스한 모델을 이름 짓고, fallback 콘텐츠 블록이 인계를 표시해요.

라우팅은 서버 측에서 적용되고 Models API에 모델별로 공개되지 않아요. 거부된 요청을 어떤 모델이 서비스했는지 보려면 이 페이지의 샘플처럼 응답의 최상위 model 필드와 usage.iterations의 fallback_message 항목을 확인하세요.

안전 분류기 거부만 폴백을 트리거해요. 요청된 모델의 레이트 한계, 과부하, 서버 오류는 그대로 반환돼요.

베타 헤더는 정확히 날짜 `2026-07-01`(이는 `"default"`와 명시적 목록 형태 둘 다를 지원) 또는 `2026-06-01`(명시적 목록 형태만 허용)을 지녀야 해요. 다른 `server-side-fallback-*` 값 아래에서는 `fallbacks` 파라미터가 400 오류로 거부돼요. 이 기능의 이전 미리보기 버전에 구축했다면 베타 헤더와 요청·응답 형태를 이 페이지의 것으로 함께 업데이트하세요.

자기 폴백 모델 이름 짓기 (Naming your own fallback models)

기본 라우팅 대신 fallbacks를 최대 세 개 모델의 목록으로 설정할 수 있어요. 요청된 모델이 거부하면 API가 같은 요청에서 체인의 다음 모델을 실행해요. 거부된 요청을 정확히 어떤 모델이 서비스하는지 제어하려 할 때(애플리케이션이 자격을 부여한 모델 고정 같은) 이 형태를 사용하세요.

이름 붙은 폴백 모델은 초대형 이미지 확인에 포함돼요. "oversized_image": "error"를 설정한 이미지 블록이 있는 요청은 요청된 모델과 모든 이름 붙은 폴백에 대해 선행으로 확인되고, 그 중 하나라도 그 이미지를 리사이즈하면 거부되며, 보고된 리스케일 대상을 모두에 맞춰요.

강조된 줄이 기본 라우팅 요청과의 유일한 차이예요.

```bash cURL curl --fail-with-body -sS https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: server-side-fallback-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "fallbacks": [{"model": "claude-opus-4-8"}], "messages": [{"role": "user", "content": "Hello, Claude"}] }' ```
ant beta:messages create \
  --model claude-fable-5 \
  --max-tokens 1024 \
  --message '{"role":"user","content":"Hello, Claude"}' \
  --fallbacks '[{"model":"claude-opus-4-8"}]' \
  --beta server-side-fallback-2026-07-01
client = Anthropic()

response = client.beta.messages.create(
    model="claude-fable-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
    fallbacks=[{"model": "claude-opus-4-8"}],
    betas=["server-side-fallback-2026-07-01"],
)
print(response.model)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude" }],
  fallbacks: [{ model: "claude-opus-4-8" }],
  betas: ["server-side-fallback-2026-07-01"]
});
console.log(response.model);
AnthropicClient client = new();

BetaMessage response = await client.Beta.Messages.Create(
    new()
    {
        Model = Messages::Model.ClaudeFable5,
        MaxTokens = 1024,
        Messages = [new() { Content = "Hello, Claude", Role = Role.User }],
        Fallbacks = new([new(Messages::Model.ClaudeOpus4_8)]),
        Betas = [AnthropicBeta.ServerSideFallback2026_07_01],
    }
);

Console.WriteLine(response.Model.Raw());
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeFable5,
	MaxTokens: 1024,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")),
	},
	Fallbacks: anthropic.BetaFallbacksParamUnion{
		OfBetaFallbackArray: []anthropic.BetaFallbackParam{{Model: anthropic.ModelClaudeOpus4_8}},
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaServerSideFallback2026_07_01},
})
if err != nil {
	panic(err)
}

fmt.Println(response.Model)
AnthropicClient client = AnthropicOkHttpClient.fromEnv();

BetaMessage response = client.beta().messages().create(MessageCreateParams.builder()
    .model(Model.CLAUDE_FABLE_5)
    .maxTokens(1024L)
    .addUserMessage("Hello, Claude")
    .fallbacksOfFallbackParams(List.of(BetaFallbackParam.builder()
        .model(Model.CLAUDE_OPUS_4_8)
        .build()))
    .addBeta(AnthropicBeta.SERVER_SIDE_FALLBACK_2026_07_01)
    .build());

IO.println(response.model().asString());
$client = new Client();

$response = $client->beta->messages->create(
    model: 'claude-fable-5',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    fallbacks: [['model' => 'claude-opus-4-8']],
    betas: ['server-side-fallback-2026-07-01'],
);

echo $response->model, PHP_EOL;
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}],
  fallbacks: [{model: "claude-opus-4-8"}],
  betas: ["server-side-fallback-2026-07-01"]
)

puts response.model

fallbacks 목록에 적용되는 몇 가지 규칙:

  • 항목은 순서대로 시도돼요. 각각은 다른 항목들과 요청된 모델과 구별되어야 해요.
  • 각 항목은 요청된 모델의 허용된 대상 중 하나여야 해요. 베타 헤더가 설정되면 그 목록은 Models API의 모델 항목에 allowed_fallback_models로 공개돼요.
  • 각 항목은 model을 이름 짓고, 그 시도에 한해서 max_tokens, thinking, output_config, speed를 덮어쓸 수 있어요.
  • 요청은 이름 붙은 모든 모델에 대한 직접 요청으로 유효해야 해요. 폴백 모델이 요청이 쓰는 기능을 지원하지 않으면 API는 요청을 선행으로 거부해요.
  • 기본 모드처럼 안전 분류기 거부만 폴백을 트리거해요. 요청된 모델의 레이트 한계, 과부하, 서버 오류는 그대로 반환돼요.
  • 폴백 모델이 레이트 제한되거나 과부하되면 폴백 시도가 이루어지지 않고 이전 거부가 대신 반환돼요. 그 거부의 stop_details.recommended_model이 직접 재시도할 모델을 이름 지어요. 폴백 모델의 레이트 한계를 기대하는 거부 볼륨에 맞춰 크기 조정하거나, 부하 아래에서 폴백이 거부로 저하돼요.

응답은 두 모드에서 같은 형태예요. 턴을 서비스한 모델이 최상위 model 필드에 나타나고, fallback 콘텐츠 블록이 인계를 표시하며, usage.iterations가 각 시도를 기록해요.

응답이 담는 것 (What the response contains)

응답은 다른 메시지처럼 보이는데 두 가지가 추가돼요:

  • 최상위 model 필드는 반환된 메시지를 만든 모델을 보고해요. 요청된 모델이든 폴백이든요.

  • fallback 콘텐츠 블록은 한 모델의 출력이 다음 모델로 넘어가는 content의 각 지점을 표시해요: {"type": "fallback", "from": {"model": ...}, "to": {"model": ...}}.

    • from.model은 거부하는 홉이 요청된 모델일 때 보낸 모델 문자열을 되돌려줘요.
    • to.model은 항상 이어가는 모델의 해결된 ID예요.

출력 전 거부에서는 fallback 블록이 첫 콘텐츠 블록이에요. 예를 들어 기본 라우팅이 그 거부 범주에 대해 Claude Opus 4.8을 선택하면:

{
  "id": "msg_01XFUDYJgAACzvnptvVoYEL",
  "type": "message",
  "role": "assistant",
  "model": "claude-opus-4-8",
  "content": [
    {
      "type": "fallback",
      "from": { "model": "claude-fable-5" },
      "to": { "model": "claude-opus-4-8" }
    },
    { "type": "text", "text": "Hi! How can I help you today?" }
  ],
  "stop_reason": "end_turn",
  "stop_details": null,
  "usage": {
    "input_tokens": 412,
    "output_tokens": 264,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 0,
    "iterations": [
      {
        "type": "message",
        "model": "claude-fable-5",
        "input_tokens": 535,
        "output_tokens": 0,
        "cache_read_input_tokens": 0,
        "cache_creation_input_tokens": 0
      },
      {
        "type": "fallback_message",
        "model": "claude-opus-4-8",
        "input_tokens": 412,
        "output_tokens": 264,
        "cache_read_input_tokens": 0,
        "cache_creation_input_tokens": 0
      }
    ]
  }
}

usage.iterations 배열은 모든 시도를 기록해요. 거부한 모델은 일반 message 항목으로 나타나고, 턴을 서비스한 모델은 fallback_message 항목으로 나타나요. 체인의 모든 모델이 거부하면 응답은 마지막 모델의 거부이고, 각 이전 홉에 message 항목, 마지막에 fallback_message 항목이 있어요.

스티키 라우팅이 이후 턴을 곧장 폴백 모델로 보낼 수 있어요. 그런 턴은 거부한 모델이 없으므로 fallback 콘텐츠 블록을 지니지 않아요. usage.iterations의 fallback_message 항목, 요청된 모델의 message 항목 부재, 응답의 model 필드로 식별하세요.

대화 이어가기 (Continuing the conversation)

다음 턴에는 어시스턴트 콘텐츠를 받은 그대로 다시 보내세요. 중간 출력 폴백 후 content는 인계 전에 거부한 모델이 만든 블록 유형을 포함할 수 있어요. 다음 표는 턴을 되돌려 보낼 때 무엇을 유지하고 무엇을 버릴지 다룹니다.

블록 유형 다음 턴에서
fallback 나타난 정확한 위치에 유지하세요. API는 그 위치를 사용해 주변 thinking 블록을 검증하므로, 경계 양쪽의 thinking 블록을 되돌려 보내는 요청은 블록이 생략되거나 이동되면 거부돼요.
text 유지.
마지막 fallback 블록 이후의 모든 블록 유지.
마지막 fallback 블록 앞의 thinking, redacted_thinking, connector_text 버림.
마지막 fallback 블록 앞의 클라이언트 측 tool_use 버림.
마지막 fallback 블록 앞의 server_tool_use 결과와 쌍을 이루면 유지. 맞는 결과가 없으면 버림.
`connector_text` 블록은 일부 도구 사용 응답이 도구 호출 사이에 포함하는 내레이션 텍스트를 지녀요.

스트리밍 (Streaming)

스트리밍 요청에서 재시도는 같은 스트림에서 일어나고, 이미 받은 어떤 것도 무효화되지 않아요. 보는 것은 거부가 언제 일어나는지에 따라 달라져요.

거부가 출력 전에 일어날 때:

  • message_start는 폴백 모델을 이름 짓고, fallback 블록이 첫 콘텐츠 블록이에요.
  • message_start가 폴백 시도 시작을 기다리므로, 첫 바이트까지 시간에는 거부된 시도가 포함돼요.

거부가 중간 출력으로 일어날 때:

  • 열린 콘텐츠 블록이 닫히고, fallback 블록(델타가 없는 평범한 content_block_start/content_block_stop 쌍)이 경계를 표시해요.
  • 폴백 모델은 부분 출력에서 이어가요. 부분 출력의 text 블록만 컨텍스트로 폴백 모델에 전달돼요. 다른 블록 유형은 content에 남아요.
  • message_start가 이미 요청된 모델을 이름 지었으므로, 서비스 모델은 fallback 블록의 to.model과 최종 message_delta의 usage.iterations에 있는 fallback_message 항목에서 읽으세요.

비스트리밍 응답 (Non-streaming responses)

비스트리밍 요청에서 중간 출력 거부는 다르게 동작해요. 응답이 거부된 모델의 부분 출력을 생략하고, 폴백 모델이 처음부터 답해요. 결과는 출력 전 거부처럼 보이고 fallback 블록이 첫 번째예요. 거부된 시도와 그 출력 토큰은 여전히 usage.iterations에 나타나요.

**도구 사용 중 거부:** 완료된 도구 작업은 폴백을 막지 않아요. 요청 내에서 서버 도구(웹 검색이나 코드 실행 같은)가 실행을 마친 뒤 거부가 발화하면 폴백 시도가 진행돼요. 완료된 도구 결과가 이월되고, 폴백 모델이 서버 도구를 계속 호출할 수 있어요. 재시도하지 않는 유일한 경우는 스트림에서 어떤 유형의 도구 사용 블록(클라이언트 도구, 서버 도구, MCP 도구 호출)이 여전히 열려 있는 동안 발화하는 스트리밍 거부예요. 그 거부는 직접 반환되고, `fallback-credit-2026-07-01` 헤더가 설정되어 있으면 부분 응답을 이어가며 상환할 수 있는 크레딧 토큰을 여전히 지녀요. 비스트리밍 요청은 영향받지 않아요. API는 부분 작업을 지우고 응답 전에 재시도해요.

과금과 레이트 한계 (Billing and rate limits)

출력을 만들기 전에 거부한 시도는 과금되지 않아요. 그 토큰은 usage.iterations 항목에 보고되지만 청구되지 않아요. 출력을 만든 모든 시도(응답 도중 거부한 것 포함)는 그것을 실행한 모델의 요율로 개별 과금돼요. usage.iterations 배열은 과금되는 대상의 시도별 기록이에요. 최상위 usage 카운트는 반환된 메시지를 만든 시도만 설명해요. 다른 모델의 토큰은 하나의 필드로 합산되지 않아요.

실행되는 모든 시도(거부한 것 포함)는 자기 모델의 레이트 한계에 집계돼요.

스티키 라우팅 (Sticky routing)

대화가 폴백된 후 API는 어떤 모델이 서비스했는지 기록해요. 그 대화에 대한 이후 요청이 fallbacks를 포함하면 요청된 모델을 실행하지 않고 곧장 그 폴백 모델로 가요. 이는 매 턴 예측 가능하게 다시 거부될 시도를 위해 돈을 내지 않게 해줘요.

라우팅 결정의 몇 가지 속성:

  • 약 1시간 동안 유지되고 조직 범위로 한정돼요.
  • 대화 접두사의 콘텐츠 해시와 그것을 서비스한 모델로 저장돼요. 메시지 콘텐츠 자체는 저장되지 않아요.
  • best-effort이므로, 코드는 요청된 모델이 언제든 다시 시도되는 것을 처리해야 해요.

스티키 라우팅은 스트리밍과 비스트리밍 요청 모두에 적용돼요. 스트리밍 요청에서 라우팅 결정은 스트림이 열리기 전에 내려지므로, message_start 이벤트의 model 필드가 이미 폴백 모델의 ID를 지녀요.

SDK 미들웨어로 클라이언트 측 폴백 (Client-side fallback with the SDK middleware)

모든 Anthropic SDK에는 거부 폴백 미들웨어가 포함돼 있어요. 클라이언트에서 자기 폴백 모델 목록으로 한 번 구성해요. 그 후 client.beta.messages를 통한 호출은 모든 플랫폼에서 거부된 요청을 자동으로 재시도해요. 미들웨어는 다루는 모든 요청에 fallback-credit-2026-07-01 베타 헤더도 보내므로, 요청별 설정 없이 재시도가 재가격 책정돼요.

설정하기 (Setting it up)

미들웨어를 클라이언트 생성자에 전달하고, 대화의 요청들에 하나의 BetaFallbackState 인스턴스를 공유하세요.

```bash cURL # The refusal-fallback middleware is an SDK feature. See the # server-side fallback section for the equivalent single-request approach, # or the fallback credit page for the raw HTTP retry pattern. ```
# The refusal-fallback middleware is an SDK feature. See the
# server-side fallback section for the equivalent single-request approach,
# or the fallback credit page for the raw HTTP retry pattern.
from anthropic import Anthropic, BetaFallbackState, BetaRefusalFallbackMiddleware

# On a refusal, the middleware retries on the listed fallback model and
# automatically sends the fallback-credit beta header on every request it handles.
client = Anthropic(
    middleware=[BetaRefusalFallbackMiddleware([{"model": "claude-opus-4-8"}])],
)

state = BetaFallbackState()  # pins follow-ups to the model that accepted

# Streaming: on a refusal the middleware retries on the fallback model and
# splices its events onto the open stream.
with (
    state,
    client.beta.messages.stream(
        max_tokens=1024,
        model="claude-fable-5",
        messages=[{"role": "user", "content": "Hello, Claude"}],
    ) as stream,
):
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final_message = stream.get_final_message()
print(f"\nserved by: {final_message.model}")

# Non-streaming: reusing the state keeps the conversation pinned.
with state:
    message = client.beta.messages.create(
        max_tokens=1024,
        model="claude-fable-5",
        messages=[{"role": "user", "content": "Hello, Claude"}],
    )
print(f"served by: {message.model}")
import { BetaFallbackState, betaRefusalFallbackMiddleware } from "@anthropic-ai/sdk";

// On a refusal, the middleware retries on the listed fallback model and
// automatically sends the fallback-credit beta header on every request it handles.
const client = new Anthropic({
  middleware: [betaRefusalFallbackMiddleware([{ model: "claude-opus-4-8" }])]
});

// Share one state across the conversation so follow-up requests stay
// pinned to the model that accepted.
const fallbackState = new BetaFallbackState();

// Streaming: on a refusal the middleware retries on the fallback model and
// splices its events onto the open stream.
const stream = client.beta.messages
  .stream(
    {
      max_tokens: 1024,
      model: "claude-fable-5",
      messages: [{ role: "user", content: "Hello, Claude" }]
    },
    { fallbackState }
  )
  .on("text", (text) => process.stdout.write(text));

const finalMessage = await stream.finalMessage();
console.log("\nserved by:", finalMessage.model);

// Non-streaming: reusing the state keeps the conversation pinned.
const message = await client.beta.messages.create(
  {
    max_tokens: 1024,
    model: "claude-fable-5",
    messages: [{ role: "user", content: "Hello, Claude" }]
  },
  { fallbackState }
);
console.log("served by:", message.model);
using Anthropic;
using Anthropic.Helpers;
using Anthropic.Models.Beta.Messages;
using Messages = Anthropic.Models.Messages;

// On a refusal, the handler retries on the listed fallback model and
// automatically sends the fallback-credit beta header on every request it handles.
AnthropicClient client = new()
{
    Handlers =
    [
        new BetaRefusalFallbackHandler { Fallbacks = [new(Messages::Model.ClaudeOpus4_8)] },
    ],
};

// Pins follow-up requests sharing this state to the model that accepted.
BetaFallbackState fallbackState = BetaFallbackState.Create();

MessageCreateParams parameters = new()
{
    Model = Messages::Model.ClaudeFable5,
    MaxTokens = 1024,
    Messages = [new() { Content = "Hello, Claude", Role = Role.User }],
};

// Streaming: if the stream ends in a refusal, the handler splices the fallback
// model's events onto the still-open stream.
BetaMessageContentAggregator aggregator = new();
using (fallbackState.Use())
{
    var responseUpdates = client.Beta.Messages.CreateStreaming(parameters);
    await foreach (BetaRawMessageStreamEvent rawEvent in responseUpdates.CollectAsync(aggregator))
    {
        if (
            rawEvent.TryPickContentBlockDelta(out var deltaEvent)
            && deltaEvent.Delta.TryPickText(out var textDelta)
        )
        {
            Console.Write(textDelta.Text);
        }
    }
}
BetaMessage streamedMessage = aggregator.Message();
Console.WriteLine($"\nserved by: {streamedMessage.Model.Raw()}");

// Non-streaming: reusing the state keeps the conversation pinned to the model that accepted.
using (fallbackState.Use())
{
    BetaMessage message = await client.Beta.Messages.Create(parameters);
    Console.WriteLine($"served by: {message.Model.Raw()}");
}
import (
// ...
	"github.com/anthropics/anthropic-sdk-go/lib/betafallback"
// ...
)

func main() {
	ctx := context.Background()

	// The middleware retries a refused request on each fallback model in
	// turn, and opts requests into the fallback-credit beta automatically.
	client := anthropic.NewClient(
		option.WithMiddleware(betafallback.BetaRefusalFallbackMiddleware(
			[]anthropic.BetaFallbackParam{{Model: anthropic.ModelClaudeOpus4_8}},
		)),
	)

	// One state per conversation: requests sharing it stay pinned to the
	// model that accepted, so a follow-up never re-asks a model that refused.
	state := &betafallback.BetaFallbackState{}
	conversation := betafallback.WithBetaFallbackState(state)

	params := anthropic.BetaMessageNewParams{
		MaxTokens: 1024,
		Model:     anthropic.ModelClaudeFable5,
		Messages: []anthropic.BetaMessageParam{
			anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")),
		},
	}

	// Streaming: on a refusal the middleware retries in place, splicing the
	// fallback model's events onto the open stream as one continuous message.
	stream := client.Beta.Messages.NewStreaming(ctx, params, conversation)
	defer stream.Close()
	var streamed anthropic.BetaMessage
	for stream.Next() {
		event := stream.Current()
		if err := streamed.Accumulate(event); err != nil {
			panic(err)
		}
		switch eventVariant := event.AsAny().(type) {
		case anthropic.BetaRawContentBlockDeltaEvent:
			if textDelta, ok := eventVariant.Delta.AsAny().(anthropic.BetaTextDelta); ok {
				fmt.Print(textDelta.Text)
			}
		}
	}
	if err := stream.Err(); err != nil {
		panic(err)
	}
	fmt.Println("\nserved by:", streamed.Model)

	// Non-streaming: the shared state pins this follow-up to the model that
	// served the streamed turn.
	message, err := client.Beta.Messages.New(ctx, params, conversation)
	if err != nil {
		panic(err)
	}
	fmt.Println("served by:", message.Model)
}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.RequestOptions;
import com.anthropic.core.http.StreamResponse;
import com.anthropic.helpers.BetaFallbackState;
import com.anthropic.helpers.BetaMessageAccumulator;
import com.anthropic.helpers.BetaRefusalFallbackInterceptor;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaRawMessageStreamEvent;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;

void main() {
    // The interceptor retries refused requests on the fallback model. It automatically
    // adds the fallback-credit beta header to every request it handles.
    AnthropicClient client = AnthropicOkHttpClient.builder()
        .fromEnv()
        .addInterceptor(BetaRefusalFallbackInterceptor.builder()
            .addFallback(Model.CLAUDE_OPUS_4_8)
            .build())
        .build();

    // Share one state across requests so follow-ups stay pinned to the model that accepted.
    BetaFallbackState state = BetaFallbackState.create();

    MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_FABLE_5)
        .maxTokens(1024)
        .addUserMessage("Hello, Claude")
        .build();

    // Streaming: on a refusal, the fallback model's events are spliced onto the open stream.
    BetaMessageAccumulator accumulator = BetaMessageAccumulator.create();
    try (StreamResponse<BetaRawMessageStreamEvent> streamResponse = client.beta()
            .messages()
            .createStreaming(params, RequestOptions.builder().fallbackState(state).build())) {
        streamResponse.stream()
            .peek(accumulator::accumulate)
            .forEach(event -> event.contentBlockDelta()
                .flatMap(deltaEvent -> deltaEvent.delta().text())
                .ifPresent(textDelta -> IO.print(textDelta.text())));
    }
    IO.println("\nserved by: " + accumulator.message().model().asString());

    // Non-streaming: reusing the same state keeps the conversation pinned.
    BetaMessage message = client.beta()
        .messages()
        .create(params, RequestOptions.builder().fallbackState(state).build());
    IO.println("served by: " + message.model().asString());
}
use Anthropic\Beta\Messages\BetaRawContentBlockDeltaEvent;
use Anthropic\Beta\Messages\BetaTextDelta;
use Anthropic\Client;
use Anthropic\Lib\Middleware\BetaFallbackState;
use Anthropic\Lib\Middleware\RefusalFallbackMiddleware;
use Anthropic\Lib\Streaming\MessageAccumulator;

// Configure the fallback chain once. On a refusal, the middleware retries the
// request down the chain and sends the fallback-credit beta header for you.
$client = new Client(
    requestOptions: [
        'middleware' => [new RefusalFallbackMiddleware([['model' => 'claude-opus-4-8']])],
    ],
);

// Share one state across the conversation so follow-up requests stay pinned
// to the model that accepted.
$state = new BetaFallbackState();

// Streaming: on a refusal the middleware splices the fallback model's events
// onto the still-open stream. The accumulator's model is the serving model.
$stream = $client->beta->messages->createStream(
    model: 'claude-fable-5',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    requestOptions: ['fallbackState' => $state],
);
$accumulator = MessageAccumulator::forBetaMessages();
foreach ($stream as $event) {
    $accumulator->accumulate($event);
    if ($event instanceof BetaRawContentBlockDeltaEvent
        && $event->delta instanceof BetaTextDelta) {
        echo $event->delta->text;
    }
}
echo "\nserved by: {$accumulator->message()->model}\n";

// Non-streaming: same middleware. Reusing the state keeps the conversation
// pinned to the model that accepted.
$message = $client->beta->messages->create(
    model: 'claude-fable-5',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
    requestOptions: ['fallbackState' => $state],
);
echo "served by: {$message->model}\n";
# On a refusal, the middleware retries the request down the fallback chain.
# It sends the fallback-credit beta header on every request it handles.
client = Anthropic::Client.new(
  middleware: [Anthropic::BetaRefusalFallbackMiddleware.new([{model: "claude-opus-4-8"}])]
)

# Share one state across the conversation so follow-up requests stay
# pinned to the model that accepted.
state = Anthropic::BetaFallbackState.new

# Streaming: on a refusal the middleware splices the fallback model's
# events onto the still-open stream.
stream = client.beta.messages.stream(
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}],
  request_options: {fallback_state: state}
)
stream.text.each { print it }
puts "\nserved by: #{stream.accumulated_message.model}"

# Non-streaming: reusing the state keeps the conversation pinned to the model that accepted.
message = client.beta.messages.create(
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}],
  request_options: {fallback_state: state}
)
puts "served by: #{message.model}"

동작 방식 (How it behaves)

  • 재시도는 폴백 목록을 순서대로 걸어요. 스스로 거부하는 폴백 모델은 요청을 다음 항목으로 넘겨요.
  • 목록의 모든 모델이 거부하면 미들웨어는 오류를 일으키기보다 최종 거부(마지막 모델의 거부 응답)를 반환해요.
  • Claude Fable 5.1, Claude Opus 5.5, Claude Fable 5의 thinking 블록은 그대로 통과해요. 각 재시도는 원래 요청 본문을 다시 보내고, 미들웨어가 이후 요청의 대화 기록에서 제거하는 유일한 블록은 스스로 추가한 fallback 경계 블록이에요. 폴백 모델은 Claude Fable 5.1 블록을 읽을 수 없는데, 블록이 그 모델 또는 더 새로운 모델에만 보존되기 때문에 API가 버려요. API는 Claude Fable 5.1과 Claude Mythos 5.1을 제외한 모든 폴백 모델에 대해 Claude Opus 5.5 블록도 버려요(대화 중간에 모델 전환하기 참조).
  • 미들웨어를 통해 서비스된 응답은 서버 측 폴백 응답과 마찬가지로 각 모델 경계에 fallback 콘텐츠 블록을 포함해요. 미들웨어는 이후 요청에서 그 블록들을 관리해요.
  • 받아들인 모델은 BetaFallbackState에 기록되므로, 상태를 공유하는 후속 요청은 거부한 모델을 다시 묻지 않고 그것에 고정돼요.
미들웨어와 서버 측 `fallbacks` 파라미터는 같은 일을 해요. 둘 중 하나만 구성하고, 같은 요청에 둘 다 절대 구성하지 마세요. 미들웨어를 설치한 애플리케이션에서 서버 측 `fallbacks` 요청을 보내려면 그것 없는 별도 클라이언트 인스턴스를 사용하세요.

재시도를 직접 작성하기 (Writing the retry yourself)

원시 HTTP 또는 커스텀 재시도 로직에서는 미들웨어가 감싸는 패턴을 구현하세요:

응답에서 `stop_reason: "refusal"`을 확인하세요. `model`을 폴백 모델(예: Claude Opus 4.8)로 설정한 같은 요청을 보내세요. 다른 모델이 보통 Claude Fable 5.1이나 Claude Fable 5가 거부한 요청을 서비스할 수 있어요. 대화 기록을 어떻게 처리하는지는 [폴백 크레딧](https://platform.claude.com/docs/en/build-with-claude/fallback-credit)을 상환하는지에 달려 있어요:
* **크레딧을 상환하지 않음:** 이전 `thinking`과 `redacted_thinking` 블록을 제자리에 두거나 입력 토큰을 아끼려고 벗겨낼 수 있어요. 폴백 모델은 어느 쪽이든 보통 그것을 쓸 수 없어요. Claude Fable 5 블록을 무시하고, Claude Fable 5.1 블록은 [그 모델 또는 더 새로운 모델에만 보존](https://platform.claude.com/docs/en/build-with-claude/thinking#preserved-for-model)되므로 API가 버려요. API는 Claude Fable 5.1과 Claude Mythos 5.1을 제외한 모든 폴백 모델에 대해 Claude Opus 5.5 블록도 버려요([대화 중간에 모델 전환하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#switching-models) 참조).
* **크레딧을 상환:** 본문을 그대로 보내세요. 상환은 정확한 일치를 요구하기 때문이에요. 서버는 상환 시 이전 모델의 thinking 블록을 처리하므로, 그것을 벗겨내지 마세요([거부된 요청과 일치해야 하는 필드](https://platform.claude.com/docs/en/build-with-claude/fallback-credit#reference) 참조).
멀티턴 대화에서는 이후 턴에도 폴백 모델을 계속 사용하고 다시 바꾸지 마세요.

수동 재시도는 폴백 모델의 프롬프트 캐시를 처음부터 써서, 기존 캐시를 읽는 것보다 비용이 더 들어요. 폴백 크레딧이 그 비용을 환불해요. 직접 만드는 모든 재시도에서 상환하세요.

Message Batches에서의 거부 (Refusals in Message Batches)

Message Batch의 거부된 요청은 stop_reason: "refusal"을 가진 result.type: "succeeded"로 돌아와요. 배치 결과는 동기 응답과 같은 stop_details 객체를 지니므로, stop_reason 또는 stop_details.type 중 하나로 거부를 감지할 수 있어요. 한 가지 차이: 배치 거부는 폴백 크레딧을 발행하지 않으므로, 배치 결과의 stop_details에는 fallback_credit_token이 절대 포함되지 않아요.

서버 측 폴백은 배치에서 사용할 수 없어요(fallbacks를 포함한 배치 요청은 항목별 에러 결과를 만들어요). 거부된 배치 항목을 재시도하려면:

  1. 결과에서 거부된 항목을 수집하세요.
  2. 멀티턴 기록에서 Claude Fable 5.1 또는 Claude Fable 5 thinking 블록을 벗겨내세요.
  3. 새 배치나 직접 요청으로 폴백 모델에 다시 제출하세요.

흔한 함정 (Common pitfalls)

  • 다른 모델에서 재시도하세요. 거부된 요청을 같은 모델에 다시 보내는 것은 보통 또 다른 거부를 얻어요. 재시도를 폴백 모델로 향하게 하세요.
  • 턴이나 세션별이 아니라 요청별로 재시도를 예산하세요. 단일 턴이 여러 거부를 만들 수 있어요. 예를 들어 에이전트와 그 하위 에이전트들.
  • 모든 요청 경로에 폴백을 구성하세요. 재시도 핸들러, 오류 복구 분기, 백그라운드 워커 모두 필요해요. 폴백 없이 요청을 다시 발행하는 핸들러는 정확히 보호가 가장 필요한 요청에서 보호를 잃어요.
  • 하위 에이전트 호출에 자체 폴백을 주세요. fallbacks 파라미터는 도구 실행 내부에서 이뤄지는 모델 호출로 전파되지 않아요.
  • 폴백을 주변 상태가 아니라 요청의 속성으로 만드세요. 공유 플래그, 캐시된 설정 값, 전역 토글은 동기화에서 벗어나 조용히 요청을 보호되지 않은 채로 남길 수 있어요. 폴백이 활성인지 확인할 수 없을 때는 켜져 있다고 가정하지 말고 구성하세요.
  • 거부를 자기 신호로 계측하세요. 거부는 HTTP 200이므로, 오류율이나 5xx 응답에 구축된 모니터링은 그것을 절대 보지 못해요. 거부마다 이벤트 하나, 폴백 서비스 응답마다 이벤트 하나(usage.iterations의 fallback_message 항목이 후자를 표시)를 내보내고 둘 사이의 차이에 알림을 설정하세요.
  • content나 내부 stop_details 필드가 아니라 stop_reason 또는 stop_details.type으로 분기하세요. stop_details 객체는 거부에서 항상 존재하지만, 그 category와 explanation 필드는 null일 수 있어요. stop_reason이 "refusal"과 같은지 직접 확인하세요.

다음 단계 (Next steps)

재시도를 직접 만들 때 프롬프트 캐시 비용을 두 번 내지 않는 방법. 모든 `stop_reason` 값과 처리 방법. SDK 미들웨어가 어떻게 동작하는지. 거부 폴백 헬퍼 포함. 기존 애플리케이션을 Claude Fable 5.1로 옮기기.

더 알아보기 (Learn more)