보존된 사고

보존된 사고 (Preserved thinking)

보존된 사고(preserved thinking)는 최신 Claude 모델들이 지니는 속성으로, 증류(loss-of-distillation)를 막기 위한 것이에요. 이전 턴에서 다시 보내는 thinking 블록을 모델이 사용할 수 있는지를 판단해요. 새 모델에서 thinking이나 redacted_thinking 블록이 요청에 다시 들어오면, API는 블록의 signature가 두 가지 조건을 충족하는지 확인해요: (1) 현재 모델이 그 블록을 읽을 수 있는지, (2) 블록 앞의 내용이 바뀌지 않았는지. 이 문서에서는 이 규칙을 이해하고, 통합(integration) 코드가 접두사(prefix)를 편집하지 않도록 유지하는 방법을 차근차근 알려드릴게요.

출처: 문서

본문

보존된 사고는 증류를 막아 주는 최신 Claude 모델의 속성이에요. 이전 턴에서 보냈던 thinking 블록을 모델이 다시 사용할 수 있는지 여부를 결정하죠. Claude Fable 5.1부터, thinking 또는 redacted_thinking 블록이 요청에 다시 들어오면 API는 블록의 signature에 대해 두 가지를 확인해요:

  • 모델이 블록을 읽을 수 있는가. 각 모델은 자기 자신의 thinking 블록과 정해진 일부 다른 모델의 블록을 읽어요. Claude Fable 5.1은 Claude Opus 5의 블록과, Claude API에서는 Claude Opus 5.5의 블록을 읽어요. 반대로 Claude Opus 5나 Claude Opus 5.5는 Claude Fable 5.1의 블록을 읽지 못해요. 현재 모델이 블록을 읽을 수 없다면, API는 오류 없이 그 블록을 해당 요청에서 제외해요. 대화 중간에 모델 전환하기를 참고하세요.
  • thinking 블록 앞쪽의 내용이 바뀌지 않았는가. 최상위 system 프롬프트, tools, 블록 이전의 messages가 그 블록의 접두사(prefix)예요. 이 접두사가 블록이 생성될 때 보낸 값과 다르다면, 그 블록과 그 이후의 모든 thinking 블록이 무효가 되고, API는 요청을 400 오류로 거부하거나 무효 블록을 삭제해요. 이 둘 중 어느 쪽을 선택할지는 사용자가 정해요. 접두사 유지하기를 참고하세요.

모델 확인은 모든 계정에 적용돼요. 접두사 확인은 2026년 8월 31일 00:00 UTC 이후에 만든 계정에서는 기본으로 적용돼요. 이전 계정에서는 thinking.block_binding.prefix_mismatch_behavior를 설정한 요청에만 접두사 확인을 적용해요. 계정 생성 시점과 무관하게 통합 코드를 항상 append-only로 만들어 주세요. 그러면 새 계정을 포함해 모든 계정에서 같은 코드가 동작해요.

누가 코드를 바꿔야 하나요 (Who needs to change anything)

Claude Code, claude.ai, Claude Managed Agents, 또는 Claude Agent SDK가 요청을 만든다면, 또는 코드가 세션 동안 systemtools를 고정하고 messages에만 계속 추가한다면 바꿀 것이 없어요. Claude Mythos 5.1과 Claude Fable 5.1 이전 모델들은 접두사 확인을 실행하지 않아요. thinking 블록을 다시 보내는 일이 없다면 접두사 확인이 거부할 것이 없고, 모델은 이전 추론을 하나도 받지 못해요.

한 대화 안에서 두 요청 사이에 아래 중 하나를 한다면 통합 코드를 확인해 보세요. 각 항목은 대신 취할 방법으로 연결돼요:

이전 계정에서는 요청이 prefix_mismatch_behavior를 설정하지 않는 한 이러한 경우 어떤 오류도 발생하지 않아요. 그러니 내 키로 실행했을 때 오류가 없더라도 코드가 영향받는지 알 수 없어요. 사용자가 자신의 API 키로 도구를 실행한다면, 새 계정 사용자는 사용자보다 먼저 400 오류를 만나게 돼요. 요청 동작을 바꾸지 않고 그들이 보는 것을 확인하려면 thinking-binding-controls-2026-08-01 베타 헤더를 보내세요. 이전 계정에서 각 응답은 확인에 실패한 블록을 표시하고, 모델은 그 블록을 여전히 읽어요 (불일치 동작 설정과 input_transformations 읽기 참조).

대화 중간에 모델 전환하기 (Switching models mid-conversation)

Claude Fable 5.1과 Claude Mythos 5.1은 서로가 만든 thinking 블록과 그 이전 Claude 모델들의 블록을 읽어요. 어떤 이전 모델도 Claude Fable 5.1이나 Claude Mythos 5.1의 thinking 블록은 읽지 못해요.

Claude Opus 5.5는 Claude Opus 5와 그 이전 Opus·Sonnet·Haiku 모델들의 thinking 블록은 읽지만, Claude Fable이나 Claude Mythos 모델의 블록은 읽지 못해요. Claude API에서 Claude Fable 5.1과 Claude Mythos 5.1은 Claude Opus 5.5의 thinking 블록을 읽어요. 그 외 어떤 모델도 읽지 못해요. 그래서 Claude Opus 5에서 Claude Opus 5.5로 옮기는 대화는 추론을 유지하고, Claude API에서 Claude Opus 5.5에서 Claude Fable 5.1이나 Claude Mythos 5.1로 올라가는 대화도 추론을 유지해요. Claude Fable 5.1이나 Claude Mythos 5.1에서 Claude Opus 5.5로, 또는 Claude Opus 5.5에서 그 둘 외의 모델로 옮기는 대화는 전환 이후의 턴을 이전 모델의 추론 없이 실행해요. 이런 블록은 아래에서 설명하듯 거부되는 게 아니라 삭제돼요.

  • 이전 모델에서 Claude Fable 5.1로, 또는 Claude API에서 Claude Opus 5.5에서 옮겨오는 대화는 추론을 유지해요. 이전 모델의 thinking 블록은 읽을 수 있는 상태로 남으므로, 전환 후 첫 턴부터 모델은 평소처럼 생각해요.
  • 이전 모델로 내려가는 대화는 해당 요청에서 Claude Fable 5.1의 추론을 잃어요. 라우터가 더 저렴한 모델로 턴을 보낼 때, 분류기 거부 폴백 후에, 또는 서버 측 폴백 중에 이런 일이 생겨요. API는 프롬프트가 모델에 도달하기 전에 읽을 수 없는 블록을 제거해요. 이 블록들은 과금되지 않고 input_tokens에도 포함되지 않아요.

모든 요청에 전체 기록을 계속 보내고, thinking 블록도 포함해서 보내되, 현재 모델이 읽을 수 없는 것은 API가 버리도록 두세요. API는 messages 배열을 절대 편집하지 않으므로, 삭제된 블록은 기록에 그대로 남아요. 같은 기록이 Claude Fable 5.1로 돌아가면, 그 블록과 이전 모델의 thinking이 다시 읽힐 수 있어요. 추론이 완전히 사라지는 경우는 클라이언트가 블록을 직접 제거할 때뿐이에요. 예를 들어 모델 전환 시 thinking을 벗겨내거나 각 모델이 사용한 내용으로 기록을 다시 만드는 하네스가 그런 경우죠.

애니메이션: Claude Opus로 전환하면 그 턴에서는 Claude Fable 5.1의 thinking을 건너뛰고, 다시 돌아오면 모든 것이 다시 읽힘

thinking-binding-controls-2026-08-01 베타 헤더를 사용하면 응답이 최상위 input_transformations 배열에 삭제된 각 블록을 reason: "model_binding_mismatch"와 함께 나열해요:

{
  "input_transformations": [
    {
      "type": "thinking_dropped",
      "path": "messages.3.content.0",
      "reason": "model_binding_mismatch"
    }
  ]
}

헤더가 없으면 삭제는 조용히 일어나요. 이 항목은 통합 코드의 버그가 아니며, prefix_mismatch_behavior는 여기에 영향을 주지 않아요. 현재 모델이 읽을 수 없는 블록은 항상 삭제돼요.

접두사 유지하기 (Keeping the prefix unchanged)

Claude Fable 5.1과 Claude Opus 5.5에서 thinking 블록은 나중 요청에서 그 앞에 보낸 모든 것이 바뀌지 않는 동안에만 유효하게 유지돼요. 확인되는 접두사는 세 부분으로 이뤄져요:

  • 최상위 system 프롬프트
  • tools 집합
  • 블록 이전의 모든 message

참고: 서버 측 컴팩션을 쓰면 확인되는 접두사는 가장 최근 컴팩션 블록부터 시작돼요.

effort, max_tokens, output_config, tool_choice, metadata 같은 세 필드 밖의 요청 파라미터는 접두사 확인 대상이 아니고, cache_control 마커도 아니에요. 무엇이 편집으로 간주되는가에 전체 목록이 있어요.

이전 thinking 블록은 접두사에 없지만, 각 thinking 블록은 턴을 넘어 자기 이전에 온 thinking 블록을 기록해요. 기록 앞쪽(가장 오래된 것부터), 뒤쪽, 또는 전부에서 thinking 블록을 제거할 수 있어요. 문제가 되는 것은 틈(gap)이에요. 유지하는 thinking 블록은 원래 순서의 끊기지 않은 연속이어야 해요. 그래서 중간에서 하나를 제거하면 그 이후의 thinking 블록이 무효가 되죠. 블록을 하나 제거하면 계속 빼 둬야 해요. 다시 넣으면 그동안 만들어진 thinking 블록이 무효가 돼요.

systemtools를 세션 내내 고정하고 messages는 append-only로 취급하세요. 같은 원칙이 프롬프트 캐싱에서도 접두사를 안정적으로 유지해요. thinking을 무효로 만드는 편집은 캐시를 다시 시작하게 만드는 편집이에요.

API가 무효 블록을 처리하는 방식 (What the API does with an invalid block)

thinking.block_binding.prefix_mismatch_behavior로 방식을 고를 수 있어요:

  • "error"(기본값): API는 첫 번째 실패 블록의 이름을 밝히는 400 invalid_request_error로 요청을 거부해요.
  • "drop_block": API는 실패하는 각 블록과 그 뒤의 모든 thinking 블록을 버리고 요청은 성공해요. 삭제된 블록은 과금되지 않아요. 모델은 삭제된 블록의 추론을 쓰지 않고 그 턴에 답하며, 프롬프트 캐시는 편집 지점에서 다시 시작돼요. 응답은 삭제된 각 블록을 input_transformations에(message_start 이벤트로 스트리밍할 때) reason: "prefix_binding_mismatch"로 나열해요.

"drop_block"은 요청이 계속 성공하게 하지만 편집을 고치진 않아요. 각 세션에서 input_transformationsprefix_binding_mismatch 항목이 있는 응답 수를 세고 알림을 설정하세요. Message Batches API에서 필드를 설정하지 않은 항목은 실패하지 않아요. API가 기본으로 확인을 적용하는 곳에서는 실패 블록을 대신 삭제해요. 배치 항목을 실패시키고 싶다면 그곳에서 명시적으로 "error"를 설정하세요.

이 필드와 input_transformations 배열 모두 thinking-binding-controls-2026-08-01 베타 헤더가 필요해요. 불일치 동작 설정과 input_transformations 읽기에서 각 SDK의 요청을 보여줘요.

400 메시지는 이렇게 시작돼요:

messages.1.content.0: Invalid `signature` in `thinking` block. The block is bound to a different conversation. Remove the block, or set `thinking.block_binding.prefix_mismatch_behavior` to "drop_block".

