폴백 크레딧

폴백 크레딧 (Fallback credit)

거부된 요청을 다른 모델에서 재시도할 때 프롬프트 캐시 비용을 두 번 내지 않게 해 주는 기능이에요. 프롬프트 캐시는 모델별이라서, 모델 A가 요청을 거절하고 모델 B에서 재시도하면 첫 모델에 이미 캐시된 대화 접두사를 새 모델의 캐시에 처음부터 다시 써야 해요. 캐시 쓰기는 캐시 읽기보다 비싸요. 폴백 크레딧이 그 추가 비용을 제거해 줘요. 서버 측 폴백이나 SDK 미들웨어를 쓰면 폴백 크레딧이 자동으로 적용되므로 이 페이지는 필요 없어요.

출처: 문서

본문

프롬프트 캐시는 모델별이에요. 모델이 요청을 거절하고 다른 모델에서 재시도하면, 첫 모델에 이미 캐시된 대화 접두사를 새 모델의 캐시에 처음부터 써야 해요. 캐시 쓰기는 캐시 읽기보다 비싸요. 폴백 크레딧이 그 추가 비용을 제거해요. 거절에는 크레딧 토큰이 붙고, 재시도에서 그 토큰을 반향하면 새 모델에서 처음부터 대화가 있었다는 것처럼 요금이 매겨져요.

이 페이지는 당신이 재시도를 직접 구축할 때만 필요해요. 원시 HTTP나 맞춤 재시도 로직으로 말이에요. 서버 측 폴백SDK 미들웨어는 폴백 크레딧을 자동으로 적용해요. 둘 중 하나를 쓰면 이 페이지는 건너뛰세요.

거부와 폴백은 거부를 감지하고 폴백 방식을 선택하는 것을 다뤄요. 프롬프트 캐싱은 그 용어가 새롭다면 캐시 읽기와 캐시 쓰기를 설명해요.

기본 흐름 (The basic flow)

거절될 수 있는 요청을 `anthropic-beta: fallback-credit-2026-07-01` 헤더와 함께 보내세요. `server-side-fallback-2026-07-01` 헤더도 같은 필드를 부여하고, 이전의 `fallback-credit-2026-06-01` 헤더는 계속 수용되며 같은 필드를 부여해요. 거절 시 `stop_details`에는 두 필드가 포함돼요:
* **`fallback_credit_token`:** 크레딧을 나타내는 불투명 문자열.
* **`fallback_has_prefill_claim`:** 어떤 재시도 본문 모양을 쓸지 알려주는 Boolean.

둘 다 거절에 대해 크레딧을 사용할 수 없으면 `null`이에요.
거절된 요청 본문에서 시작하세요. `model`을 폴백 모델로 설정하고 토큰을 최상위 `fallback_credit_token` 매개변수로 추가하세요. 다음 표에서 본문 모양을 고르세요. 같은 `fallback-credit-2026-07-01` 베타 헤더로 재시도를 보내세요. 재시도는 토큰을 청산(redemption)하려면 그 헤더가 필요해요.

fallback_has_prefill_claim 필드는 재시도가 거절된 모델의 부분 출력을 이어갈 수 있는지(처음부터 시작하는 대신) 알려줘요:

fallback_has_prefill_claim 재시도 본문
true 거절된 요청 본문, 그대로, 더하기 거절된 응답의 content를 반향하는 assistant 메시지 하나. 재시도 모델은 거절된 모델이 멈춘 곳에서 응답을 이어가고, 완료된 서버 도구 호출은 재실행되지 않아요.
false 거절된 요청 본문, 그대로.

예시 (Example)

다음 예시는 거절될 수 있는 요청을 만들고 Claude Opus 4.8에 대한 재시도에서 크레딧 토큰을 청산해요. 재시도 시도가 거부되면 예시는 거부 사다리(재시도가 거부될 때 다루는, 점점 더 단순해지는 재시도 모양들의 시퀀스)를 따라 내려가요. 이는 재시도가 거부될 때에서 다뤄요.

