캐시 진단

캐시 진단 (Cache diagnostics)

연속된 요청을 비교해서 프롬프트 캐시가 예상치 못하게 빗나간 이유를 정확히 진단할 수 있어요. 캐시 미스가 났을 때 캐시 읽기 토큰이 0으로 떨어지는 것만 보는 대신, 왜 어디서 프롬프트 접두사가 갈라졌는지 알려줘요. 프롬프트가 바이트 단위로 똑같아야만 캐시가 적용되기 때문이에요.

출처: 문서

본문

프롬프트 캐싱은 프롬프트 앞부분이 최근 요청과 바이트 단위로 똑같을 때만 레이턴시와 비용을 크게 줄여줘요. 도구 순서를 바꾸거나, 시스템 프롬프트에 타임스탬프를 끼워 넣거나, 이전 메시지를 편집하면 조용히 캐시가 무효화될 수 있어요. 캐시 진단이 없다면 보이는 신호는 usage.cache_read_input_tokens가 0으로 떨어지는 것뿐이고, 무엇이 바뀌었는지는 알 수 없어요.

캐시 진단은 그 빈틈을 메워줘요. 이전 응답의 id를 넘기면 API가 두 요청을 비교해서 어디서 갈라졌는지(모델, 시스템 프롬프트, 도구, 메시지 기록) 알려줘요. 그래서 추측 대신 근본 원인을 고칠 수 있어요.

캐시 진단이 동작하는 방식

베타 헤더가 있으면 API는 각 요청의 가벼운 지문을 응답 id를 키로 저장해요. 다음 요청에서 그 id를 diagnostics.previous_message_id로 넘기면, API는 새 요청의 지문을 다시 만들어 저장된 것과 비교하고, 첫 번째 분기 지점을 설명하는 diagnostics 객체를 응답에 붙여줘요.

비교는 캐시가 실제로 적중했는지와 무관하게 요청 구조에 관한 거예요. diagnostics 결과를 usage.cache_read_input_tokens와 어떻게 조합하는지는 reading diagnostics alongside usage를 보세요.

지문에는 해시와 토큰 수 추정치만 들어 있고(원시 프롬프트 내용은 절대 없어요), 제한된 시간 동안 보관되며 조직과 워크스페이스 범위로 한정돼요. 다른 용도로는 쓰이지 않아요.

기본 사용법

매 턴마다 베타 헤더를 보내요. 첫 턴에서는 "previous_message_id": null을 넘겨 비교할 이전 메시지 없이 옵트인해요. 이후 턴에서는 이전 응답의 id를 넘겨요.

```bash cURL # Turn 1: establish the cache and opt in to diagnostics response=$(curl -sS --fail-with-body https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: cache-diagnosis-2026-04-07" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are an AI assistant analyzing a large document. ...", "messages": [{"role": "user", "content": "Summarize section 1."}], "diagnostics": {"previous_message_id": null} }') jq '{id, diagnostics}' <<< "$response" message_id=$(jq -r '.id' <<< "$response")

Turn 2: reference the previous turn so the API can compare prefixes

curl -sS --fail-with-body https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: cache-diagnosis-2026-04-07"
-H "content-type: application/json"
-d @- <<EOF | jq '{id, diagnostics}' # diagnostics: null means no divergence was found { "model": "claude-opus-5-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "system": "You are an AI assistant analyzing a large document. ...", "messages": [ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": "Section 1 covers..."}, {"role": "user", "content": "Now summarize section 2."} ], "diagnostics": {"previous_message_id": "$message_id"} } EOF


```bash CLI
# Turn 1
turn1=$(ant beta:messages create \
  --beta cache-diagnosis-2026-04-07 \
  --transform '{id,usage,diagnostics}' <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
cache_control:
  type: ephemeral
system: "You are an AI assistant analyzing a large document. <document>...</document>"
messages:
  - role: user
    content: Summarize section 1.
diagnostics:
  previous_message_id: null
YAML
)
printf '%s\n' "$turn1"

# Turn 2: pass the id from turn 1 as previous_message_id
message_id=$(jq -r '.id' <<<"$turn1")
ant beta:messages create \
  --beta cache-diagnosis-2026-04-07 \
  --transform '{id,usage,diagnostics}' <<YAML
model: claude-opus-5-5
max_tokens: 1024
cache_control:
  type: ephemeral
system: "You are an AI assistant analyzing a large document. <document>...</document>"
messages:
  - role: user
    content: Summarize section 1.
  - role: assistant
    content: Section 1 covers...
  - role: user
    content: Now summarize section 2.
diagnostics:
  previous_message_id: $message_id
YAML
client = anthropic.Anthropic()

SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

# Turn 1: opt in with previous_message_id=None
r1 = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM,
    messages=[{"role": "user", "content": "Summarize section 1."}],
    diagnostics={"previous_message_id": None},
    betas=["cache-diagnosis-2026-04-07"],
)

# Turn 2: reference the previous response id
r2 = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM,
    messages=[
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": r1.content},
        {"role": "user", "content": "Now summarize section 2."},
    ],
    diagnostics={"previous_message_id": r1.id},
    betas=["cache-diagnosis-2026-04-07"],
)

diagnostics = r2.diagnostics
if diagnostics is None:
    print("No divergence detected.")
elif diagnostics.cache_miss_reason is None:
    print("Comparison still pending.")
else:
    print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}")
const client = new Anthropic();

const SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>";

// Turn 1: opt in with previous_message_id: null
const r1 = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  cache_control: { type: "ephemeral" },
  system: SYSTEM,
  messages: [{ role: "user", content: "Summarize section 1." }],
  diagnostics: { previous_message_id: null },
  betas: ["cache-diagnosis-2026-04-07"]
});

// Turn 2: reference the previous response id
const r2 = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  cache_control: { type: "ephemeral" },
  system: SYSTEM,
  messages: [
    { role: "user", content: "Summarize section 1." },
    { role: "assistant", content: r1.content },
    { role: "user", content: "Now summarize section 2." }
  ],
  diagnostics: { previous_message_id: r1.id },
  betas: ["cache-diagnosis-2026-04-07"]
});