요청이 베타 헤더를 보내지 않았다면 메시지는 이어집니다:

That setting requires the `thinking-binding-controls-2026-08-01` value in the `anthropic-beta` header.

보통은 무엇이 바뀌었는지 이름을 밝히는 문장으로 끝나요. 예를 들어 system 프롬프트나 tools 목록이 블록 생성 시점과 다르다고 알려주죠. Thinking 문제 해결에서 그 문장이 무엇을 가리킬 수 있는지 설명해요.

변조되거나 해독할 수 없는 서명은 다른 종류의 실패예요. 항상 400(Invalidsignature inthinking block, 대화에 관한 문장 없음)을 반환하고, prefix_mismatch_behavior는 여기에 적용되지 않아요.

코드에서 오류 처리하기 (Handle the error in code)

이것은 앞서 이 섹션에서 보여드린 400 invalid_request_error예요. 같은 본문을 다시 보내지 마세요. 매번 같은 방식으로 실패해요. 베타 헤더와 prefix_mismatch_behavior: "drop_block"으로 한 번 재시도하고, 그 선택을 세션과 함께 저장해서 이후 모든 요청(재시작 후 포함)도 함께 보내도록 하세요. 베타 헤더를 보낼 수 없다면 기록에서 모든 thinkingredacted_thinking 블록을 한 번 제거하고, 그대로 두고 계속 진행해요. 그리고 불일치를 일으킨 편집을 고치세요.

불일치 동작 설정과 input_transformations 읽기 (Set the mismatch behavior and read input_transformations)

thinking-binding-controls-2026-08-01 베타 헤더는 다음을 추가해요:

  • 모든 응답의 최상위 input_transformations 배열
  • thinking 설정의 block_binding 객체. 유일한 필드는 prefix_mismatch_behavior예요

block_bindingthinking.type: "adaptive"thinking.type: "enabled"와 함께 받아들여져요. 베타 헤더 없이 보내면 400 오류가 나는데 메시지가 block_binding: Extra inputs are not permitted로 끝나요. 접두사 확인을 실행하지 않는 모델은 이 객체를 받아들이고 모델 확인 삭제만 보고하므로, 하나의 요청 본문이 여러 모델에서 동작해요. API 참조에서는 접두사 확인을 대화 확인(conversation check)이라고 불러요.

다음 요청은 거부 대신 삭제를 선택한 것이에요. 첫 턴에는 재생할 것이 없으므로 input_transformations는 비어서 돌아와요:

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: thinking-binding-controls-2026-08-01" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5-1", "max_tokens": 16000, "thinking": { "type": "adaptive", "block_binding": { "prefix_mismatch_behavior": "drop_block" } }, "messages": [ { "role": "user", "content": "What is the greatest common divisor of 1071 and 462?" } ] }' ```
ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format yaml <<'YAML'
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
  block_binding:
    prefix_mismatch_behavior: drop_block
messages:
  - role: user
    content: What is the greatest common divisor of 1071 and 462?
YAML
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-fable-5-1",
    max_tokens=16000,
    thinking={
        "type": "adaptive",
        "block_binding": {"prefix_mismatch_behavior": "drop_block"},
    },
    messages=[
        {
            "role": "user",
            "content": "What is the greatest common divisor of 1071 and 462?",
        }
    ],
    betas=["thinking-binding-controls-2026-08-01"],
)

for block in response.content:
    if block.type == "text":
        print(block.text)

print(f"Input transformations: {len(response.input_transformations or [])}")
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-fable-5-1",
  max_tokens: 16000,
  thinking: {
    type: "adaptive",
    block_binding: { prefix_mismatch_behavior: "drop_block" }
  },
  messages: [
    { role: "user", content: "What is the greatest common divisor of 1071 and 462?" }
  ],
  betas: ["thinking-binding-controls-2026-08-01"]
});

for (const block of response.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
console.log(`Input transformations: ${response.input_transformations?.length ?? 0}`);
AnthropicClient client = new();

var response = await client.Beta.Messages.Create(
    new()
    {
        Model = "claude-fable-5-1",
        MaxTokens = 16000,
        Thinking = new BetaThinkingConfigAdaptive
        {
            BlockBinding = new()
            {
                PrefixMismatchBehavior = BetaThinkingPrefixMismatchBehavior.DropBlock,
            },
        },
        Messages =
        [
            new()
            {
                Role = Role.User,
                Content = "What is the greatest common divisor of 1071 and 462?",
            },
        ],
        Betas = [AnthropicBeta.ThinkingBindingControls2026_08_01],
    }
);

foreach (var block in response.Content)
{
    if (block.TryPickText(out var textBlock))
    {
        Console.WriteLine(textBlock.Text);
    }
}

Console.WriteLine($"Input transformations: {response.InputTransformations?.Count ?? 0}");
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
	Model:     "claude-fable-5-1",
	MaxTokens: 16000,
	Thinking: anthropic.BetaThinkingConfigParamUnion{
		OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{
			BlockBinding: anthropic.BetaThinkingBlockBindingParam{
				PrefixMismatchBehavior: anthropic.BetaThinkingPrefixMismatchBehaviorDropBlock,
			},
		},
	},
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What is the greatest common divisor of 1071 and 462?")),
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaThinkingBindingControls2026_08_01},
})
if err != nil {
	log.Fatal(err)
}

for _, block := range response.Content {
	if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
		fmt.Println(textBlock.Text)
	}
}
fmt.Printf("Input transformations: %d\n", len(response.InputTransformations))
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaThinkingBlockBinding;
import com.anthropic.models.beta.messages.BetaThinkingConfigAdaptive;
import com.anthropic.models.beta.messages.BetaThinkingPrefixMismatchBehavior;
import com.anthropic.models.beta.messages.MessageCreateParams;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    MessageCreateParams params = MessageCreateParams.builder()
        .model("claude-fable-5-1")
        .maxTokens(16000L)
        .addBeta(AnthropicBeta.THINKING_BINDING_CONTROLS_2026_08_01)
        .thinking(BetaThinkingConfigAdaptive.builder()
            .blockBinding(BetaThinkingBlockBinding.builder()
                .prefixMismatchBehavior(BetaThinkingPrefixMismatchBehavior.DROP_BLOCK)
                .build())
            .build())
        .addUserMessage("What is the greatest common divisor of 1071 and 462?")
        .build();

    BetaMessage response = client.beta().messages().create(params);

    response.content().stream()
        .flatMap(block -> block.text().stream())
        .forEach(textBlock -> IO.println(textBlock.text()));
    IO.println("Input transformations: "
        + response.inputTransformations().map(List::size).orElse(0));
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaThinkingBlockBinding;
use Anthropic\Beta\Messages\BetaThinkingConfigAdaptive;
use Anthropic\Beta\Messages\BetaThinkingPrefixMismatchBehavior;
use Anthropic\Client;

$client = new Client();

$response = $client->beta->messages->create(
    model: 'claude-fable-5-1',
    maxTokens: 16000,
    thinking: BetaThinkingConfigAdaptive::with(
        blockBinding: BetaThinkingBlockBinding::with(
            prefixMismatchBehavior: BetaThinkingPrefixMismatchBehavior::DROP_BLOCK,
        ),
    ),
    messages: [
        ['role' => 'user', 'content' => 'What is the greatest common divisor of 1071 and 462?'],
    ],
    betas: [AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01],
);

foreach ($response->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, PHP_EOL;
    }
}

echo 'Input transformations: ', count($response->inputTransformations ?? []), PHP_EOL;
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-fable-5-1",
  max_tokens: 16_000,
  thinking: {
    type: "adaptive",
    block_binding: {prefix_mismatch_behavior: "drop_block"}
  },
  messages: [
    {role: "user", content: "What is the greatest common divisor of 1071 and 462?"}
  ],
  betas: [Anthropic::AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01]
)

response.content.each do |block|
  puts block.text if block.type == :text
end

puts "Input transformations: #{response.input_transformations&.length || 0}"
The greatest common divisor of 1071 and 462 is 21.
Input transformations: 0

베타 헤더 아래에서 thinking 가능한 모델의 모든 응답은 input_transformations를 지녀요. 각 항목은 하나의 thinking 블록을 path(예: messages.1.content.0)로 이름 지어주고 reason을 제공해요. 두 가지 항목 유형이 있어요:

  • thinking_dropped: API는 모델이 읽기 전에 블록을 버려요. 블록은 과금되지 않아요. reasonprefix_binding_mismatch 또는 model_binding_mismatch예요(대화 중간에 모델 전환하기 참조).
  • thinking_mismatch_allowed: 블록은 접두사 확인에 실패하지만, 이 요청에는 API가 그 확인을 적용하지 않아서 블록은 그대로 모델에 도달하고 과금돼요. reason은 항상 prefix_binding_mismatch예요. 이 항목은 API가 기본으로 확인을 적용하지 않는 요청에서만 나타나요. 예를 들어 이전 계정에서 온 요청이 그렇죠(API가 확인을 적용하는 시점 참조). prefix_mismatch_behavior를 어떤 값으로든 설정하면 그 요청이 확인 적용을 선택하게 되므로, 그 필드를 설정한 요청은 이 항목을 절대 받지 않아요.

블록이 하나도 삭제되지 않고 접두사 확인에 실패한 것도 없으면 배열은 비어 있어요. 이후 확인이 값을 더 추가하므로 type이나 reason을 알 수 없는 항목은 무시하세요.

스트리밍할 때 배열은 message_start 이벤트의 message 객체에 도착해요. 스트림 중간에 서버 측 폴백이 끝난 뒤에는 마지막 message_delta 이벤트가 제공 모델의 항목과 함께 다시 실어 나르죠. 메시지 배치에서는 명시적 "error" 아래 접두사 확인에 실패한 블록이 있는 항목이 errored로 해결돼요. 필드를 설정하지 않은 항목은 실패하지 않아요. API가 기본으로 확인을 적용하는 곳에서는 실패 블록을 대신 삭제해요. 토큰 계산 엔드포인트는 같은 접두사 확인을 실행하고 같은 400을 반환해요.

API가 확인을 적용하는 시점 (When the API enforces the check)

API는 새 계정의 Claude Fable 5.1과 Claude Opus 5.5에 접두사 확인을 적용해요.

  • 2026년 8월 31일 00:00 UTC 이후 생성된 계정: API는 Claude Fable 5.1과 Claude Opus 5.5 요청을 확인하고, "drop_block"을 설정하지 않는 한 "error"를 적용해요. 새 계정의 정의는 Claude API와 클라우드 플랫폼에 동일하게 적용돼요.
  • 이전 계정: API는 prefix_mismatch_behavior를 설정한 요청에만 확인을 적용해요. 필드를 설정하면 요청이 적용을 선택하는 것이므로, 새 계정을 만들지 않고도 새 계정이 보는 것을 확인할 수 있어요. 필드를 설정하지 않은 요청에서 API는 여전히 확인을 실행하지만 실패 블록이 모델에 도달하게 둬요. 베타 헤더로 응답은 각각을 input_transformationsthinking_mismatch_allowed로 나열해요. 그래서 모델이 받는 것을 바꾸지 않고 접두사 편집을 찾을 수 있어요.

자기 계정이 어느 그룹인지 알아내려면, thinking 블록이 들어 있는 Claude Fable 5.1 대화를 가져와서 그 블록 앞의 무언가를 바꾼 뒤 베타 헤더와 block_binding 필드 없이 Claude Fable 5.1로 보내 보세요. 헤더 이름을 밝히는 400 응답이 돌아오면 계정이 기본으로 적용되는 것. 200 응답이면 그렇지 않은 것. 확인하려면 베타 헤더를 쓰되 여전히 block_binding 없이 같은 요청을 다시 보내 보세요. 응답은 편집 이후의 모든 thinking 블록을 input_transformationsthinking_mismatch_allowed로 나열해요.