```bash cURL # Initial request (may be refused) response=$(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: fallback-credit-2026-07-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "Hello, Claude"}] }')

A refusal carries a one-time credit token in stop_details

token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}")

if [[ -n "${token}" ]]; then # Retry on the fallback model with the credit token (same body) response=$(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: 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 }')") fi

See the SDK examples for the full rejection-handling ladder.

jq -c '{stop_reason, model}' <<<"${response}"


```bash CLI
# Initial request (may be refused)
response=$(ant beta:messages create \
  --model claude-fable-5 \
  --max-tokens 1024 \
  --message '{"role":"user","content":"Hello, Claude"}' \
  --beta fallback-credit-2026-07-01 \
  --format json)

# A refusal carries a one-time credit token in stop_details
token=$(jq -r '.stop_details.fallback_credit_token // empty' <<<"${response}")

if [[ -n "${token}" ]]; then
  # Retry on the fallback model with the credit token
  response=$(ant beta:messages create \
    --model claude-opus-4-8 \
    --max-tokens 1024 \
    --message '{"role":"user","content":"Hello, Claude"}' \
    --fallback-credit-token "${token}" \
    --beta fallback-credit-2026-07-01 \
    --format json)
fi

# See the SDK examples for the full rejection-handling ladder.
jq -c '{stop_reason, model}' <<<"${response}"
client = Anthropic()

request = {
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Hello, Claude"}],
}


def send(model: str, body: dict[str, object]) -> BetaMessage:
    return client.beta.messages.create(
        model=model, betas=["fallback-credit-2026-07-01"], **body
    )


response = send("claude-fable-5", request)

if (
    response.stop_reason == "refusal"
    and (details := response.stop_details)
    and (token := details.fallback_credit_token)
):
    exact_body = request | {"fallback_credit_token": token}
    # Prefer the continuation shape unless the claim is False
    if details.fallback_has_prefill_claim is not False:
        echoed = [block.model_dump() for block in response.content]
        match echoed:
            case [*_, {"type": "text"} as final_block]:
                final_block["text"] = final_block["text"].rstrip()
        attempt = exact_body | {
            "messages": [
                *request["messages"],
                {"role": "assistant", "content": echoed},
            ]
        }
    else:
        attempt = exact_body

    try:
        response = send("claude-opus-4-8", attempt)
    except BadRequestError as error:
        if "redemption temporarily unavailable" in error.message:
            raise  # Transient: retry with the token within its five-minute window
        try:
            # Fall back to the unchanged body, still with the token
            response = send("claude-opus-4-8", exact_body)
        except BadRequestError as retry_error:
            if "redemption temporarily unavailable" in retry_error.message:
                raise  # Transient: retry with the token within its five-minute window
            # The token itself was rejected: forfeit it and retry without.
            response = send("claude-opus-4-8", request)

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

const request: Anthropic.Beta.MessageCreateParamsNonStreaming = {
  model: "claude-fable-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude" }],
  betas: ["fallback-credit-2026-07-01"]
};

let response = await client.beta.messages.create(request);

if (
  response.stop_reason === "refusal" &&
  response.stop_details?.type === "refusal" &&
  response.stop_details.fallback_credit_token
) {
  const { fallback_credit_token, fallback_has_prefill_claim } = response.stop_details;
  const fallbackModel = "claude-opus-4-8";

  const exactRetry: Anthropic.Beta.MessageCreateParamsNonStreaming = {
    ...request,
    model: fallbackModel,
    fallback_credit_token
  };

  // Richest shape first, degrading on each rejection: the continuation
  // shape (unless the claim is false), the unchanged body still carrying
  // the token, and finally forfeiting the token.
  let attempt = exactRetry;
  if (fallback_has_prefill_claim !== false) {
    const finalBlock = response.content.at(-1);
    const echoed: Anthropic.Beta.BetaContentBlockParam[] =
      finalBlock?.type === "text"
        ? [
            ...response.content.slice(0, -1),
            { ...finalBlock, text: finalBlock.text.trimEnd() }
          ]
        : response.content;
    attempt = {
      ...exactRetry,
      messages: [...request.messages, { role: "assistant", content: echoed }]
    };
  }

  try {
    response = await client.beta.messages.create(attempt);
  } catch (error) {
    // Degrade only on a shape-related 400. "redemption temporarily
    // unavailable" is transient: retry the same way within the token's
    // five-minute window instead.
    if (
      !(error instanceof Anthropic.BadRequestError) ||
      error.message.includes("redemption temporarily unavailable")
    ) {
      throw error;
    }
    try {
      response = await client.beta.messages.create(exactRetry);
    } catch (retryError) {
      if (
        !(retryError instanceof Anthropic.BadRequestError) ||
        retryError.message.includes("redemption temporarily unavailable")
      ) {
        throw retryError;
      }
      response = await client.beta.messages.create({ ...request, model: fallbackModel });
    }
  }
}