if (r2.diagnostics === null) {
  console.log("No divergence detected.");
} else if (r2.diagnostics.cache_miss_reason === null) {
  console.log("Comparison still pending.");
} else {
  console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`);
}
AnthropicClient client = new();

var system = "You are an AI assistant analyzing a large document. <document>...</document>";

var r1 = await client.Beta.Messages.Create(
    new()
    {
        Model = Messages::Model.ClaudeOpus5_5,
        MaxTokens = 1024,
        CacheControl = new(),
        System = system,
        Messages =
        [
            new() { Role = Role.User, Content = "Summarize section 1." },
        ],
        Diagnostics = new() { PreviousMessageID = null },
        Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
    }
);

var r2 = await client.Beta.Messages.Create(
    new()
    {
        Model = Messages::Model.ClaudeOpus5_5,
        MaxTokens = 1024,
        CacheControl = new(),
        System = system,
        Messages =
        [
            new() { Role = Role.User, Content = "Summarize section 1." },
            new()
            {
                Role = Role.Assistant,
                Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
            },
            new() { Role = Role.User, Content = "Now summarize section 2." },
        ],
        Diagnostics = new() { PreviousMessageID = r1.ID },
        Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
    }
);

Console.WriteLine(r2.Diagnostics switch
{
    null => "No divergence detected.",
    { CacheMissReason: null } => "Comparison still pending.",
    { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}",
});
client := anthropic.NewClient()
ctx := context.Background()

system := []anthropic.BetaTextBlockParam{
	{Text: "You are an AI assistant analyzing a large document. <document>...</document>"},
}

r1, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
	Model:        anthropic.ModelClaudeOpus5_5,
	MaxTokens:    1024,
	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
	System:       system,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
	},
	Diagnostics: anthropic.BetaDiagnosticsParam{
		PreviousMessageID: param.Null[string](),
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
})
if err != nil {
	panic(err)
}

r2, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
	Model:        anthropic.ModelClaudeOpus5_5,
	MaxTokens:    1024,
	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
	System:       system,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
		r1.ToParam(),
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")),
	},
	Diagnostics: anthropic.BetaDiagnosticsParam{
		PreviousMessageID: anthropic.String(r1.ID),
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
})
if err != nil {
	panic(err)
}

switch {
case !r2.JSON.Diagnostics.Valid():
	fmt.Println("No divergence detected.")
case !r2.Diagnostics.JSON.CacheMissReason.Valid():
	fmt.Println("Comparison still pending.")
default:
	fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type)
}
var client = AnthropicOkHttpClient.fromEnv();

var system = "You are an AI assistant analyzing a large document. <document>...</document>";

var r1 = client.beta().messages().create(
    MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        .cacheControl(BetaCacheControlEphemeral.builder().build())
        .system(system)
        .addUserMessage("Summarize section 1.")
        // Pass null on the first turn to opt in without a prior message to compare.
        .diagnostics(BetaDiagnosticsParam.builder().previousMessageId((String) null).build())
        .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
        .build()
);

var r2 = client.beta().messages().create(
    MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        .cacheControl(BetaCacheControlEphemeral.builder().build())
        .system(system)
        .addUserMessage("Summarize section 1.")
        .addMessage(r1)
        .addUserMessage("Now summarize section 2.")
        .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
        .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
        .build()
);

if (r2.diagnostics().isEmpty()) {
    IO.println("No divergence detected.");
} else if (r2.diagnostics().get().cacheMissReason().isEmpty()) {
    IO.println("Comparison still pending.");
} else {
    var reason = r2.diagnostics().get().cacheMissReason().get();
    // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON.
    @SuppressWarnings("unchecked")
    var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
    IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow());
}
$client = new Client();

$system = 'You are an AI assistant analyzing a large document. <document>...</document>';

$r1 = $client->beta->messages->create(
    model: Model::CLAUDE_OPUS_5_5,
    maxTokens: 1024,
    cacheControl: new BetaCacheControlEphemeral,
    system: $system,
    messages: [
        ['role' => 'user', 'content' => 'Summarize section 1.'],
    ],
    diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID(null),
    betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
);

$r2 = $client->beta->messages->create(
    model: Model::CLAUDE_OPUS_5_5,
    maxTokens: 1024,
    cacheControl: new BetaCacheControlEphemeral,
    system: $system,
    messages: [
        ['role' => 'user', 'content' => 'Summarize section 1.'],
        ['role' => 'assistant', 'content' => $r1->content],
        ['role' => 'user', 'content' => 'Now summarize section 2.'],
    ],
    diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
    betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
);

echo match (true) {
    $r2->diagnostics === null => "No divergence detected.\n",
    $r2->diagnostics->cacheMissReason === null => "Comparison still pending.\n",
    default => "cache_miss_reason: {$r2->diagnostics->cacheMissReason->type}\n",
};
client = Anthropic::Client.new

SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

r1 = client.beta.messages.create(
  model: :"claude-opus-5-5",
  max_tokens: 1024,
  cache_control: {type: "ephemeral"},
  system_: SYSTEM,
  messages: [
    {role: "user", content: "Summarize section 1."}
  ],
  diagnostics: {previous_message_id: nil},
  betas: ["cache-diagnosis-2026-04-07"]
)

r2 = client.beta.messages.create(
  model: :"claude-opus-5-5",
  max_tokens: 1024,
  cache_control: {type: "ephemeral"},
  system_: SYSTEM,
  messages: [
    {role: "user", content: "Summarize section 1."},
    {role: "assistant", content: r1.content},
    {role: "user", content: "Now summarize section 2."}
  ],
  diagnostics: {previous_message_id: r1.id},
  betas: ["cache-diagnosis-2026-04-07"]
)

case r2.diagnostics
in nil
  puts "No divergence detected."
in {cache_miss_reason: nil}
  puts "Comparison still pending."
in {cache_miss_reason: {type:}}
  puts "cache_miss_reason: #{type}"
end

스트리밍

스트리밍 응답에서는 diagnostics가 message_start 이벤트에 나타나요.

```bash cURL # Turn 2: stream the response. diagnostics arrives on the message_start event; # a null value means no divergence was found. curl -sS --fail-with-body https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: cache-diagnosis-2026-04-07" \ -H "content-type: application/json" \ -d @- <...", "messages": [ {"role": "user", "content": "Summarize section 1."}, {"role": "assistant", "content": "Section 1 covers..."}, {"role": "user", "content": "Now summarize section 2."} ], "diagnostics": {"previous_message_id": "$message_id"} } EOF ```
# Turn 2: stream. With --stream the CLI emits each SSE event as one JSON object.
# diagnostics arrives on the message_start event; pick it out with jq.
ant beta:messages create \
  --beta cache-diagnosis-2026-04-07 \
  --stream --format jsonl <<YAML |
model: claude-opus-5-5
max_tokens: 1024
cache_control:
  type: ephemeral
system: "You are an AI assistant analyzing a large document. <document>...</document>"
messages:
  - role: user
    content: Summarize section 1.
  - role: assistant
    content: Section 1 covers...
  - role: user
    content: Now summarize section 2.
diagnostics:
  previous_message_id: $message_id
YAML
  jq -c 'select(.type == "message_start") | .message | {id,usage,diagnostics}'
# Turn 2: stream, referencing the previous response id
with client.beta.messages.stream(
    model="claude-opus-5-5",
    max_tokens=1024,
    cache_control={"type": "ephemeral"},
    system=SYSTEM,
    messages=[
        {"role": "user", "content": "Summarize section 1."},
        {"role": "assistant", "content": r1.content},
        {"role": "user", "content": "Now summarize section 2."},
    ],
    diagnostics={"previous_message_id": r1.id},
    betas=["cache-diagnosis-2026-04-07"],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    print()
    r2 = stream.get_final_message()

diagnostics = r2.diagnostics
if diagnostics is None:
    print("No divergence detected.")
elif diagnostics.cache_miss_reason is None:
    print("Comparison still pending.")
else:
    print(f"cache_miss_reason: {diagnostics.cache_miss_reason.type}")
const stream = client.beta.messages.stream({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  cache_control: { type: "ephemeral" },
  system: SYSTEM,
  messages: [
    { role: "user", content: "Summarize section 1." },
    { role: "assistant", content: r1.content },
    { role: "user", content: "Now summarize section 2." }
  ],
  diagnostics: { previous_message_id: r1.id },
  betas: ["cache-diagnosis-2026-04-07"]
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}
process.stdout.write("\n");

// diagnostics arrives on message_start and is carried through to the final message
const r2 = await stream.finalMessage();

if (r2.diagnostics === null) {
  console.log("No divergence detected.");
} else if (r2.diagnostics.cache_miss_reason === null) {
  console.log("Comparison still pending.");
} else {
  console.log(`cache_miss_reason: ${r2.diagnostics.cache_miss_reason.type}`);
}
// Turn 2: stream, referencing the previous response id
BetaDiagnostics? diagnostics = null;

var stream = client.Beta.Messages.CreateStreaming(
    new()
    {
        Model = Messages::Model.ClaudeOpus5_5,
        MaxTokens = 1024,
        CacheControl = new(),
        System = system,
        Messages =
        [
            new() { Role = Role.User, Content = "Summarize section 1." },
            new()
            {
                Role = Role.Assistant,
                Content = r1.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
            },
            new() { Role = Role.User, Content = "Now summarize section 2." },
        ],
        Diagnostics = new() { PreviousMessageID = r1.ID },
        Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
    }
);

await foreach (var streamEvent in stream)
{
    if (streamEvent.TryPickStart(out var start))
    {
        // diagnostics arrives on the message_start event
        diagnostics = start.Message.Diagnostics;
    }
    else if (streamEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickText(out var textDelta))
    {
        Console.Write(textDelta.Text);
    }
}
Console.WriteLine();

Console.WriteLine(diagnostics switch
{
    null => "No divergence detected.",
    { CacheMissReason: null } => "Comparison still pending.",
    { CacheMissReason.Type: var type } => $"cache_miss_reason: {type.GetString()}",
});
// Turn 2: stream, referencing the previous response id
stream := client.Beta.Messages.NewStreaming(ctx, anthropic.BetaMessageNewParams{
	Model:        anthropic.ModelClaudeOpus5_5,
	MaxTokens:    1024,
	CacheControl: anthropic.BetaCacheControlEphemeralParam{},
	System:       system,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Summarize section 1.")),
		r1.ToParam(),
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Now summarize section 2.")),
	},
	Diagnostics: anthropic.BetaDiagnosticsParam{
		PreviousMessageID: anthropic.String(r1.ID),
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
})
defer stream.Close()

// diagnostics arrives on message_start; Accumulate carries it into r2
var r2 anthropic.BetaMessage
for stream.Next() {
	if err := r2.Accumulate(stream.Current()); err != nil {
		panic(err)
	}
}
if err := stream.Err(); err != nil {
	panic(err)
}

switch {
case !r2.JSON.Diagnostics.Valid():
	fmt.Println("No divergence detected.")
case !r2.Diagnostics.JSON.CacheMissReason.Valid():
	fmt.Println("Comparison still pending.")
default:
	fmt.Printf("cache_miss_reason: %s\n", r2.Diagnostics.CacheMissReason.Type)
}
// Turn 2: stream, referencing the previous response id
var params = MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024)
    .cacheControl(BetaCacheControlEphemeral.builder().build())
    .system(system)
    .addUserMessage("Summarize section 1.")
    .addMessage(r1)
    .addUserMessage("Now summarize section 2.")
    .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(r1.id()).build())
    .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
    .build();

var accumulator = BetaMessageAccumulator.create();
try (var streamResponse = client.beta().messages().createStreaming(params)) {
    streamResponse.stream()
        .peek(accumulator::accumulate)
        .flatMap(event -> event.contentBlockDelta().stream())
        .flatMap(deltaEvent -> deltaEvent.delta().text().stream())
        .forEach(textDelta -> IO.print(textDelta.text()));
    IO.println("");
}

// diagnostics arrives on message_start and is carried through to the accumulated message
var diagnostics = accumulator.message().diagnostics();
if (diagnostics.isEmpty()) {
    IO.println("No divergence detected.");
} else if (diagnostics.get().cacheMissReason().isEmpty()) {
    IO.println("Comparison still pending.");
} else {
    var reason = diagnostics.get().cacheMissReason().get();
    // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON.
    @SuppressWarnings("unchecked")
    var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
    IO.println("cache_miss_reason: " + json.get("type").asStringOrThrow());
}
// Turn 2: stream, referencing the previous response id
$stream = $client->beta->messages->createStream(
    model: Model::CLAUDE_OPUS_5_5,
    maxTokens: 1024,
    cacheControl: new BetaCacheControlEphemeral,
    system: $system,
    messages: [
        ['role' => 'user', 'content' => 'Summarize section 1.'],
        ['role' => 'assistant', 'content' => $r1->content],
        ['role' => 'user', 'content' => 'Now summarize section 2.'],
    ],
    diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($r1->id),
    betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
);

$diagnostics = null;
foreach ($stream as $event) {
    switch (true) {
        case $event instanceof \Anthropic\Beta\Messages\BetaRawMessageStartEvent:
            // diagnostics arrives on the message_start event's embedded BetaMessage
            $diagnostics = $event->message->diagnostics;
            break;
        case $event instanceof \Anthropic\Beta\Messages\BetaRawContentBlockDeltaEvent:
            if ($event->delta instanceof \Anthropic\Beta\Messages\BetaTextDelta) {
                echo $event->delta->text;
            }
            break;
    }
}
echo PHP_EOL;

echo match (true) {
    $diagnostics === null => "No divergence detected.\n",
    $diagnostics->cacheMissReason === null => "Comparison still pending.\n",
    default => "cache_miss_reason: {$diagnostics->cacheMissReason->type}\n",
};
# Turn 2: stream, referencing the previous response id
stream = client.beta.messages.stream(
  model: :"claude-opus-5-5",
  max_tokens: 1024,
  cache_control: {type: "ephemeral"},
  system_: SYSTEM,
  messages: [
    {role: "user", content: "Summarize section 1."},
    {role: "assistant", content: r1.content},
    {role: "user", content: "Now summarize section 2."}
  ],
  diagnostics: {previous_message_id: r1.id},
  betas: ["cache-diagnosis-2026-04-07"]
)

stream.each do |event|
  print(event.text) if event.is_a?(Anthropic::Streaming::TextEvent)
end
puts

# diagnostics arrives on message_start and is retained on the accumulated message
r2 = stream.accumulated_message

case r2.diagnostics
in nil
  puts "No divergence detected."
in {cache_miss_reason: nil}
  puts "Comparison still pending."
in {cache_miss_reason: {type:}}
  puts "cache_miss_reason: #{type}"
end

message_start 이벤트는 전체 diagnostics 필드를 담아요. 가능한 값은 response format을 보세요.

대화 루프에 진단을 관통시키기

다중 턴 대화에서는 최신 응답 id를 매 턴 previous_message_id로 이월해요. 첫 번째 반복은 null을 넘겨 옵트인하고, 이후 각 반복은 이전 응답의 id를 넘겨요.

This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the loop pattern; the per-turn HTTP request is identical to [Basic usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#basic-usage). This workflow doesn't translate well to a one-off shell command. See the SDK tabs for the loop pattern; the per-turn CLI invocation is identical to [Basic usage](https://platform.claude.com/docs/en/build-with-claude/cache-diagnostics#basic-usage). ```python client = anthropic.Anthropic()
SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

messages = []
prev_id = None

for i, user_message in enumerate(
    ["Summarize section 1.", "Now section 2.", "Now section 3."]
):
    messages.append({"role": "user", "content": user_message})

    r = client.beta.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        cache_control={"type": "ephemeral"},
        system=SYSTEM,
        messages=messages,
        diagnostics={"previous_message_id": prev_id},
        betas=["cache-diagnosis-2026-04-07"],
    )

    if r.diagnostics is not None and r.diagnostics.cache_miss_reason is not None:
        print(f"Turn {i + 1} cache_miss_reason: {r.diagnostics.cache_miss_reason.type}")

    messages.append({"role": "assistant", "content": r.content})
    prev_id = r.id
```
```typescript const client = new Anthropic();
const SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>";

const prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."];

const messages: BetaMessageParam[] = [];
let prevId: string | null = null;

for (const [i, prompt] of prompts.entries()) {
  messages.push({ role: "user", content: prompt });

  const r: BetaMessage = await client.beta.messages.create({
    model: "claude-opus-5-5",
    max_tokens: 1024,
    cache_control: { type: "ephemeral" },
    system: SYSTEM,
    messages,
    diagnostics: { previous_message_id: prevId },
    betas: ["cache-diagnosis-2026-04-07"]
  });

  if (r.diagnostics?.cache_miss_reason) {
    console.log(`Turn ${i + 1} cache_miss_reason: ${r.diagnostics.cache_miss_reason.type}`);
  }

  messages.push({ role: "assistant", content: r.content });
  prevId = r.id;
}
```
```csharp AnthropicClient client = new();
var system = "You are an AI assistant analyzing a large document. <document>...</document>";

List<BetaMessageParam> messages = [];
string? prevId = null;
string[] prompts = ["Summarize section 1.", "Now section 2.", "Now section 3."];

for (int i = 0; i < prompts.Length; i++)
{
    messages.Add(new() { Role = Role.User, Content = prompts[i] });

    var r = await client.Beta.Messages.Create(
        new()
        {
            Model = Messages::Model.ClaudeOpus5_5,
            MaxTokens = 1024,
            CacheControl = new(),
            System = system,
            Messages = messages,
            Diagnostics = new() { PreviousMessageID = prevId },
            Betas = [AnthropicBeta.CacheDiagnosis2026_04_07],
        }
    );

    if (r.Diagnostics?.CacheMissReason is { Type: var type })
    {
        Console.WriteLine($"Turn {i + 1} cache_miss_reason: {type.GetString()}");
    }

    messages.Add(
        new()
        {
            Role = Role.Assistant,
            Content = r.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
        }
    );
    prevId = r.ID;
}
```
```go client := anthropic.NewClient() ctx := context.Background()
system := []anthropic.BetaTextBlockParam{
	{Text: "You are an AI assistant analyzing a large document. <document>...</document>"},
}

prompts := []string{"Summarize section 1.", "Now section 2.", "Now section 3."}

var messages []anthropic.BetaMessageParam
prevID := param.Null[string]()

for turn, prompt := range prompts {
	messages = append(messages, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(prompt)))

	r, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
		Model:        anthropic.ModelClaudeOpus5_5,
		MaxTokens:    1024,
		CacheControl: anthropic.BetaCacheControlEphemeralParam{},
		System:       system,
		Messages:     messages,
		Diagnostics: anthropic.BetaDiagnosticsParam{
			PreviousMessageID: prevID,
		},
		Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCacheDiagnosis2026_04_07},
	})
	if err != nil {
		panic(err)
	}

	if r.JSON.Diagnostics.Valid() && r.Diagnostics.JSON.CacheMissReason.Valid() {
		fmt.Printf("Turn %d cache_miss_reason: %s\n", turn+1, r.Diagnostics.CacheMissReason.Type)
	}

	messages = append(messages, r.ToParam())
	prevID = anthropic.String(r.ID)
}
```
```java var client = AnthropicOkHttpClient.fromEnv();
var system = "You are an AI assistant analyzing a large document. <document>...</document>";
var prompts = List.of("Summarize section 1.", "Now section 2.", "Now section 3.");