무엇이 편집으로 간주되는가 (What counts as an edit)

각 행은 두 연속 요청을 비교해요:

요청 사이의 변경 이후 thinking 블록
메시지를 끝에 추가하기 유효
아직 아무도 참조하지 않은 defer_loading: true 도구 추가하기 유효
기록 앞쪽, 뒤쪽, 또는 전부에서 thinking 블록 제거하기 유효(모델은 그 추론을 잃음)
system, tools, messages 밖의 요청 파라미터 변경(effort, max_tokens, output_config, tool_choice, metadata, thinking.display 등) 유효
cache_control 마커 추가·이동·제거하기 유효
같은 바이트를 반환하는 회전 서명 URL 유효
서버 측 컴팩션이나 컨텍스트 편집으로 내용 제거·교체하기 유효(확인은 보낸 내용을 비교하지, 서버가 편집한 복사본을 비교하지 않음)
지워진 턴 범위 시스템 메시지를 제자리에 남기기 유효
이전 user, assistant, system 메시지를 편집·재정렬·삭제하기 무효. 단, 온디맨드 컴팩션의 서명 블록이 유지되는 thinking 조건 아래에서 요약한 메시지를 대체하는 경우는 예외
첫 사용자 메시지에 넣은 컨텍스트를 바뀐 값으로 다시 렌더링하기 모든 thinking 블록에 대해 무효
이전 tool_result를 지우거나 짧게 줄이기, 이전 이미지 다시 인코딩, 이전 tool_use 입력 변경하기 이후의 모든 thinking 블록에 대해 무효
이전 사용자 턴에 텍스트 블록 추가, 또는 지난번에 추가한 것을 제거하기 무효
최상위 system 문자열이나 블록 바꾸기 무효
tools에서 도구 추가·제거·이름 변경·수정하기 무효
기록 중간에서 thinking 블록 제거하고 이후 블록 유지하기 이후의 모든 thinking 블록에 대해 무효
이전 요청에서 제거했던 thinking 블록 다시 넣기 그동안 만들어진 thinking 블록에 대해 무효
다음 요청에서 다른 바이트를 반환하는 이미지·문서 URL 무효
같은 턴 범위 메시지를 나중 요청에서 삭제하거나 다시 쓰기 무효

코드가 접두사를 편집하는지 확인하기 (Check whether your code edits the prefix)

먼저 보내는 것을 diff하세요. 통합 코드가 몇 번의 일반 턴에 보내는 요청 본문을 포착하세요(컴팩션이나 도구 변경 포함). 연속된 각 요청 쌍에 대해 공유하는 system, tools, messages를 비교해요. 새로 추가된 턴까지는 동일해야 해요.

그런 다음 API로 확인하세요. thinking-binding-controls-2026-08-01 베타 헤더를 추가하고 prefix_mismatch_behavior"drop_block"으로 설정한 뒤 claude-fable-5-1에서 통합 코드로 정상적인 멀티턴 세션을 실행해 보세요. 다음 예제는 통합 코드가 해야 하는 방식대로 두 턴을 실행해요. messages는 늘어나기만 하고, 각 어시스턴트 턴은 API가 반환한 그대로(thinking 블록 포함) 되돌아가며, 모든 요청에 block_binding이 설정돼요. 각 턴 후 응답의 thinking 블록 수와 삭제된 블록 수를 출력해요:

```bash cURL # Counts the thinking blocks in a response and the blocks the API dropped COUNTS='"thinking blocks: \([.content[] | select(.type == "thinking")] | length), " + "dropped: \(.input_transformations | length)"'

FIRST=$(curl -s https://api.anthropic.com/v1/messages
-H "content-type: application/json"
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: thinking-binding-controls-2026-08-01"
-d '{ "model": "claude-fable-5-1", "max_tokens": 16000, "thinking": { "type": "adaptive", "block_binding": { "prefix_mismatch_behavior": "drop_block" } }, "messages": [ { "role": "user", "content": "How many positive integers below 500 have exactly 6 positive divisors?" } ] }') echo "$FIRST" | jq -r "$COUNTS"

Turn 2: the assistant turn goes back exactly as returned, then the next user message

MESSAGES=$(jq -n --argjson first "$FIRST" '[ { role: "user", content: "How many positive integers below 500 have exactly 6 positive divisors?" }, { role: "assistant", content: $first.content }, { role: "user", content: "How many of those are odd?" } ]')

jq -n --argjson messages "$MESSAGES" '{ model: "claude-fable-5-1", max_tokens: 16000, thinking: { type: "adaptive", block_binding: { prefix_mismatch_behavior: "drop_block" } }, messages: $messages }' | curl -s https://api.anthropic.com/v1/messages
-H "content-type: application/json"
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: thinking-binding-controls-2026-08-01"
-d @- | jq -r "$COUNTS"


```bash CLI
# Counts the thinking blocks in a response and the blocks the API dropped
COUNTS='"thinking blocks: \([.content[] | select(.type == "thinking")] | length), " +
  "dropped: \(.input_transformations | length)"'

FIRST=$(ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format json <<'YAML'
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
  block_binding:
    prefix_mismatch_behavior: drop_block
messages:
  - role: user
    content: How many positive integers below 500 have exactly 6 positive divisors?
YAML
)
echo "$FIRST" | jq -r "$COUNTS"

# Turn 2: the assistant turn goes back exactly as returned, then the next user message
ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format json <<YAML | jq -r "$COUNTS"
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
  block_binding:
    prefix_mismatch_behavior: drop_block
messages:
  - role: user
    content: How many positive integers below 500 have exactly 6 positive divisors?
  - role: assistant
    content: $(echo "$FIRST" | jq -c .content)
  - role: user
    content: How many of those are odd?
YAML
client = anthropic.Anthropic()

user_turns = [
    "How many positive integers below 500 have exactly 6 positive divisors?",
    "How many of those are odd?",
]

# messages grows across turns: each assistant turn goes back exactly as returned
messages = []
for user_turn in user_turns:
    messages.append({"role": "user", "content": user_turn})
    response = client.beta.messages.create(
        model="claude-fable-5-1",
        max_tokens=16000,
        thinking={
            "type": "adaptive",
            "block_binding": {"prefix_mismatch_behavior": "drop_block"},
        },
        messages=messages,
        betas=["thinking-binding-controls-2026-08-01"],
    )
    messages.append({"role": "assistant", "content": response.content})
    thinking_blocks = sum(block.type == "thinking" for block in response.content)
    dropped = len(response.input_transformations or [])
    print(f"thinking blocks: {thinking_blocks}, dropped: {dropped}")
const client = new Anthropic();

const userTurns = [
  "How many positive integers below 500 have exactly 6 positive divisors?",
  "How many of those are odd?"
];

// messages grows across turns: each assistant turn goes back exactly as returned
const messages: Anthropic.Beta.BetaMessageParam[] = [];
for (const userTurn of userTurns) {
  messages.push({ role: "user", content: userTurn });
  const response = await client.beta.messages.create({
    model: "claude-fable-5-1",
    max_tokens: 16000,
    thinking: {
      type: "adaptive",
      block_binding: { prefix_mismatch_behavior: "drop_block" }
    },
    messages,
    betas: ["thinking-binding-controls-2026-08-01"]
  });
  messages.push({ role: "assistant", content: response.content });
  const thinkingBlocks = response.content.filter((block) => block.type === "thinking");
  const dropped = response.input_transformations ?? [];
  console.log(`thinking blocks: ${thinkingBlocks.length}, dropped: ${dropped.length}`);
}
AnthropicClient client = new();

string[] userTurns =
[
    "How many positive integers below 500 have exactly 6 positive divisors?",
    "How many of those are odd?",
];

// messages grows across turns: each assistant turn goes back exactly as returned
List<BetaMessageParam> messages = [];
foreach (var userTurn in userTurns)
{
    messages.Add(new() { Role = Role.User, Content = userTurn });
    var response = await client.Beta.Messages.Create(
        new()
        {
            Model = "claude-fable-5-1",
            MaxTokens = 16000,
            Thinking = new BetaThinkingConfigAdaptive
            {
                BlockBinding = new()
                {
                    PrefixMismatchBehavior = BetaThinkingPrefixMismatchBehavior.DropBlock,
                },
            },
            Messages = messages,
            Betas = [AnthropicBeta.ThinkingBindingControls2026_08_01],
        }
    );
    messages.Add(new()
    {
        Role = Role.Assistant,
        Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
    });
    var thinkingBlocks = response.Content.Count(block => block.TryPickThinking(out _));
    var dropped = response.InputTransformations?.Count ?? 0;
    Console.WriteLine($"thinking blocks: {thinkingBlocks}, dropped: {dropped}");
}
client := anthropic.NewClient()

userTurns := []string{
	"How many positive integers below 500 have exactly 6 positive divisors?",
	"How many of those are odd?",
}

// messages grows across turns: each assistant turn goes back exactly as returned
messages := []anthropic.BetaMessageParam{}
for _, userTurn := range userTurns {
	messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(userTurn)))
	response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
		Model:     "claude-fable-5-1",
		MaxTokens: 16000,
		Thinking: anthropic.BetaThinkingConfigParamUnion{
			OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{
				BlockBinding: anthropic.BetaThinkingBlockBindingParam{
					PrefixMismatchBehavior: anthropic.BetaThinkingPrefixMismatchBehaviorDropBlock,
				},
			},
		},
		Messages: messages,
		Betas:    []anthropic.AnthropicBeta{anthropic.AnthropicBetaThinkingBindingControls2026_08_01},
	})
	if err != nil {
		log.Fatal(err)
	}
	messages = append(messages, response.ToParam())
	thinkingBlocks := 0
	for _, block := range response.Content {
		if block.Type == "thinking" {
			thinkingBlocks++
		}
	}
	fmt.Printf("thinking blocks: %d, dropped: %d\n", thinkingBlocks, len(response.InputTransformations))
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaContentBlock;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaThinkingBlockBinding;
import com.anthropic.models.beta.messages.BetaThinkingConfigAdaptive;
import com.anthropic.models.beta.messages.BetaThinkingPrefixMismatchBehavior;
import com.anthropic.models.beta.messages.MessageCreateParams;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    List<String> userTurns = List.of(
        "How many positive integers below 500 have exactly 6 positive divisors?",
        "How many of those are odd?");

    // The builder's message list grows across turns: each assistant turn goes back exactly as returned
    MessageCreateParams.Builder conversation = MessageCreateParams.builder()
        .model("claude-fable-5-1")
        .maxTokens(16000L)
        .thinking(BetaThinkingConfigAdaptive.builder()
            .blockBinding(BetaThinkingBlockBinding.builder()
                .prefixMismatchBehavior(BetaThinkingPrefixMismatchBehavior.DROP_BLOCK)
                .build())
            .build())
        .addBeta(AnthropicBeta.THINKING_BINDING_CONTROLS_2026_08_01);

    for (String userTurn : userTurns) {
        conversation.addUserMessage(userTurn);
        BetaMessage response = client.beta().messages().create(conversation.build());
        conversation.addMessage(response);
        long thinkingBlocks = response.content().stream()
            .filter(BetaContentBlock::isThinking)
            .count();
        int dropped = response.inputTransformations().map(List::size).orElse(0);
        IO.println("thinking blocks: " + thinkingBlocks + ", dropped: " + dropped);
    }
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaThinkingBlockBinding;
use Anthropic\Beta\Messages\BetaThinkingConfigAdaptive;
use Anthropic\Beta\Messages\BetaThinkingPrefixMismatchBehavior;
use Anthropic\Client;