const { stop_reason, model } = response;
console.log(JSON.stringify({ stop_reason, model }));
var client = new AnthropicClient();
const string beta = "fallback-credit-2026-07-01";

List<BetaMessageParam> requestMessages =
[
    new() { Role = Role.User, Content = "Hello, Claude" },
];
MessageCreateParams Request(string model) => new()
{
    Model = model,
    MaxTokens = 1024,
    Messages = requestMessages,
    Betas = [beta],
};
var response = await client.Beta.Messages.Create(Request("claude-fable-5"));

if (
    response.StopReason == BetaStopReason.Refusal
    && response.StopDetails is { FallbackCreditToken: string token } details
)
{
    var exactBody = Request("claude-opus-4-8") with { FallbackCreditToken = token };
    var attempt = exactBody;
    // Prefer the continuation shape unless the claim is false
    if (details.FallbackHasPrefillClaim is not false)
    {
        var echoed = JsonArray.Create(response.RawData["content"])!;
        if (
            echoed is [.., JsonObject lastBlock]
            && lastBlock["type"]?.GetValue<string>() is "text"
            && lastBlock["text"]?.GetValue<string>() is string text
        )
        {
            lastBlock["text"] = text.TrimEnd();
        }
        attempt = exactBody with
        {
            Messages =
            [
                .. requestMessages,
                new()
                {
                    Role = Role.Assistant,
                    Content = new BetaMessageParamContent(
                        JsonSerializer.SerializeToElement(echoed)
                    ),
                },
            ],
        };
    }
    // A transient "redemption temporarily unavailable" rejection propagates out of
    // each of the following catch filters: retry with the token within its five-minute window.
    try
    {
        response = await client.Beta.Messages.Create(attempt);
    }
    catch (AnthropicBadRequestException e)
        when (!e.Message.Contains("redemption temporarily unavailable"))
    {
        try
        {
            // Fall back to the unchanged body, still with the token
            response = await client.Beta.Messages.Create(exactBody);
        }
        catch (AnthropicBadRequestException retryError)
            when (!retryError.Message.Contains("redemption temporarily unavailable"))
        {
            // The token itself was rejected: forfeit it and retry without.
            response = await client.Beta.Messages.Create(Request("claude-opus-4-8"));
        }
    }
}

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

request := anthropic.BetaMessageNewParams{
	MaxTokens: 1024,
	Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaFallbackCredit2026_07_01},
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Hello, Claude")),
	},
}

send := func(model anthropic.Model, body anthropic.BetaMessageNewParams) (*anthropic.BetaMessage, error) {
	body.Model = model
	return client.Beta.Messages.New(ctx, body)
}
// A non-transient 400 means this attempt shape or token was rejected and
// the next rung of the ladder should run. "redemption temporarily
// unavailable" is transient: surface it and retry with the token within
// its five-minute window.
canFallBack := func(err error) bool {
	apiErr, ok := errors.AsType[*anthropic.Error](err)
	return ok && apiErr.StatusCode == 400 &&
		!strings.Contains(apiErr.Error(), "redemption temporarily unavailable")
}