var messages = new ArrayList<BetaMessageParam>();
String prevId = null;

for (var turn = 0; turn < prompts.size(); turn++) {
    messages.add(
        BetaMessageParam.builder()
            .role(BetaMessageParam.Role.USER)
            .content(prompts.get(turn))
            .build()
    );

    var r = client.beta().messages().create(
        MessageCreateParams.builder()
            .model(Model.CLAUDE_OPUS_5_5)
            .maxTokens(1024)
            .cacheControl(BetaCacheControlEphemeral.builder().build())
            .system(system)
            .messages(messages)
            .diagnostics(BetaDiagnosticsParam.builder().previousMessageId(prevId).build())
            .addBeta(AnthropicBeta.CACHE_DIAGNOSIS_2026_04_07)
            .build()
    );

    if (r.diagnostics().isPresent() && r.diagnostics().get().cacheMissReason().isPresent()) {
        var reason = r.diagnostics().get().cacheMissReason().get();
        // CacheMissReason doesn't expose a typed .type() accessor; read it from the raw JSON.
        @SuppressWarnings("unchecked")
        var json = (Map<String, JsonValue>) reason._json().orElseThrow().asObject().orElseThrow();
        IO.println("Turn " + (turn + 1) + " cache_miss_reason: " + json.get("type").asStringOrThrow());
    }

    messages.add(r.toParam());
    prevId = r.id();
}
```
```php $client = new Client();
$system = 'You are an AI assistant analyzing a large document. <document>...</document>';