$client = new Client();

$userTurns = [
    'How many positive integers below 500 have exactly 6 positive divisors?',
    'How many of those are odd?',
];

// $messages grows across turns: each assistant turn goes back exactly as returned
$messages = [];
foreach ($userTurns as $userTurn) {
    $messages[] = ['role' => 'user', 'content' => $userTurn];
    $response = $client->beta->messages->create(
        model: 'claude-fable-5-1',
        maxTokens: 16000,
        thinking: BetaThinkingConfigAdaptive::with(
            blockBinding: BetaThinkingBlockBinding::with(
                prefixMismatchBehavior: BetaThinkingPrefixMismatchBehavior::DROP_BLOCK,
            ),
        ),
        messages: $messages,
        betas: [AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01],
    );
    $messages[] = ['role' => 'assistant', 'content' => $response->content];
    $thinkingBlocks = array_filter($response->content, fn ($block) => $block->type === 'thinking');
    $dropped = $response->inputTransformations ?? [];
    echo 'thinking blocks: ', count($thinkingBlocks), ', dropped: ', count($dropped), PHP_EOL;
}
client = Anthropic::Client.new

user_turns = [
  "How many positive integers below 500 have exactly 6 positive divisors?",
  "How many of those are odd?"
]

# messages grows across turns: each assistant turn goes back exactly as returned
messages = []
user_turns.each do |user_turn|
  messages << {role: "user", content: user_turn}
  response = client.beta.messages.create(
    model: "claude-fable-5-1",
    max_tokens: 16_000,
    thinking: {
      type: "adaptive",
      block_binding: {prefix_mismatch_behavior: "drop_block"}
    },
    messages: messages,
    betas: [Anthropic::AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01]
  )
  messages << {role: "assistant", content: response.content}
  thinking_blocks = response.content.count { |block| block.type == :thinking }
  dropped = (response.input_transformations || []).length
  puts "thinking blocks: #{thinking_blocks}, dropped: #{dropped}"
end
thinking blocks: 1, dropped: 0
thinking blocks: 1, dropped: 0

이전에 바뀐 것이 없으므로 어느 턴도 블록을 삭제하지 않아요. 첫 번째 응답에 thinking 블록이 들어 있는지 확인하세요. 적응형(adaptive) thinking에서는 일부 응답에 thinking이 없을 수 있어요. 세션의 어떤 응답에도 하나가 없다면 확인할 것이 없고, 무엇을 바꾸든 삭제 횟수는 0이므로 예제를 다시 실행하세요.

통합 코드의 매 턴에서 input_transformations를 기록하세요. API가 블록을 삭제하면 항목이 이렇게 생겨요:

{
  "input_transformations": [
    {
      "type": "thinking_dropped",
      "path": "messages.1.content.0",
      "reason": "prefix_binding_mismatch"
    }
  ]
}
  • thinking 블록이 들어 있는 세션의 매 턴에서 비어 있음: 통합 코드가 접두사를 온전히 유지한다는 뜻이에요.
  • reason: "prefix_binding_mismatch": path의 블록 앞에 있는 무언가가 이전 요청 이후 바뀌었다는 뜻이에요. 그 턴까지의 system, tools, messages를 diff해서 찾거나 "error"로 요청을 다시 보내 보세요. 400은 보통 무엇이 바뀌었는지 이름을 밝히는 문장으로 끝나요. 그런 다음 접두사를 편집하지 않고 변경하기에서 맞는 대체 방법을 찾으세요.
  • reason: "model_binding_mismatch": 대화가 이전 모델의 블록을 읽을 수 없는 모델로 옮겨졌다는 뜻이에요. 접두사 편집이 아니에요. 대화 중간에 모델 전환하기를 참고하세요.

의도적으로 실패를 보려면 앞선 예제에서 세 번째 턴을 보내고, 그 요청에만 system 프롬프트를 추가해 처음 두 요청(둘 다 없었던)과 다르게 만들어 보세요. "drop_block"으로는 삭제 횟수가 더 이상 0이 아니에요. 응답은 기록의 각 thinking 블록마다 하나씩, 각각 reason: "prefix_binding_mismatch"인 항목을 지녀요. "error"로는 요청이 API가 무효 블록을 처리하는 방식에 설명된 400을 반환하고, 마지막 문장이 system 프롬프트를 가리켜요. cURL과 CLI 탭에서 jq 필터를 빼면 오류 본문을 볼 수 있어요. 횟수가 여전히 0이라면 확인할 것이 없었던 것: 모델이 claude-fable-5-1이고, 요청이 block_binding을 설정하며, 보낸 기록에 thinking 블록이 있고, 처음 두 요청에 system 프롬프트가 없었는지 확인하세요.

두 개의 평범한 턴으로는 문제를 잘 드러내지 못해요. 각 경우를 "error"로 설정하고 아래 항목들을 통과시켜 회귀가 CI를 실패시키게 하세요:

  • 첫 번째 클라이언트 측 컴팩션 또는 정리(trim)
  • 첫 턴 이후에 연결되는 도구·플러그인·MCP 서버
  • 모드 또는 지침 변경
  • 리마인더를 추가하거나 이전 도구 결과를 짧게 줄인다면 긴 도구 루프
  • 다른 모델로 전환했다가 되돌아오기
  • 저장, 재시작, 그리고 나중 날짜에 재개하기

이전 계정에서는 적용을 선택하지 않고 프로덕션 트래픽을 볼 수도 있어요. 베타 헤더를 보내고 block_binding을 빼며 input_transformations를 기록하세요. 헤더만 보내는 것은 모델이 받는 것을 바꾸지 않아요. 편집한 내용 뒤에 오는 모든 thinking 블록은 확인에 실패하고 thinking_dropped 항목과 같은 pathreason 필드를 가진 thinking_mismatch_allowed 항목을 각각 받아요. 편집 앞의 블록은 여전히 통과해요. system이나 tools에 대한 편집은 모든 블록 앞에 오므로 요청의 모든 thinking 블록을 실패시켜요. 항목 하나는 이렇게 생겨요:

{
  "input_transformations": [
    {
      "type": "thinking_mismatch_allowed",
      "path": "messages.1.content.0",
      "reason": "prefix_binding_mismatch"
    }
  ]
}

다음 예제는 이전 계정에서 실행하세요. 새 계정은 세 번째 요청을 400으로 거부하거든요. 이 예제는 block_binding 없이 헤더를 보내고, 앞선 세션에 system 프롬프트를 추가하는 세 번째 요청을 붙여 의도적으로 접두사를 바꿔요. 각 턴 후 thinking 블록 수와 플래그된 블록 수를 출력해요:

```bash cURL # Counts the thinking blocks in a response and the blocks that failed the prefix check COUNTS='"thinking blocks: \([.content[] | select(.type == "thinking")] | length), " + "flagged: \([.input_transformations[] | select(.type == "thinking_mismatch_allowed")] | length)"'

FIRST=$(curl -s https://api.anthropic.com/v1/messages
-H "content-type: application/json"
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: thinking-binding-controls-2026-08-01"
-d '{ "model": "claude-fable-5-1", "max_tokens": 16000, "thinking": { "type": "adaptive" }, "messages": [ { "role": "user", "content": "How many positive integers below 500 have exactly 6 positive divisors?" } ] }') echo "$FIRST" | jq -r "$COUNTS"

Turn 2: the assistant turn goes back exactly as returned, then the next user message

MESSAGES=$(jq -n --argjson first "$FIRST" '[ { role: "user", content: "How many positive integers below 500 have exactly 6 positive divisors?" }, { role: "assistant", content: $first.content }, { role: "user", content: "How many of those are odd?" } ]')

SECOND=$(jq -n --argjson messages "$MESSAGES" '{ model: "claude-fable-5-1", max_tokens: 16000, thinking: { type: "adaptive" }, messages: $messages }' | curl -s https://api.anthropic.com/v1/messages
-H "content-type: application/json"
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: thinking-binding-controls-2026-08-01"
-d @-) echo "$SECOND" | jq -r "$COUNTS"

Turn 3: only this request adds a system prompt, which changes the prefix on purpose

jq -n --argjson messages "$MESSAGES" --argjson second "$SECOND" '{ model: "claude-fable-5-1", max_tokens: 16000, thinking: { type: "adaptive" }, system: "Answer briefly.", messages: ($messages + [ { role: "assistant", content: $second.content }, { role: "user", content: "And how many of the odd ones are below 100?" } ]) }' | curl -s https://api.anthropic.com/v1/messages
-H "content-type: application/json"
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: thinking-binding-controls-2026-08-01"
-d @- | jq -r "$COUNTS"


```bash CLI
# Counts the thinking blocks in a response and the blocks that failed the prefix check
COUNTS='"thinking blocks: \([.content[] | select(.type == "thinking")] | length), " +
  "flagged: \([.input_transformations[] |
    select(.type == "thinking_mismatch_allowed")] | length)"'

FIRST=$(ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format json <<'YAML'
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
messages:
  - role: user
    content: How many positive integers below 500 have exactly 6 positive divisors?
YAML
)
echo "$FIRST" | jq -r "$COUNTS"

# Turn 2: the assistant turn goes back exactly as returned, then the next user message
SECOND=$(ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format json <<YAML
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
messages:
  - role: user
    content: How many positive integers below 500 have exactly 6 positive divisors?
  - role: assistant
    content: $(echo "$FIRST" | jq -c .content)
  - role: user
    content: How many of those are odd?
YAML
)
echo "$SECOND" | jq -r "$COUNTS"

# Turn 3: only this request adds a system prompt, which changes the prefix on purpose
ant beta:messages create --beta thinking-binding-controls-2026-08-01 \
  --format json <<YAML | jq -r "$COUNTS"
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
system: Answer briefly.
messages:
  - role: user
    content: How many positive integers below 500 have exactly 6 positive divisors?
  - role: assistant
    content: $(echo "$FIRST" | jq -c .content)
  - role: user
    content: How many of those are odd?
  - role: assistant
    content: $(echo "$SECOND" | jq -c .content)
  - role: user
    content: And how many of the odd ones are below 100?
YAML
client = anthropic.Anthropic()

user_turns = [
    "How many positive integers below 500 have exactly 6 positive divisors?",
    "How many of those are odd?",
    "And how many of the odd ones are below 100?",
]

# messages grows across turns: each assistant turn goes back exactly as returned
messages = []
for turn, user_turn in enumerate(user_turns, start=1):
    messages.append({"role": "user", "content": user_turn})
    response = client.beta.messages.create(
        model="claude-fable-5-1",
        max_tokens=16000,
        thinking={"type": "adaptive"},
        # Only the last request adds a system prompt, which changes the prefix on purpose
        system="Answer briefly." if turn == len(user_turns) else anthropic.omit,
        messages=messages,
        betas=["thinking-binding-controls-2026-08-01"],
    )
    messages.append({"role": "assistant", "content": response.content})
    thinking_blocks = sum(block.type == "thinking" for block in response.content)
    flagged = sum(
        transformation.type == "thinking_mismatch_allowed"
        for transformation in response.input_transformations or []
    )
    print(f"thinking blocks: {thinking_blocks}, flagged: {flagged}")
const client = new Anthropic();

const userTurns = [
  "How many positive integers below 500 have exactly 6 positive divisors?",
  "How many of those are odd?",
  "And how many of the odd ones are below 100?"
];