response, err := send(anthropic.ModelClaudeFable5, request)
if err != nil {
	log.Fatal(err)
}

if response.StopReason == anthropic.BetaStopReasonRefusal {
	details := response.StopDetails
	if token := details.FallbackCreditToken; token != "" {
		exactBody := request
		exactBody.FallbackCreditToken = anthropic.BetaMessageNewParamsFallbackCreditTokenUnion{
			OfString: anthropic.String(token),
		}
		attempt := exactBody
		// Prefer the continuation shape unless the claim is false
		if details.FallbackHasPrefillClaim || !details.JSON.FallbackHasPrefillClaim.Valid() {
			echoed := response.ToParam()
			if len(echoed.Content) > 0 {
				if text := echoed.Content[len(echoed.Content)-1].OfText; text != nil {
					text.Text = strings.TrimRightFunc(text.Text, unicode.IsSpace)
				}
			}
			attempt.Messages = append(slices.Clone(request.Messages), echoed)
		}
		response, err = send(anthropic.ModelClaudeOpus4_8, attempt)
		if err != nil && canFallBack(err) {
			// Fall back to the unchanged body, still with the token
			response, err = send(anthropic.ModelClaudeOpus4_8, exactBody)
			if err != nil && canFallBack(err) {
				// The token itself was rejected: forfeit it and retry without.
				response, err = send(anthropic.ModelClaudeOpus4_8, request)
			}
		}
		if err != nil {
			log.Fatal(err)
		}
	}
}

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

MessageCreateParams.Builder request() {
    return MessageCreateParams.builder()
        .maxTokens(1024L)
        .addUserMessage("Hello, Claude")
        .addBeta(AnthropicBeta.FALLBACK_CREDIT_2026_07_01);
}

BetaMessage send(Model model, MessageCreateParams.Builder body) {
    return client.beta().messages().create(body.model(model).build());
}

void main() {
    BetaMessage response = send(Model.CLAUDE_FABLE_5, request());

    if (response.stopReason().map(BetaStopReason.REFUSAL::equals).orElse(false)
            && response.stopDetails().orElse(null) instanceof BetaRefusalStopDetails details
            && details.fallbackCreditToken().orElse(null) instanceof String creditToken) {
        MessageCreateParams.Builder attempt = request().fallbackCreditToken(creditToken);
        // Prefer the continuation shape unless the claim is false
        if (details.fallbackHasPrefillClaim().orElse(true)) {
            List<BetaContentBlockParam> echoed = new ArrayList<>(
                response.content().stream().map(BetaContentBlock::toParam).toList());
            if (!echoed.isEmpty() && echoed.getLast().isText()) {
                var lastText = echoed.removeLast().asText();
                echoed.addLast(BetaContentBlockParam.ofText(
                    lastText.toBuilder().text(lastText.text().stripTrailing()).build()));
            }
            attempt.addAssistantMessageOfBetaContentBlockParams(echoed);
        }
        try {
            response = send(Model.CLAUDE_OPUS_4_8, attempt);
        } catch (BadRequestException badRequest) {
            // Transient: retry with the token within its five-minute window
            if (badRequest.getMessage().contains("redemption temporarily unavailable")) {
                throw badRequest;
            }
            try {
                // Fall back to the unchanged body, still with the token
                response = send(Model.CLAUDE_OPUS_4_8, request().fallbackCreditToken(creditToken));
            } catch (BadRequestException retryBadRequest) {
                if (retryBadRequest.getMessage().contains("redemption temporarily unavailable")) {
                    throw retryBadRequest;
                }
                // The token itself was rejected: forfeit it and retry without.
                response = send(Model.CLAUDE_OPUS_4_8, request());
            }
        }
    }

    IO.println("""
        {"stop_reason": "%s", "model": "%s"}"""
        .formatted(response.stopReason().orElseThrow(), response.model()));
}
$client = new Client();
$beta = 'fallback-credit-2026-07-01';
$messages = [['role' => 'user', 'content' => 'Hello, Claude']];