$messages = [];
$prevId = null;

foreach (['Summarize section 1.', 'Now section 2.', 'Now section 3.'] as $i => $userMsg) {
    $turn = $i + 1;
    $messages[] = ['role' => 'user', 'content' => $userMsg];

    $r = $client->beta->messages->create(
        model: Model::CLAUDE_OPUS_5_5,
        maxTokens: 1024,
        cacheControl: new BetaCacheControlEphemeral,
        system: $system,
        messages: $messages,
        diagnostics: (new BetaDiagnosticsParam)->withPreviousMessageID($prevId),
        betas: [AnthropicBeta::CACHE_DIAGNOSIS_2026_04_07],
    );

    if ($r->diagnostics?->cacheMissReason !== null) {
        echo "Turn {$turn} cache_miss_reason: {$r->diagnostics->cacheMissReason->type}\n";
    }

    $messages[] = ['role' => 'assistant', 'content' => $r->content];
    $prevId = $r->id;
}
```
```ruby client = Anthropic::Client.new
SYSTEM = "You are an AI assistant analyzing a large document. <document>...</document>"

messages = []
prev_id = nil

["Summarize section 1.", "Now section 2.", "Now section 3."].each_with_index do |user_msg, i|
  messages << {role: "user", content: user_msg}

  r = client.beta.messages.create(
    model: :"claude-opus-5-5",
    max_tokens: 1024,
    cache_control: {type: "ephemeral"},
    system_: SYSTEM,
    messages: messages,
    diagnostics: {previous_message_id: prev_id},
    betas: ["cache-diagnosis-2026-04-07"]
  )

  if (reason = r.diagnostics&.cache_miss_reason)
    puts "Turn #{i + 1} cache_miss_reason: #{reason.type}"
  end

  messages << {role: "assistant", content: r.content}
  prev_id = r.id