// messages grows across turns: each assistant turn goes back exactly as returned
const messages: Anthropic.Beta.BetaMessageParam[] = [];
for (const [turnIndex, userTurn] of userTurns.entries()) {
  messages.push({ role: "user", content: userTurn });
  const response = await client.beta.messages.create({
    model: "claude-fable-5-1",
    max_tokens: 16000,
    thinking: { type: "adaptive" },
    // Only the last request adds a system prompt, which changes the prefix on purpose
    system: turnIndex === userTurns.length - 1 ? "Answer briefly." : undefined,
    messages,
    betas: ["thinking-binding-controls-2026-08-01"]
  });
  messages.push({ role: "assistant", content: response.content });
  const thinkingBlocks = response.content.filter((block) => block.type === "thinking");
  const flagged = (response.input_transformations ?? []).filter(
    (transformation) => transformation.type === "thinking_mismatch_allowed"
  );
  console.log(`thinking blocks: ${thinkingBlocks.length}, flagged: ${flagged.length}`);
}
AnthropicClient client = new();

string[] userTurns =
[
    "How many positive integers below 500 have exactly 6 positive divisors?",
    "How many of those are odd?",
    "And how many of the odd ones are below 100?",
];

// messages grows across turns: each assistant turn goes back exactly as returned
List<BetaMessageParam> messages = [];
for (var turnIndex = 0; turnIndex < userTurns.Length; turnIndex++)
{
    messages.Add(new() { Role = Role.User, Content = userTurns[turnIndex] });
    var response = await client.Beta.Messages.Create(
        new()
        {
            Model = "claude-fable-5-1",
            MaxTokens = 16000,
            Thinking = new BetaThinkingConfigAdaptive(),
            // Only the last request adds a system prompt, which changes the prefix on purpose
            System = turnIndex == userTurns.Length - 1 ? new("Answer briefly.") : null,
            Messages = messages,
            Betas = [AnthropicBeta.ThinkingBindingControls2026_08_01],
        }
    );
    messages.Add(new()
    {
        Role = Role.Assistant,
        Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
    });
    var thinkingBlocks = response.Content.Count(block => block.TryPickThinking(out _));
    var flagged = response.InputTransformations?.Count(transformation =>
        transformation.TryPickThinkingMismatchAllowed(out _)
    ) ?? 0;
    Console.WriteLine($"thinking blocks: {thinkingBlocks}, flagged: {flagged}");
}
client := anthropic.NewClient()

userTurns := []string{
	"How many positive integers below 500 have exactly 6 positive divisors?",
	"How many of those are odd?",
	"And how many of the odd ones are below 100?",
}

// messages grows across turns: each assistant turn goes back exactly as returned
messages := []anthropic.BetaMessageParam{}
for i, userTurn := range userTurns {
	messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(userTurn)))
	// Only the last request adds a system prompt, which changes the prefix on purpose
	var system []anthropic.BetaTextBlockParam
	if i == len(userTurns)-1 {
		system = []anthropic.BetaTextBlockParam{{Text: "Answer briefly."}}
	}
	response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
		Model:     "claude-fable-5-1",
		MaxTokens: 16000,
		Thinking: anthropic.BetaThinkingConfigParamUnion{
			OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{},
		},
		System:   system,
		Messages: messages,
		Betas:    []anthropic.AnthropicBeta{anthropic.AnthropicBetaThinkingBindingControls2026_08_01},
	})
	if err != nil {
		log.Fatal(err)
	}
	messages = append(messages, response.ToParam())
	thinkingBlocks := 0
	for _, block := range response.Content {
		if block.Type == "thinking" {
			thinkingBlocks++
		}
	}
	flagged := 0
	for _, transformation := range response.InputTransformations {
		if transformation.Type == "thinking_mismatch_allowed" {
			flagged++
		}
	}
	fmt.Printf("thinking blocks: %d, flagged: %d\n", thinkingBlocks, flagged)
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaContentBlock;
import com.anthropic.models.beta.messages.BetaInputTransformation;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaThinkingConfigAdaptive;
import com.anthropic.models.beta.messages.MessageCreateParams;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    List<String> userTurns = List.of(
        "How many positive integers below 500 have exactly 6 positive divisors?",
        "How many of those are odd?",
        "And how many of the odd ones are below 100?");

    // The builder's message list grows across turns: each assistant turn goes back exactly as returned
    MessageCreateParams.Builder conversation = MessageCreateParams.builder()
        .model("claude-fable-5-1")
        .maxTokens(16000L)
        .thinking(BetaThinkingConfigAdaptive.builder().build())
        .addBeta(AnthropicBeta.THINKING_BINDING_CONTROLS_2026_08_01);

    for (int turnIndex = 0; turnIndex < userTurns.size(); turnIndex++) {
        if (turnIndex == userTurns.size() - 1) {
            // Only the last request adds a system prompt, which changes the prefix on purpose
            conversation.system("Answer briefly.");
        }
        conversation.addUserMessage(userTurns.get(turnIndex));
        BetaMessage response = client.beta().messages().create(conversation.build());
        conversation.addMessage(response);
        long thinkingBlocks = response.content().stream()
            .filter(BetaContentBlock::isThinking)
            .count();
        long flagged = response.inputTransformations().stream()
            .flatMap(List::stream)
            .filter(BetaInputTransformation::isThinkingMismatchAllowed)
            .count();
        IO.println("thinking blocks: " + thinkingBlocks + ", flagged: " + flagged);
    }
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaThinkingConfigAdaptive;
use Anthropic\Beta\Messages\BetaThinkingMismatchAllowedInputTransformation;
use Anthropic\Client;

$client = new Client();

$userTurns = [
    'How many positive integers below 500 have exactly 6 positive divisors?',
    'How many of those are odd?',
    'And how many of the odd ones are below 100?',
];

// $messages grows across turns: each assistant turn goes back exactly as returned
$messages = [];
foreach ($userTurns as $turnIndex => $userTurn) {
    $messages[] = ['role' => 'user', 'content' => $userTurn];
    $response = $client->beta->messages->create(
        model: 'claude-fable-5-1',
        maxTokens: 16000,
        thinking: BetaThinkingConfigAdaptive::with(),
        // Only the last request adds a system prompt, which changes the prefix on purpose
        system: $turnIndex === array_key_last($userTurns) ? 'Answer briefly.' : null,
        messages: $messages,
        betas: [AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01],
    );
    $messages[] = ['role' => 'assistant', 'content' => $response->content];
    $thinkingBlocks = array_filter($response->content, fn ($block) => $block->type === 'thinking');
    $flagged = array_filter(
        $response->inputTransformations ?? [],
        fn ($transformation) => $transformation instanceof BetaThinkingMismatchAllowedInputTransformation,
    );
    echo 'thinking blocks: ', count($thinkingBlocks), ', flagged: ', count($flagged), PHP_EOL;
}
client = Anthropic::Client.new

user_turns = [
  "How many positive integers below 500 have exactly 6 positive divisors?",
  "How many of those are odd?",
  "And how many of the odd ones are below 100?"
]

# messages grows across turns: each assistant turn goes back exactly as returned
messages = []
user_turns.each_with_index do |user_turn, turn_index|
  messages << {role: "user", content: user_turn}
  # Only the last request adds a system prompt, which changes the prefix on purpose
  system_param = (turn_index == user_turns.length - 1) ? {system_: "Answer briefly."} : {}
  response = client.beta.messages.create(
    model: "claude-fable-5-1",
    max_tokens: 16_000,
    thinking: {type: "adaptive"},
    messages: messages,
    betas: [Anthropic::AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01],
    **system_param
  )
  messages << {role: "assistant", content: response.content}
  thinking_blocks = response.content.count { |block| block.type == :thinking }
  flagged = (response.input_transformations || []).count do |transformation|
    transformation.type == :thinking_mismatch_allowed
  end
  puts "thinking blocks: #{thinking_blocks}, flagged: #{flagged}"
end
thinking blocks: 1, flagged: 0
thinking blocks: 1, flagged: 0
thinking blocks: 1, flagged: 2

세 번째 응답은 이전 턴들의 모든 thinking 블록(이 실행에서는 턴당 하나씩)을 플래그해요. 새 system 프롬프트가 그 모두 앞에 오기 때문이에요. 모델은 여전히 그 블록을 읽었어요.

이 항목들을 prefix_binding_mismatch 삭제처럼 다루세요. 편집은 첫 번째로 나열된 블록 앞에 있어요. 그 블록의 path까지의 system, tools, messages를 이전 요청과 diff해서 찾은 뒤 접두사를 편집하지 않고 변경하기의 맞는 패턴으로 교체하세요. 새 계정이나 prefix_mismatch_behavior를 설정한 요청에서는 API가 대신 요청을 거부하거나 실패 블록을 삭제해요. 이 항목들은 적용이 제거할 것의 하한선이에요. "drop_block"에서는 실패 블록 하나가 그 턴의 나머지 thinking 블록도 함께 데려가고, 블록 하나를 제거하면 다음 블록도 실패하게 만들 수 있어요. API가 확인만 기록할 때는 각 블록을 따로 판단해서, 오직 그 블록이 실패할 때만 나열해요.

접두사를 편집하지 않고 변경하기 (Make changes without editing the prefix)

흔한 접두사 편집마다 모델에 같은 정보를 주면서 이전 바이트를 그대로 두는 대체 방법이 있어요. 그래서 이후 thinking이 유효하게 유지되죠. 오늘 코드가 만드는 편집을 첫 번째 열에서 찾으세요:

대신에 사용하기 베타 헤더
최상위 system 프롬프트 다시 만들기 대화 중간 시스템 메시지 없음
각 요청에서 첫 사용자 메시지의 컨텍스트(환경, 날짜, 메모리, 프로젝트 지침) 다시 렌더링하기 한 번 렌더링해서 바꾸지 않고 다시 보내기. 무언가 바뀌면 새 버전을 가장 최근 턴에 넣기 없음
제자리에서 이전 tool_result 내용을 지우거나 짧게 줄이거나 이전 이미지를 다시 인코딩하기 도구 결과를 짧게 줄이거나 이미지를 축소할 때는 처음 보낼 때 미리. 나중에 이전 결과를 지우려면 clear_tool_uses_20250919서버에서 컨텍스트 정리하기 context-management-2025-06-27
리마인더를 주입했다가 다음 요청에서 삭제하기 턴 범위 시스템 메시지(clear_at: "next_user_message") mid-conversation-system-clear-at-2026-08-21
tools 항목을 추가하거나 제거하기 tool_additiontool_removal 블록 inline-tools-2026-09-15(도구가 MCP 커넥터로 연결한 MCP 서버에서 온 경우 mcp-client-2026-09-15 추가), 또는 Claude API·Amazon Bedrock·Google Cloud에서 동작하는 이전 mid-conversation-tool-changes-2026-07-01
최상위 output_config.effort 바꾸기(캐시 재시작, thinking에는 영향 없음) 메시지별 output_config mid-conversation-output-config-2026-07-01
클라이언트에서 이전 턴 버리기 또는 요약하기 thinking을 유지하면서 최근 턴을 보존하는 온디맨드 컴팩션, 다른 서버 측 컴팩션 또는 컨텍스트 편집, 또는 오래된 thinking을 남기지 않는 클라이언트 측 컴팩션 compact-2026-09-04
요청 사이에 바이트가 바뀌는 이미지·문서 URL Files API의 file_id, 또는 base64 없음

이 모든 것은 어시스턴트 턴을 반환된 그대로 다시 보내는 것을 전제로 해요. 대화 중간 시스템 메시지, 턴 범위 시스템 메시지, 도구 변경은 모든 모델에서 쓸 수 있는 게 아니에요. 대화 중간 시스템 메시지와 도구 변경에 이를 받아들이는 모델 목록이 나와 있어요. 코드가 여러 모델을 서비스한다면, 그 메시지를 받지 않는 모델에서는 최상위 system 프롬프트를 계속 편집하세요.