$send = fn (string $model, array $messages, ?string $token = null) => $client->beta->messages->create(
    maxTokens: 1024,
    messages: $messages,
    model: $model,
    fallbackCreditToken: $token,
    betas: [$beta],
);
$response = $send('claude-fable-5', $messages);

$token = $response->stopReason === 'refusal'
    ? $response->stopDetails?->fallbackCreditToken
    : null;

if ($token !== null) {
    $attemptMessages = $messages;
    // Prefer the continuation shape unless the claim is false
    if ($response->stopDetails->fallbackHasPrefillClaim !== false) {
        $echoed = $response->content
            |> json_encode(...)
            |> (fn (string $json): array => json_decode($json, associative: true));
        $lastIndex = array_key_last($echoed);
        if ($lastIndex !== null && $echoed[$lastIndex]['type'] === 'text') {
            $echoed[$lastIndex]['text'] = rtrim($echoed[$lastIndex]['text']);
        }
        $attemptMessages[] = ['role' => 'assistant', 'content' => $echoed];
    }
    // Transient: retry with the token within its five-minute window
    $isTransientRedemption = fn (BadRequestException $error): bool =>
        str_contains($error->getMessage(), 'redemption temporarily unavailable');
    try {
        $response = $send('claude-opus-4-8', $attemptMessages, $token);
    } catch (BadRequestException $error) {
        if ($isTransientRedemption($error)) {
            throw $error;
        }
        try {
            // Fall back to the unchanged body, still with the token
            $response = $send('claude-opus-4-8', $messages, $token);
        } catch (BadRequestException $retryError) {
            if ($isTransientRedemption($retryError)) {
                throw $retryError;
            }
            // The token itself was rejected: forfeit it and retry without.
            $response = $send('claude-opus-4-8', $messages);
        }
    }
}

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

request = {
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}]
}

send_message = ->(model, body) do
  client.beta.messages.create(model:, betas: ["fallback-credit-2026-07-01"], **body)
end

response = send_message.call("claude-fable-5", request)

if response in {stop_reason: :refusal,
                stop_details: {fallback_credit_token: String => credit_token} => details}
  exact_body = request.merge(fallback_credit_token: credit_token)

  # Prefer the continuation shape unless the claim is false
  attempt = if details.fallback_has_prefill_claim != false
    echoed = response.content.map(&:to_h)
    if echoed.last in {type: :text, text: String => final_text}
      echoed[-1] = echoed.last.merge(text: final_text.rstrip)
    end
    exact_body.merge(
      messages: [*request[:messages], {role: "assistant", content: echoed}]
    )
  else
    exact_body
  end

  begin
    response = send_message.call("claude-opus-4-8", attempt)
  rescue Anthropic::Errors::BadRequestError => error
    # Transient: retry with the token within its five-minute window
    raise if error.message.include?("redemption temporarily unavailable")
    begin
      # Fall back to the unchanged body, still with the token
      response = send_message.call("claude-opus-4-8", exact_body)
    rescue Anthropic::Errors::BadRequestError => error
      # Transient: retry with the token within its five-minute window
      raise if error.message.include?("redemption temporarily unavailable")
      # The token itself was rejected: forfeit it and retry without.
      response = send_message.call("claude-opus-4-8", request)
    end
  end
end

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

어디서 동작하는지 (Where it works)

폴백 크레딧은 Claude API, Amazon Bedrock, Claude Platform on AWS, Google Cloud, Microsoft Foundry에서 베타 상태예요. Message Batches의 거절은 크레딧 토큰을 만들지 않고, 청산은 직접 Messages API 요청에만 적용돼요. 배치 요청에 전달된 토큰은 수용되지만 무시돼요.

재시도 모델은 거절된 모델의 허용된 폴백 대상 중 하나여야 해요. Claude Fable 5.1과 Claude Fable 5의 경우 그것은 Claude Opus 4.8(claude-opus-4-8)과 Claude Opus 5(claude-opus-5)예요.