end
```

응답 형식

응답 Message의 diagnostics 필드는 네 가지 상태가 있어요.

값 의미
필드 없음 요청에 diagnostics가 없었거나 베타 헤더가 빠졌어요.
null previous_message_id가 null이었거나(첫 턴, 비교할 것이 없음), 비교가 실행됐는데 분기점이 없었어요.
{"cache_miss_reason": null} 응답이 직렬화될 때 비교가 아직 실행 중이었어요. 응답이 매우 빨리 시작될 때 발생할 수 있어요. 결론을 내리지 말고 다음 턴을 확인하세요.
{"cache_miss_reason": {...}} cache_miss_reason이 붙었어요. *_changed 유형은 첫 번째 분기 지점을 알려주고, previous_message_not_found와 unavailable은 비교 결과가 없었던 경우예요.

cache_miss_reason이 null이 아니면 이런 모양이에요.

{
  "id": "msg_01Xyz...",
  "type": "message",
  "role": "assistant",
  "content": [{ "type": "text", "text": "..." }],
  "usage": {
    "input_tokens": 42,
    "cache_read_input_tokens": 0,
    "cache_creation_input_tokens": 41850,
    "output_tokens": 210
  },
  "diagnostics": {
    "cache_miss_reason": {
      "type": "system_changed",
      "cache_missed_input_tokens": 41850
    }
  }
}

캐시 미스 원인 유형

cache_miss_reason은 type에 따라 갈라지는 판별 유니온이에요. 응답은 가장 이른 분기점만 보고하므로 그것부터 고쳐요. 그 뒤에 있는 것들은 가려질 수 있어요.

유형 의미 바꿀 것
model_changed model이 이전 요청과 달라요(예: 라우터, A/B 테스트, 폴백이 다른 모델을 선택). 캐시는 모델별이에요. 캐시된 대화 안에서 모델을 일정하게 유지하세요.
system_changed system 매개변수가 달라요. 보통 타임스탬프, 요청 ID, 기타 요청별 값이 시스템 프롬프트에 끼워 넣어졌기 때문이에요. 시스템 프롬프트를 바이트 안정적인 상수로 만들고, 동적 데이터는 캐시 중단점 뒤의 첫 번째 user 메시지로 옮기세요.
tools_changed tools 배열이 달라요. 턴 사이에 도구가 추가·제거·재정렬됐거나, 도구 input_schema JSON이 비결정적으로 직렬화됐어요. 매 턴 같은 도구 목록을 고정 순서로, 결정적으로 직렬화된 스키마(예: 키 정렬)로 보내세요.
messages_changed 모델·시스템·도구가 모두 같은데 messages의 이전 항목이 추가 대신 변경·재정렬·제거됐어요. 보통 대화 기록이 잘렸거나 편집됐거나, 도우미 턴과 tool_result 블록이 재전송 시 다르게 재직렬화됐어요. 기록을 append-only로 다루고, 도우미 content와 도구 결과를 그대로 에코하세요.
previous_message_not_found 제공한 previous_message_id에 대한 저장된 지문이 없어요. 요청이 바뀌었다는 증거는 아니에요. 보통 이전 요청이 베타 헤더를 안 썼거나, 다른 워크스페이스에서 왔거나, 보낸 지 너무 오래 지났어요. 매 턴 베타 헤더를 보내고 연속 턴을 시간상 가깝게 유지하세요.
unavailable 이 요청에 대해 진단 정보를 쓸 수 없었어요. model, system, tools가 같은데 다른 프롬프트 영향 요청 매개변수(tool_choice, thinking, context_management, output_config, output_format, 또는 활성 anthropic-beta 헤더 집합)가 다르거나, 분기가 비교 지평선을 넘는 아주 긴 대화가 그 예시예요. 요청은 정상 처리됐어요. 캐시된 대화 수명 동안 프롬프트 영향 요청 매개변수를 일정하게 유지하세요. 지속되면 프롬프트 캐싱 페이지의 troubleshooting common issues 아래 수동 점검을 적용하세요.
네 가지 `*_changed` 유형은 `cache_missed_input_tokens` 정수도 함께 실어요. 분기점 뒤로 떨어진 입력 토큰 수의 추정치로, 캐시 가능한 접두사가 얼마나 손실됐는지 감을 줘요. 토큰화 전 바이트 길이에서 유도되므로 규모 지표로 다루지 청구 숫자로 다루지 마세요. `usage.input_tokens`와 다를 수 있고(가끔 더 클 수도 있어요).

usage와 함께 진단 읽기

diagnostics는 "요청이 바뀌었나?"에 답하고, usage.cache_read_input_tokens는 "캐시가 적중했나?"에 답해요. 둘을 합치면 어디를 봐야 할지 알 수 있어요.

이 표는 실제 previous_message_id를 넘긴 턴에 적용돼요. 첫 턴(previous_message_id: null)에서는 diagnostics가 항상 null이고, 캐시가 읽히는 게 아니라 쓰이는 중이라 cache_read_input_tokens는 보통 0이에요. 문제 해결이 필요 없어요. cache_miss_reason이 null(비교 아직 진행 중, 다음 턴 확인)이거나 type이 previous_message_not_found·unavailable(비교 결과 없음)일 때도 표가 적용되지 않아요.

진단 결과 캐시 읽기 토큰 해석
null 높음 정상 동작이에요. 접두사가 안정적이고 캐시가 적중했어요.
null 낮음 또는 0 요청은 같지만 캐시 항목을 더는 쓸 수 없었어요. 턴 간 간격을 줄이거나 1시간 캐시 TTL을 사용하세요.
cache_miss_reason이 *_changed 유형 낮음 또는 0 문제는 여러분 쪽이에요. 요청이 바뀌었으니 type이 가리키는 원인을 고치세요.
cache_miss_reason이 *_changed 유형 높음 드물어요. 프롬프트 늦은 부분에서 변화가 일어났지만 이전 cache_control 중단점이 여전히 적중했어요. 고칠 가치가 있지만 영향은 작아요.

제약 사항

  • 베타: 이 기능이 베타인 동안 필드 이름과 의미는 바뀔 수 있어요.
  • Claude API 전용: Amazon Bedrock이나 Google Cloud에는 없어요.
  • 제한된 보관: previous_message_id 조회용 지문은 짧은 시간 후 만료돼요. 진단 비교는 시간이 가까운 요청 사이에서 실행하세요.
  • 같은 워크스페이스: 이전 요청은 같은 조직과 워크스페이스에서 실행됐어야 해요. 확인하려면 두 응답의 anthropic-workspace-id 응답 헤더를 비교하세요.
  • 비교 지평선: 메시지 목록 깊은 곳에서만 바뀐 아주 긴 대화는 정확한 위치 대신 unavailable로 응답할 수 있어요.
  • 최선 노력: 진단은 요청을 막거나 실패시키지 않아요. 진단 정보를 쓸 수 없으면 unavailable을, 비교가 아직 진행 중이면 cache_miss_reason: null을 반환해요.

데이터 보존

캐시 진단은 ZDR 자격이 있어요(적격). Anthropic은 이 기능을 위해 프롬프트 원문이나 Claude 출력을 저장하지 않아요.

각 요청에 저장되는 지문은 암호화 해시와 토큰 수 추정치뿐이며, 응답 id를 키로 하고 조직과 워크스페이스 범위로 한정돼요. 지문은 짧은 시간 후 만료되고 다른 용도로 쓰이지 않아요.

모든 기능의 ZDR 자격에 대해서는 API and data retention을 보세요.

더 알아보기 (Learn more)