한 요청에서 여러 베타를 쓰려면 값을 하나의 anthropic-beta 헤더에 합치세요. 베타 이름은 Amazon Bedrock과 Google Cloud에서 사용 가능한 곳에서 동일해요(베타 헤더 참조):

anthropic-beta: thinking-binding-controls-2026-08-01,mid-conversation-system-clear-at-2026-08-21,inline-tools-2026-09-15

어시스턴트 턴을 반환된 그대로 다시 보내기 (Send assistant turns back exactly as returned)

각 응답의 content 배열을 저장하고 어시스턴트 턴으로 그대로 다시 보내세요. 모든 블록 유형을 받은 순서대로, thinking 필드가 비어 있는 thinking 블록을 포함해서요. 알 수 없는 블록 유형을 버리거나 빈 필드를 버리거나 블록 순서를 바꾸는 직렬화기는 이후의 모든 턴의 접두사를 편집해요.

Claude Fable 5.1에서 thinking 필드는 기본으로 비어 있고 signature가 추론을 담아요. 그래서 빈 블록을 건너뛰는 직렬화기는 thinking을 제거하게 돼요. 전부 제거하면 아무것도 실패하지 않고 모델은 매 턴 이전 추론을 잃어요. 스트림을 직접 파싱한다면 thinking 텍스트가 도착하지 않아도 블록을 유지하세요. 블록이 열리고 signature_delta 이벤트로 signature를 받은 뒤 닫혀요. 빈 signature로 다시 보낸 블록은 실패해요.

대화 중간 시스템 메시지로 지침 추가하기 (Add instructions with a mid-conversation system message)

일부 하네스는 현재 시간, 토큰 예산, 모드 플래그, 새로 발견한 프로젝트 컨텍스트를 실어 나르기 위해 매 요청마다 최상위 system 프롬프트를 다시 만들어요. 그러면 대화의 모든 thinking 블록이 무효가 돼요. 대신 세션 시작 시 system을 고정하세요. 무언가 바뀌면 그 변경이 참이 되는 지점의 messagesrole: "system" 메시지를 추가하세요:

{
  "role": "system",
  "content": "The user switched the workspace to read-only mode. Do not write files until told otherwise."
}

모델은 이 메시지를 시스템 프롬프트 권위로 취급하고, 그 앞의 모든 것은 그대로 유지돼요. 도구 루프에서는 어시스턴트 tool_use와 그 tool_result 사이가 아니라 tool_result 사용자 메시지 뒤에 메시지를 두세요(제한 사항 참조). 한 번 보내면 이 메시지는 이후 thinking의 접두사 일부가 돼요. 이후 요청에서 제자리에 두세요.

바뀌는 컨텍스트를 가장 최근 턴에 넣기 (Put changing context in the newest turn)

일부 하네스는 첫 사용자 메시지에 환경 블록(작업 디렉터리, 브랜치, 날짜, 메모리, 프로젝트 지침)을 넣고 매 요청마다 다시 렌더링해요. 어떤 값이라도 바뀌면 messages[0]이 바뀌고 대화의 모든 thinking 블록이 무효가 돼요. 그 블록을 한 번 렌더링해서 그대로 다시 보내세요. 값이 바뀌면 가장 최근 턴에서 말하세요. 보내려는 사용자 메시지에 텍스트 블록을 추가하거나, 변경이 운영자로서 사용자에게서 온 것이라면 대화 중간 시스템 메시지를 추가하세요.

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "Environment update: the current branch is now release-2."
    },
    { "type": "text", "text": "Run the tests again." }
  ]
}

한 번 보내면 그 텍스트 블록은 이후 thinking의 접두사 일부가 돼요. 이후 요청에서 제자리에 두세요.

턴별 리마인더를 턴 범위 시스템 메시지로 보내기 (Send per-turn reminders as turn-scoped system messages)

흔한 접두사 편집은 턴별 넛지(nudge)예요. "독립적인 읽기는 함께 요청하세요" 또는 "한동안 사용자를 업데이트하지 않았어요" 같은 한 줄을 코드가 각 도구 결과 묶음 뒤에 추가하는 것. 리마인더가 쌓이는 것을 막으려면 각 넛지를 clear_at: "next_user_message"가 있는 대화 중간 시스템 메시지로 보내고 tool_result 사용자 메시지 뒤에 두세요. clear_at는 베타 헤더 mid-conversation-system-clear-at-2026-08-21이 필요해요. 다음 messages 배열은 두 번의 도구 호출과 그 결과 이후의 요청이에요. messages[3]은 이전 요청의 넛지를 제자리에 둔 것이고, messages[6]은 이 요청의 복사본이에요:

[
  { "role": "user", "content": "Fix the failing test." },
  {
    "role": "assistant",
    "content": [
      { "type": "thinking", "thinking": "", "signature": "..." },
      {
        "type": "tool_use",
        "id": "toolu_01",
        "name": "read_file",
        "input": { "path": "tests/test_auth.py" }
      }
    ]
  },
  {
    "role": "user",
    "content": [{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "..." }]
  },
  {
    "role": "system",
    "clear_at": "next_user_message",
    "content": "Request every independent read in one turn."
  },
  {
    "role": "assistant",
    "content": [
      { "type": "thinking", "thinking": "", "signature": "..." },
      {
        "type": "tool_use",
        "id": "toolu_02",
        "name": "read_file",
        "input": { "path": "src/auth.py" }
      }
    ]
  },
  {
    "role": "user",
    "content": [{ "type": "tool_result", "tool_use_id": "toolu_02", "content": "..." }]
  },
  {
    "role": "system",
    "clear_at": "next_user_message",
    "content": "Request every independent read in one turn."
  }
]

tool_result 블록만 들어 있는 사용자 메시지는 "다음 사용자 메시지"로 간주되므로 messages[3]은 이미 지워져요. 모델이 보는 것에 아무것도 더하지 않고 입력 토큰 비용도 없지만, 여전히 배열에 있으므로 messages[4]의 thinking은 유효하게 유지돼요. messages[6]이 이번 턴에 모델이 보는 복사본이에요. 이후 요청에서는 둘 다 제자리에 두고 다음 tool_result 메시지 뒤에 새 복사본을 추가하세요.

tool_additiontool_removal로 도구 추가·제거하기 (Add or remove tools with tool_addition and tool_removal)

세션 중간에 tools 배열을 편집하면 보존된 thinking 블록이 무효가 돼요. 처음 보낸 그대로 배열을 두고, tool_addition 또는 tool_removal 블록을 담은 role: "system" 메시지를 추가해서 모델이 사용할 수 있는 도구를 바꾸세요. 이것들은 대화 중간 도구 변경이며 Claude API에서 쓸 수 있는 베타 헤더 inline-tools-2026-09-15가 필요해요. 이전 mid-conversation-tool-changes-2026-07-01 헤더는 도구를 참조로 이름 지어 부르는 변경에서 여전히 동작해요(Claude API, Amazon Bedrock, Google Cloud에서).

이 블록들을 사용하는 두 가지 방법이 있어요:

  • 모든 도구를 미리 선언하기. 세션이 필요로 할 수 있는 모든 도구를 첫 요청의 tools에 넣고, 아직 모델이 보면 안 되는 것에는 defer_loading: true를 붙여요. 그런 다음 그들을 이름 짓는 tool_additiontool_removal 블록으로 도구를 켜고 꺼요.
  • 스냅샷으로 시작하고 도구를 추가해 가기. 아는 도구를 첫 요청의 tools에 넣어요. 새 도구가 나타나면 tools를 편집하는 대신 tool_addition 블록 안에 정의해요.

어느 쪽이든 tools는 절대 바뀌지 않으므로 이전 thinking은 유효하게 유지되고 프롬프트 캐시도 여전히 히트해요. 아래에 언급된 한 가지 예외는 빼고요.

예를 들어 모드 전환 후 위험한 도구를 회수하려면:

{
  "role": "system",
  "content": [
    { "type": "tool_removal", "tool": { "type": "tool_reference", "name": "delete_branch" } },
    { "type": "text", "text": "Branch deletion is disabled for the rest of this session." }
  ]
}

defer_loading: true로 선언한 도구를 켜려면 이름을 짓는 tool_addition 블록을 추가하세요:

{
  "role": "system",
  "content": [
    { "type": "tool_addition", "tool": { "type": "tool_reference", "name": "deploy" } },
    { "type": "text", "text": "Authentication succeeded. Deployment is now available." }
  ]
}

스키마를 아직 모르는 도구 때문에 미리 선언할 수 없는 경우도 있어요. 런타임에 애플리케이션이 발견하는 도구나 첫 턴 후에 연결되는 MCP 서버 같은 것. tools를 건드리는 대신 tool_addition 블록 안에 정의하세요. inline-tools-2026-09-15에서 블록의 tool{"type": "tool_definition", "definition": {...}}일 수 있고, tools에 넣었을 것과 같은 항목을 담아요:

{
  "role": "system",
  "content": [
    {
      "type": "tool_addition",
      "tool": {
        "type": "tool_definition",
        "definition": {
          "name": "db_query",
          "description": "Run a read-only SQL query against the analytics database.",
          "input_schema": {
            "type": "object",
            "properties": { "sql": { "type": "string" } },
            "required": ["sql"]
          }
        }
      }
    }
  ]
}

새 도구는 messages에 도착하고 tools는 절대 바뀌지 않으며 이전 thinking은 유효하게 유지돼요. toolsdefer_loading: true가 아닌 도구를 적어도 하나 두세요. 모든 도구가 지연 상태라면 이렇게 정의한 첫 도구는 전체 프롬프트 캐시 미스 하나의 비용이 들어요.

API가 MCP 커넥터를 통해 사용자를 대신해 MCP 서버에 연결한다면 mcp-client-2026-09-15도 보내세요. 이 헤더는 mcp-client-2025-11-20이 하는 모든 것을 포함하므로 그것 대신 보내면 돼요. 그러면 블록의 definitionmcp_servers에 나열된 서버용 mcp_toolset이 될 수 있어요. API가 서버의 도구 목록을 가져와야 하면 응답은 그 서버용 mcp_tool_listing 블록으로 시작해요. 나머지 어시스턴트 턴과 함께 그대로 다시 보내고, 그 헤더를 지닌 이후의 모든 요청에서 mcp-client-2026-09-15를 계속 보내세요. 그 블록은 툴셋을 그 목록에 고정하므로 API가 그 서버에 다시 연결하지 않아요. 이 MCP 커넥터 기능들은 Claude API에서 사용할 수 있어요.

이전 헤더만으로도 세션 중간에 알게 된 도구를 defer_loading: truetools에 추가할 수 있고, tool_addition 블록으로 제공할 수 있어요. tool_addition 블록이 참조할 때까지 접두사 확인은 지연 도구를 무시하므로 안전해요. defer_loading: true 없이 도구를 추가하면 접두사가 바뀌고 이전 thinking이 무효가 돼요.

이 블록들을 담는 role: "system" 메시지는 이후 thinking의 접두사에 합류해요. 이후 요청에서 제자리에 두세요.

메시지별 output_config로 노력 수준 바꾸기 (Change effort with a per-message output_config)

요청 사이에 최상위 output_config.effort를 바꾸는 것은 thinking을 무효로 만들지 않아요. 노력 수준은 접두사 일부가 아니기 때문이에요. 다만 최상위 노력 수준을 바꾸면 프롬프트 캐시는 다시 시작돼요. Claude Fable 5.1에서는 메시지별 노력 수준을 대신 사용하세요. 빈 content와 새 수준을 가진 role: "system" 메시지를 추가하는 거예요. 베타 헤더 mid-conversation-output-config-2026-07-01이 필요해요.