Claude API와 Claude Platform on AWS에서 대상 목록은 `server-side-fallback-2026-07-01` 베타 헤더가 설정되었을 때 [Models API](https://platform.claude.com/docs/en/api/models/list)의 각 모델 항목에 `allowed_fallback_models`로 게시돼요. 그 목록은 아직 `fallback-credit-*` 헤더만으로는 보이지 않아요. Amazon Bedrock, Google Cloud, Microsoft Foundry에서는 노출되지 않아요.

크레딧이 적용됐는지 확인하기

환불은 재시도의 usage에서 보여요. 같은 요청이 토큰 없이 보고했을 것과 비교했을 때, cache_creation_input_tokens는 더 낮고 cache_read_input_tokens는 같은 양만큼 더 높아요. 이동이 0이라는 것은 토큰이 인정되었지만 재가격할 것이 없었다는 뜻이에요. 예를 들어 재시도 모델의 캐시가 이미 따뜻했기 때문이에요.

재시도가 거부될 때 (When a retry is rejected)

대부분 재시도는 첫 시도에 청산돼요. 그렇지 않으면 API가 다음에 무엇을 시도할지 알려주는 400 오류를 반환해요.

assistant 메시지를 추가하는 재시도가 400 오류로 거부되면, 거절된 요청 본문을 그대로, 여전히 토큰과 함께 재전송하세요. 변경되지 않은 본문도 메시지가 `fallback_credit_token`을 지명하는 400 오류로 거부되면, 토큰 없이 재시도하세요. 크레딧은 상실되지만 재시도 자체는 성공해요.

참고: 거절된 요청이 서버 도구를 실행했다면, 토큰 없는 재시도는 그 도구들을 재실행하고 재청구해요. 그 경우 토큰 없는 재시도로 내려가는 대신 400 오류를 호출자에게 표면화하세요.

이 거부는 일시적이에요. 재시도 모양에 대한 판결이 아니에요. 같은 요청을, 같은 토큰으로, 토큰의 5분 창 안에서 재시도하세요. 사다리의 다음 단계로 이동하지 마세요.

참조 (Reference)

다음 섹션은 엣지 케이스와 완전한 청산 규칙을 다뤄요. 대부분 통합에는 필요하지 않아요.

청산은 재시도를 거절된 요청과 비교해요. 프롬프트를 형성하는 모든 필드는 정확히 일치해야 해요. 프롬프트를 형성하지 않는 필드는 재시도에서 바뀔 수 있어요.
규칙 필드
정확히 일치해야 함 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

연속 모양(fallback_has_prefill_claim: true)은 messages 일치의 유일한 예외예요. messages 끝에 assistant 메시지를 정확히 하나 추가해요.

재시도에서 이전 턴의 thinking 또는 redacted_thinking 블록을 제거하지 마세요. 토큰 없는 일반 재시도가 보통 그런 블록을 제거하더라도요. 본문은 거절된 요청과 일치해야 하고, 서버가 그 블록들을 자체적으로 처리해요.

재시도에 거절된 요청과 같은 `anthropic-beta` 헤더를 보내세요. 두 요청 중 하나에 있지만 다른 하나에 없는 베타 헤더는, 본문이 동일하더라도 일치를 실패시킬 수 있어요. 결과 400 오류는 본문 차이와 같은 `request body ... does not match` 메시지를 지니므로, 헤더 차이를 본문 문제로 잘못 읽기 쉬워요. 특히 요청이 대상으로 하는 모델에 따라 베타 헤더를 추가하거나 빼지 마세요.

두 헤더 계열은 재시도를 위해 일치에서 제외돼요:

  • server-side-fallback-*: 재시도는 fallbacks 매개변수를 버려야 하고, 그 헤더도 함께 버려도 불일치를 일으키지 않아요.
  • fallback-credit-*: 두 요청 모두에 이 헤더를 유지하세요. 재시도는 토큰을 청산하려면 그게 필요해요.
Claude Fable 5.1, Claude Fable 5, Claude Opus 5.5, Claude Opus 5, Claude Opus 4.8 같은 1M 토큰 컨텍스트 창을 기본으로 포함하는 모델에서 `context-1m-2025-08-07` 베타 헤더는 효과가 없어요. 두 요청을 동일하게 유지하려면, 한쪽에 보내고 다른 쪽에 안 보내는 대신 양쪽 모두에서 그 헤더를 생략하세요.
이 필드는 토큰도 `null`일 때만 `null`이므로, 토큰을 보유하면서 관찰하는 값은 결코 `null`이 아니에요. Amazon Bedrock, Google Cloud, Microsoft Foundry에서 이 필드에 대한 지원이 배포되는 동안에는 그 값이 없을 수 있어요(타입 SDK에서 `None`). 그 경우 재시도 모양을 `false`로 취급하지 말고 알 수 없는 것으로 취급하세요. 추가된 assistant 메시지 모양을 먼저 시도하고, 변경되지 않은 본문으로 폴백하는 [재시도가 거부될 때](https://platform.claude.com/docs/en/build-with-claude/fallback-credit#when-a-retry-is-rejected)의 거부 처리를 신뢰하세요. 거절의 토큰이 연속 모양을 지원하면 응답 `content`는 모델 자신의 출력만 지녀요. 거절 설명은 `stop_details.explanation`으로 전달돼요. 따라서 `content`를 추가된 assistant 메시지에 그대로 반향할 수 있어요.

보내기 전에 두 가지 조정이 여전히 필요할 수 있어요:

  • 보내는 마지막 블록이 text 블록이면 그 후행 공백을 제거하세요.
  • 일치하는 tool_result가 없는 클라이언트 측 tool_use 블록은 생략하세요.

반향된 콘텐츠가 이전 서버 측 폴백fallback 블록을 포함하면, 그 블록을 정확히 나타난 위치에 유지하세요. 베타 헤더 없는 어떤 요청에서든 수용돼요. API는 그 위치로 그 주변의 thinking 블록을 검증하므로, 그 경계 양쪽의 thinking 블록을 반향하는 요청은 그 블록이 생략되거나 이동되면 거부돼요.

토큰은 Microsoft Foundry를 포함해 거절을 받은 조직과 워크스페이스에서만 청산돼요. 워크스페이스가 없는 Amazon Bedrock과 Google Cloud에서는 토큰이 플랫폼의 호출자 신원에 묶여요.

토큰은 거절 후 5분이 지나면 만료돼요. 그 후에는 그것 없이 재시도를 보내세요. 토큰은 또한 무상태예요. 서버는 그에 대해 아무것도 저장하지 않고, 검사하거나 폐기할 엔드포인트도 없어요.

거절이 요청 내에서 서버 도구가 이미 실행된 후 도착했으면, 토큰은 부분 응답을 계속함으로써만 청산돼요. 그 제한이 완료된 도구 호출이 다시 실행되고 청구되는 것을 막아요.

따라서 다음 둘이 모두 참일 때 어느 모양으로도 토큰을 청산할 수 없는 조합이 생길 수 있어요:

  • 요청이 output_config.format이나 도구 사용을 강제하는 tool_choice를 사용했음. 둘 중 하나가 추가된 assistant 메시지 모양을 배제해요.
  • 거절이 서버 도구가 실행된 후 도착했음. 그것이 변경되지 않은 본문을 배제해요.

변경되지 않은 본문 재시도가 토큰을 부분 응답을 계속함으로써 청산해야 한다고 말하는 400 오류로 거부되면 토큰을 버리세요. 그것 없이 재시도는 성공하지만 완료된 서버 도구를 재실행하고 재청구해요. 조용히 재시도하는 대신 비용이나 오류를 호출자에게 표면화하세요.

다음 단계 (Next steps)

Detect refusals and choose between server-side fallback, the SDK middleware, and a manual retry. How cache reads and cache writes are billed. Every `stop_reason` value and how to handle it. The SDK helper that applies fallback credit automatically.

더 알아보기 (Learn more)