{ "role": "system", "content": [], "output_config": { "effort": "low" } }

새 수준은 다음 user 턴부터 적용돼요. 한 번 보내면 이 메시지는 messages의 일부가 되므로 이후 thinking의 접두사 일부예요. 이후 요청에서 제자리에 두고, 노력 수준을 다시 바꾸려면 또 하나를 추가하세요.

서버에서 컨텍스트 정리하기 (Trim context on the server)

또 다른 흔한 접두사 편집은 클라이언트 측 정리(trim)예요. 가장 오래된 턴을 버리거나 요약하고 최근 턴을 그대로 유지하는 것. 유지된 턴의 thinking 블록은 제거된 기록이 여전히 있을 때 만들어졌으므로 확인에 실패해요. 서버 측 대안은 편집으로 간주되지 않아요. 확인은 보낸 그대로의 대화를 비교하기 때문이에요:

  • 온디맨드 컴팩션(베타)은 별도 요청에서 요약을 반환하고, 이는 백그라운드에서 실행될 수 있어요. 그리고 반환된 블록을 그것이 요약하는 메시지 대신 보내요. 확인은 그 교체를 받아들이므로, 유지되는 thinking 조건 아래에서 유지하는 턴이 thinking과 함께 유효하게 남을 수 있어요. 요약 요청하기에서 요청을 보여주고 필요한 베타 헤더를 알려줘요.
  • 토큰 임계값 컴팩션은 컨텍스트가 설정한 임계값에 가까워지면 이전 턴을 컴팩션 블록으로 요약하고, 확인되는 접두사는 그 블록부터 다시 시작돼요. 그 instructions 파라미터에 "모든 티커, 포지션 크기, 명시된 가정을 보존하세요" 같은 나만의 요약 프롬프트를 넣을 수 있어요.
  • 컨텍스트 편집은 규칙에 따라 이전 도구 결과나 이전 thinking 블록을 가장 오래된 것부터 지워요. 전략은 clear_tool_uses_20250919clear_thinking_20251015예요.

클라이언트에서 컴팩션하기 (Compact on the client)

클라이언트에서도 컴팩션할 수 있어요. 스스로 요약을 쓴다면 재작성 전에 만들어진 thinking 블록을 다시 보내지 마세요. API가 온디맨드 컴팩션으로 쓴다면 유지된 thinking이 유효하게 유지되는 조건이 언제 유지 thinking이 유효한지 나열해요.

단순 컴팩션(권장) (Simple compaction (recommended))

대화가 너무 길어지면 전체 세션을 하나의 사용자 메시지로 요약하고 그 메시지와 다음 지시만 보내세요. 이전의 어떤 것도 재생되지 않으므로 확인할 thinking이 남지 않고, 모델은 요약에서 새로 추론해요.

단순 컴팩션: 요청 4는 각 어시스턴트 턴에 thinking이 있는 전체 기록을 보내고, 요청 5는 턴 1~4의 요약과 다음 지시를 담은 하나의 사용자 메시지를 보내므로 이전 thinking이 전송되지 않고 확인할 것이 없음

[
  {
    "role": "user",
    "content": "<summary of the session so far>\n\n<the next instruction>"
  }
]

Claude 모델은 이 방식을 사용하는 장기 과제에 훈련되어 있고, 대부분의 워크로드에서 잘 동작해요.

꼬리 유지 컴팩션 (Keep-tail compaction)

꼬리 유지 컴팩션은 이전 턴을 요약하고 가장 최근 턴을 그대로 유지하므로, 모델은 마지막 몇 번의 교류를 그대로 보게 돼요. 스스로 요약을 쓴다면 규칙을 어기는 것이에요. 유지된 어시스턴트 턴은 여전히 원래 턴들(요약이 아니라)이 앞에 왔을 때 만들어진 thinking 블록을 지니기 때문이에요. 그 블록들은 실패해요.

그 thinking을 유지하려면 API가 온디맨드 컴팩션으로 요약을 쓰게 하세요. 최근 턴을 유지하는 컴팩션이 방법을 보여주고, 유지된 thinking이 유효하게 유지되는 조건이 언제 유효한지 나열해요.

이 섹션의 나머지는 스스로 쓰는 요약을 다룹니다.

꼬리 유지 컴팩션: 기록이 턴 1·2의 요약과 그 뒤 이어지는 턴 3~5(그대로)로 교체됨. 어시스턴트 턴 3·4의 thinking은 원래 턴들 뒤에서 만들어졌지 요약 뒤가 아니므로 실패함. prefix_mismatch_behavior drop_block을 보낸 같은 요청은 성공하고, API는 그 두 블록을 버리고 input_transformations에 나열함

해결책: 턴을 그대로 두고 prefix_mismatch_behavior: "drop_block"을 보내세요. API가 오래된 thinking 블록을 버리고, 모델은 유지된 턴의 texttool_use 블록을 읽으며, 요청은 성공해요.

컴팩션된 기록을 messages로 넘기고 thinking 설정에 block_binding을 설정하세요. 다음 예제에서 compacted_messages는 컴팩션 단계가 만든 배열이에요. 요약 메시지 뒤에 API가 반환한 그대로(thinking 블록 포함)의 유지된 턴:

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: thinking-binding-controls-2026-08-01" \ -d "{ \"model\": \"claude-fable-5-1\", \"max_tokens\": 16000, \"thinking\": { \"type\": \"adaptive\", \"block_binding\": { \"prefix_mismatch_behavior\": \"drop_block\" } }, \"messages\": $COMPACTED_MESSAGES }" ```
ant beta:messages create --beta thinking-binding-controls-2026-08-01 <<YAML
model: claude-fable-5-1
max_tokens: 16000
thinking:
  type: adaptive
  block_binding:
    prefix_mismatch_behavior: drop_block
messages: $COMPACTED_MESSAGES
YAML
client = anthropic.Anthropic()

# compacted_messages: the summary message, then the kept turns as returned
response = client.beta.messages.create(
    model="claude-fable-5-1",
    max_tokens=16000,
    thinking={
        "type": "adaptive",
        "block_binding": {"prefix_mismatch_behavior": "drop_block"},
    },
    messages=compacted_messages,
    betas=["thinking-binding-controls-2026-08-01"],
)

print(response.input_transformations)
const client = new Anthropic();

// compactedMessages: the summary message, then the kept turns as returned
const response = await client.beta.messages.create({
  model: "claude-fable-5-1",
  max_tokens: 16000,
  thinking: {
    type: "adaptive",
    block_binding: { prefix_mismatch_behavior: "drop_block" }
  },
  messages: compactedMessages,
  betas: ["thinking-binding-controls-2026-08-01"]
});

console.log(response.input_transformations);
AnthropicClient client = new();

// compactedMessages: the summary message, then the kept turns as returned
var response = await client.Beta.Messages.Create(
    new()
    {
        Model = "claude-fable-5-1",
        MaxTokens = 16000,
        Thinking = new BetaThinkingConfigAdaptive
        {
            BlockBinding = new()
            {
                PrefixMismatchBehavior = BetaThinkingPrefixMismatchBehavior.DropBlock,
            },
        },
        Messages = compactedMessages,
        Betas = [AnthropicBeta.ThinkingBindingControls2026_08_01],
    }
);

Console.WriteLine(response.InputTransformations?.Count ?? 0);
client := anthropic.NewClient()

// compactedMessages: the summary message, then the kept turns as returned
response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
	Model:     "claude-fable-5-1",
	MaxTokens: 16000,
	Thinking: anthropic.BetaThinkingConfigParamUnion{
		OfAdaptive: &anthropic.BetaThinkingConfigAdaptiveParam{
			BlockBinding: anthropic.BetaThinkingBlockBindingParam{
				PrefixMismatchBehavior: anthropic.BetaThinkingPrefixMismatchBehaviorDropBlock,
			},
		},
	},
	Messages: compactedMessages,
	Betas:    []anthropic.AnthropicBeta{anthropic.AnthropicBetaThinkingBindingControls2026_08_01},
})
if err != nil {
	log.Fatal(err)
}

fmt.Println(len(response.InputTransformations))
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaThinkingBlockBinding;
import com.anthropic.models.beta.messages.BetaThinkingConfigAdaptive;
import com.anthropic.models.beta.messages.BetaThinkingPrefixMismatchBehavior;
import com.anthropic.models.beta.messages.MessageCreateParams;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    // compactedMessages: the summary message, then the kept turns as returned
    MessageCreateParams params = MessageCreateParams.builder()
        .model("claude-fable-5-1")
        .maxTokens(16000L)
        .thinking(BetaThinkingConfigAdaptive.builder()
            .blockBinding(BetaThinkingBlockBinding.builder()
                .prefixMismatchBehavior(BetaThinkingPrefixMismatchBehavior.DROP_BLOCK)
                .build())
            .build())
        .messages(compactedMessages)
        .addBeta(AnthropicBeta.THINKING_BINDING_CONTROLS_2026_08_01)
        .build();

    BetaMessage response = client.beta().messages().create(params);

    IO.println(response.inputTransformations());
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaThinkingBlockBinding;
use Anthropic\Beta\Messages\BetaThinkingConfigAdaptive;
use Anthropic\Beta\Messages\BetaThinkingPrefixMismatchBehavior;
use Anthropic\Client;

$client = new Client();

// $compactedMessages: the summary message, then the kept turns as returned
$response = $client->beta->messages->create(
    model: 'claude-fable-5-1',
    maxTokens: 16000,
    thinking: BetaThinkingConfigAdaptive::with(
        blockBinding: BetaThinkingBlockBinding::with(
            prefixMismatchBehavior: BetaThinkingPrefixMismatchBehavior::DROP_BLOCK,
        ),
    ),
    messages: $compactedMessages,
    betas: [AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01],
);

var_dump($response->inputTransformations);
client = Anthropic::Client.new

# compacted_messages: the summary message, then the kept turns as returned
response = client.beta.messages.create(
  model: "claude-fable-5-1",
  max_tokens: 16_000,
  thinking: {
    type: "adaptive",
    block_binding: {prefix_mismatch_behavior: "drop_block"}
  },
  messages: compacted_messages,
  betas: [Anthropic::AnthropicBeta::THINKING_BINDING_CONTROLS_2026_08_01]
)

puts response.input_transformations

응답은 평소처럼 새 어시스턴트 턴을 지니고, 삭제된 블록마다 input_transformations 항목 하나를 추가로 지녀요. 다이어그램의 기록에서는 어시스턴트 턴 3·4의 thinking이 그것이에요:

{
  "input_transformations": [
    {
      "type": "thinking_dropped",
      "path": "messages.2.content.0",
      "reason": "prefix_binding_mismatch"
    },
    {
      "type": "thinking_dropped",
      "path": "messages.4.content.0",
      "reason": "prefix_binding_mismatch"
    }
  ]
}

그 두 턴이 기록에 남아 있는 한 이후 요청에서도 "drop_block"을 계속 보내세요. 이 요청부터 모델이 만드는 thinking은 요약을 따르며 유효하게 유지돼요. 베타 헤더에 의존하고 싶지 않다면, 컴팩션된 기록을 만들 때 유지된 어시스턴트 턴에서 thinkingredacted_thinking 블록을 직접 벗겨내는 방법도 있어요.

백그라운드(비동기) 컴팩션 (Background (async) compaction)

백그라운드 컴팩션은 대화가 계속되는 동안 중요 경로 밖에서 요약을 만들고 몇 요청 뒤에 교체해요. 그 사이 만들어진 thinking을 유지하려면 API가 온디맨드 컴팩션으로 요약을 쓰게 하세요. 백그라운드 컴팩션에 단계가 있고, 그 thinking은 유지된 최근 턴과 같은 유지된 thinking 조건 아래에서 유효하게 유지돼요.

스스로 만드는 요약은 꼬리 유지처럼 규칙을 어기는데, 지연이 더해져요. 요약이 만들어지는 동안 만들어진 모든 어시스턴트 턴이 교체 이전의 thinking을 지니고, 요약이 도착하는 순간 전부 실패해요. 사용한다면 교체를 꼬리 유지처럼 취급해 교체 이후부터 "drop_block"을 보내거나 동기적으로 컴팩션하세요.

보존된 thinking과 함께 동작하지 않는 패턴 (Patterns that don't work with preserved thinking)
  • 중간에서 턴 잘라내기. 개별 턴을 제거하면 그 뒤의 모든 thinking 블록이 무효가 되고, 어떤 컴팩션 방식도 이를 피하지 못해요. 지시를 바꾸려고 턴을 자르는 중이었다면 대화 중간 시스템 메시지를 추가하세요. 이전 도구 결과나 이전 thinking을 선택적으로 제거하려면 서버 측 컨텍스트 편집을 사용하세요.
  • 도구 라운드 중간에 컴팩션하기. 어시스턴트 턴의 tool_use와 그에 답하는 tool_result 사이에 컴팩션하지 마세요. 모델이 추론과 함께 그 라운드를 마치도록 그 어시스턴트 턴을 thinking과 함께 그대로 다시 보내세요. Thinking 블록 보존하기를 참고하세요.

내용이 바뀌는 URL 대신 ID로 파일 참조하기 (Reference files by ID, not by a URL whose content changes)

url 소스를 가진 image 또는 document 블록에서 확인은 URL 문자열이 아니라 가져온 바이트를 다뤄요. 내용이 바뀌는 URL은 이후 thinking을 무효로 만들어요. "최신 스크린샷" 엔드포인트나 누군가 턴 사이에 편집하는 문서 같은 것. 같은 파일의 회전 서명 URL은 그렇지 않아요. 턴에 걸쳐 참조하는 내용이라면 Files API로 한 번 업로드하고 file_id를 사용하거나 base64를 보내세요.

라이브러리, 프록시, 게이트웨이 (Libraries, proxies, and gateways)

라이브러리·프록시·게이트웨이는 다른 사람의 기록과 API 사이에 놓이므로, 그 자체의 재작성이 편집으로 간주되고, 사용자들은 그것을 보거나 고칠 수 없어요.

  • 알아보지 못하는 것은 통과시키세요. 호출자의 anthropic-beta 값과 thinking.block_binding을 그대로 전달하고 input_transformations를 돌려주세요. 알 수 없는 키를 거부하는 옵션 스키마는 사용자가 "drop_block"을 고르는 것을 막아요.
  • role: "system" 메시지를 호출자가 둔 곳에 두세요. 최상위 system 필드로 옮기면 그 요청의 system이 바뀌고 대화의 모든 thinking 블록이 무효가 돼요.
  • 한 요청에서 도구 사용을 끄려면 tool_choice: {"type": "none"}을 보내세요. tools를 제거하지 마세요.
  • 400을 숨기지 마세요. 코드가 400을 잡아 thinking을 벗겨내고 호출자를 대신해 재시도한다면, 그렇게 했다고 기록하세요. 그들의 기록은 여전히 편집되고 모델은 이후의 모든 요청에서 이전 추론을 잃어요.

FAQ

아니요. `thinking-binding-controls-2026-08-01` 베타 헤더를 보내고 `thinking.block_binding.prefix_mismatch_behavior`를 설정하세요. 필드를 설정하면 계정 나이와 무관하게 그 요청이 적용을 선택하는 것이에요. `"error"`는 새 계정이 받는 것과 같은 400으로 편집된 기록을 거부하고, `"drop_block"`은 요청을 통과시키고 무엇이 삭제됐는지 `input_transformations`에 나열해요. 이전 계정에서 확인을 적용하지 않고 접두사 편집을 찾으려면 헤더를 보내고 필드를 설정하지 마세요. 실패 블록은 여전히 모델에 도달하고 `input_transformations`는 `thinking_mismatch_allowed`로 나열해요. [코드가 접두사를 편집하는지 확인하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#how-to-tell-whether-your-integration-is-impacted)를 참고하세요. 아니요. 실패하는 것은 바꾼 지점 이후에 이미 기록에 있는 thinking이고, 그것을 어떻게 할지는 사용자가 정해요. `prefix_mismatch_behavior: "drop_block"`으로 API는 그 블록들을 버리고 요청은 성공해요. 모델은 그 추론 없이 그 턴에 답하고 프롬프트 캐시는 편집 지점에서 다시 시작돼요. 기본 `"error"`로는 API가 400으로 요청을 거부하고, 편집을 되돌리거나 `"drop_block"`으로 다시 보낼 때까지 그렇죠. [API가 무효 블록을 처리하는 방식](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#mismatch-behavior)을 참고하세요. [무엇이 편집으로 간주되는가](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#what-counts-as-an-edit)에서 어떤 변경이 영향이 있는지 나열해요. 아니요. `output_config.effort`, `max_tokens`, `thinking` 설정은 확인되는 접두사 일부가 아니에요. 접두사는 `system`, `tools`, `messages`만 다뤄요. 최상위 노력 수준 변경은 프롬프트 캐시 대부분을 무효로 만들어요. Claude Fable 5.1에서는 [메시지별 노력 수준](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#effort-changes) 변경이 프롬프트 캐시를 유지하고 다시 바꾸기 전까지 새 노력 수준으로 사용돼요. `tools`를 편집하지 마세요. 세션 시작 시 전체 집합을 선언하고 아직 사용 불가한 도구는 `defer_loading: true`로 표시한 뒤, `tool_addition`과 `tool_removal` 블록으로 제공하거나 회수하세요. 도구 스키마를 세션 중간에야 알게 됐다면 `tool_addition` 블록 안에 정의하고(`inline-tools-2026-09-15`, API의 MCP 커넥터가 닿는 서버라면 `mcp-client-2026-09-15` 추가) `tools`는 그대로 두세요. 이 블록들을 담는 `role: "system"` 메시지는 이후 thinking의 접두사에 합류하므로, 나중에 이동·재작성·삭제하지 마세요. [`tool_addition`과 `tool_removal`로 도구 추가·제거하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#tool-changes)를 참고하세요. 예, API가 요약을 쓴다면요. [온디맨드 컴팩션](https://platform.claude.com/docs/en/build-with-claude/compaction-on-demand)(베타 헤더 `compact-2026-09-04`)은 이전 턴을 그 대신 보내는 서명 블록으로 요약해요. 최근 턴은 [유지된 thinking 조건](https://platform.claude.com/docs/en/build-with-claude/compaction-thinking-blocks#conditions-for-kept-thinking-to-stay-valid) 아래에서 thinking을 유지해요.
스스로 요약을 쓴다면 유지된 턴의 thinking은 확인에 실패해요. 그 블록들이 교체 이전의 기록에 대해 만들어졌기 때문이에요. 가져갈 턴에서 `thinking`과 `redacted_thinking` 블록을 벗겨내고 `text`와 `tool_use` 블록을 유지하거나, `prefix_mismatch_behavior: "drop_block"`을 보내 API가 버리게 하세요. 단순 컴팩션은 실패할 thinking을 남기지 않으며 권장되는 방식이에요. 요약 메시지 하나와 다음 사용자 턴, 그리고 이전 턴은 재생되지 않는 것이죠. 서버 측 [컴팩션](https://platform.claude.com/docs/en/build-with-claude/compaction)과 [컨텍스트 편집](https://platform.claude.com/docs/en/build-with-claude/context-editing)은 편집으로 간주되지 않아요. [클라이언트에서 컴팩션하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#custom-compaction-on-the-client)를 참고하세요.
세션 시작 시 한 번 로드하고 최상위 `system` 프롬프트와 `tools`를 고정하세요. 파일이 바뀌면 원본을 편집하지 않고 `messages`의 그 지점에 새 버전을 추가하세요. 운영자로서 오는 지시에는 [대화 중간 시스템 메시지](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages)를 사용해요. 시스템 프롬프트 권위를 지녀서는 안 되는 신뢰하지 않는 파일 텍스트는 내용을 다음 `user` 턴에 넣으세요. [대화 중간 시스템 메시지로 지침 추가하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#new-instructions)와 [제한 사항](https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages#limitations)을 참고하세요. 예. 재개된 세션은 평범한 후속 요청이에요. `system`, `tools`, 이전 `messages`가 마지막으로 보낸 것과 같은 내용이어야 해요. JSON 형식이나 키 순서는 중요하지 않아요. 값이 중요해요. 보내고 받은 것을 정확히 저장하고 그것을 재생하세요. 렌더링된 시스템 프롬프트, 도구 정의, 반환된 그대로의 각 어시스턴트 턴. 날짜, 업데이트된 지시 파일, 새 도구 버전처럼 그 사이에 바뀌었을 수 있는 입력에서 다시 렌더링하지 마세요. 새로운 것은 모두 추가된 메시지로 넣어요. [어시스턴트 턴을 반환된 그대로 다시 보내기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#append-assistant-turns-exactly-as-returned)를 참고하세요. 저장된 기록에 편집이 있으므로 재생은 성공할 수 없어요. 그 세션을 지금부터 `prefix_mismatch_behavior: "drop_block"`으로 보내거나, 그 `thinking`과 `redacted_thinking` 블록을 한 번 제거하고 계속하세요. 그 시점부터 모델이 만드는 thinking은 그 앞의 어떤 것이 다시 바뀌지 않는 한 유효하게 유지돼요. 그런 다음 편집을 찾아 새 세션이 그것을 겪지 않게 하세요. [코드에서 오류 처리하기](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#handle-the-error-in-code)를 참고하세요. 아니요. 기존 기록 뒤에 추가되고 앞의 어떤 것도 바뀌지 않는다면요. thinking 블록이 없는 어시스턴트 메시지는 다른 어떤 추가된 메시지와 같아요. 다른 모델의 출력을 `text`와 `tool_use` 내용으로 보내세요. 다른 대화로는 가져갈 수 없어요. thinking 블록은 그것이 만들어진 정확한 `system`, `tools`, `messages` 뒤에서만 사용할 수 있어요. 분기점까지 그 기록을 그대로 재생하는 분기는 thinking을 유지해요. 다른 것으로 시작하는 대화는 그것을 쓸 수 없어요. 그런 대화는 [단순 컴팩션](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking#custom-compaction-on-the-client)처럼 과제 상태 요약에서 시작하세요. 목표, 내린 결정, 지금까지의 파일과 결과, 다음 단계.

다음 단계 (Next steps)

가장 흔한 thinking 실패를 진단하고 고쳐 보세요: 설정 400 오류, 비어 있거나 없는 thinking 블록, max\_tokens 정지, 캐시 미스. 앞선 캐시된 접두사를 무효로 만들지 않고 대화 도중 시스템 지시나 도구 가용성을 바꾸는 법. 컨텍스트 창 한계에 가까워지는 긴 대화를 관리하기 위한 서버 측 컨텍스트 컴팩션. `cache_control`로 프롬프트 접두사를 캐시해 비용과 지연을 줄이기. 자동 캐싱 또는 5분·1시간 TTL의 명시적 브레이크포인트 사용.

더 알아보기 (Learn more)