feat(translator): support cache write tokens in claude responses

- Extract cache write tokens from OpenAI and Codex usage details.
- Map cache write tokens to Claude `cache_creation_input_tokens` for streaming and non-streaming responses.

Closes: #4262
This commit is contained in:
Luis Pater
2026-09-01 19:56:40 +08:00
parent 17a65ee547
commit 893abbabc2
4 changed files with 394 additions and 14 deletions

View File

@@ -157,12 +157,15 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
output = append(output, stopCodexTextBlock(params)...)
template, _ = sjson.SetBytes(template, "delta.stop_reason", mapCodexStopReasonToClaude(codexStopReason(responseData), params.HasEmittedToolUse))
template = setClaudeStopSequence(template, "delta.stop_sequence", responseData)
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractResponsesUsage(responseData.Get("usage"))
template, _ = sjson.SetBytes(template, "usage.input_tokens", inputTokens)
template, _ = sjson.SetBytes(template, "usage.output_tokens", outputTokens)
if cachedTokens > 0 {
template, _ = sjson.SetBytes(template, "usage.cache_read_input_tokens", cachedTokens)
}
if cacheWriteTokens > 0 {
template, _ = sjson.SetBytes(template, "usage.cache_creation_input_tokens", cacheWriteTokens)
}
output = translatorcommon.AppendSSEEventBytes(output, "message_delta", template, 2)
output = translatorcommon.AppendSSEEventBytes(output, "message_stop", []byte(`{"type":"message_stop"}`), 2)
@@ -361,12 +364,15 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`)
out, _ = sjson.SetBytes(out, "id", responseData.Get("id").String())
out, _ = sjson.SetBytes(out, "model", responseData.Get("model").String())
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractResponsesUsage(responseData.Get("usage"))
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
if cachedTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
}
if cacheWriteTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_creation_input_tokens", cacheWriteTokens)
}
hasToolCall := false
webSearchSeen := make(map[string]struct{})
@@ -794,14 +800,18 @@ func resolveCodexClaudeToolUseName(originalRequestRawJSON []byte, name string) s
return name
}
func extractResponsesUsage(usage gjson.Result) (int64, int64, int64) {
func extractResponsesUsage(usage gjson.Result) (int64, int64, int64, int64) {
if !usage.Exists() || usage.Type == gjson.Null {
return 0, 0, 0
return 0, 0, 0, 0
}
inputTokens := usage.Get("input_tokens").Int()
outputTokens := usage.Get("output_tokens").Int()
cachedTokens := usage.Get("input_tokens_details.cached_tokens").Int()
cacheWriteTokens := usage.Get("input_tokens_details.cache_write_tokens").Int()
if cacheWriteTokens == 0 {
cacheWriteTokens = usage.Get("input_tokens_details.cache_creation_tokens").Int()
}
if cachedTokens > 0 {
if inputTokens >= cachedTokens {
@@ -811,7 +821,7 @@ func extractResponsesUsage(usage gjson.Result) (int64, int64, int64) {
}
}
return inputTokens, outputTokens, cachedTokens
return inputTokens, outputTokens, cachedTokens, cacheWriteTokens
}
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.

View File

@@ -3,6 +3,7 @@ package claude
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
@@ -1328,3 +1329,196 @@ func firstClaudeStreamPayloadForEvent(output, event string) (gjson.Result, bool)
}
return gjson.Result{}, false
}
func TestConvertCodexResponseToClaude_StreamPreservesCacheWriteUsage(t *testing.T) {
tests := []struct {
name string
terminalUsageJSON string
wantInputTokens int64
wantOutputTokens int64
wantCacheReadTokens int64
wantCacheWriteTokens int64
}{
{
name: "cache_write_tokens field",
terminalUsageJSON: `{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cache_creation_tokens field alias",
terminalUsageJSON: `{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_creation_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cached_tokens greater than input_tokens clamps input_tokens to zero",
terminalUsageJSON: `{"input_tokens":500,"output_tokens":100,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":50}}`,
wantInputTokens: 0,
wantOutputTokens: 100,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 50,
},
{
name: "zero cache_write_tokens does not emit cache_creation_input_tokens",
terminalUsageJSON: `{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":0}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
originalRequest := []byte(`{"messages":[]}`)
var param any
chunks := [][]byte{
[]byte(`data: {"type":"response.created","response":{"id":"resp_1","model":"gpt-5"}}`),
[]byte(`data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"ok"}]}}`),
[]byte(fmt.Sprintf(`data: {"type":"response.completed","response":{"stop_reason":"stop","usage":%s}}`, tt.terminalUsageJSON)),
}
var outputs [][]byte
for _, chunk := range chunks {
outputs = append(outputs, ConvertCodexResponseToClaude(ctx, "", originalRequest, nil, chunk, &param)...)
}
delta, ok := findClaudeStreamMessageDelta(outputs)
if !ok {
t.Fatalf("missing message_delta event; outputs=%q", outputs)
}
usage := delta.Get("usage")
if got := usage.Get("input_tokens").Int(); got != tt.wantInputTokens {
t.Fatalf("input_tokens = %d, want %d", got, tt.wantInputTokens)
}
if got := usage.Get("output_tokens").Int(); got != tt.wantOutputTokens {
t.Fatalf("output_tokens = %d, want %d", got, tt.wantOutputTokens)
}
if got := usage.Get("cache_read_input_tokens").Int(); got != tt.wantCacheReadTokens {
t.Fatalf("cache_read_input_tokens = %d, want %d", got, tt.wantCacheReadTokens)
}
if tt.wantCacheWriteTokens == 0 {
if usage.Get("cache_creation_input_tokens").Exists() {
t.Fatalf("cache_creation_input_tokens should not be emitted when zero; got %v", usage.Get("cache_creation_input_tokens").Raw)
}
} else if got := usage.Get("cache_creation_input_tokens").Int(); got != tt.wantCacheWriteTokens {
t.Fatalf("cache_creation_input_tokens = %d, want %d", got, tt.wantCacheWriteTokens)
}
})
}
}
func TestConvertCodexResponseToClaudeNonStream_PreservesCacheWriteUsage(t *testing.T) {
tests := []struct {
name string
responseJSON string
wantInputTokens int64
wantOutputTokens int64
wantCacheReadTokens int64
wantCacheWriteTokens int64
}{
{
name: "cache_write_tokens field",
responseJSON: `{
"type":"response.completed",
"response":{
"id":"resp_1",
"model":"gpt-5",
"stop_reason":"stop",
"usage":{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":150}},
"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}]
}
}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cache_creation_tokens alias",
responseJSON: `{
"type":"response.completed",
"response":{
"id":"resp_1",
"model":"gpt-5",
"stop_reason":"stop",
"usage":{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_creation_tokens":150}},
"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}]
}
}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cached_tokens greater than input_tokens clamps input_tokens to zero",
responseJSON: `{
"type":"response.completed",
"response":{
"id":"resp_1",
"model":"gpt-5",
"stop_reason":"stop",
"usage":{"input_tokens":500,"output_tokens":100,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":50}},
"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}]
}
}`,
wantInputTokens: 0,
wantOutputTokens: 100,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 50,
},
{
name: "zero cache_write_tokens does not emit cache_creation_input_tokens",
responseJSON: `{
"type":"response.completed",
"response":{
"id":"resp_1",
"model":"gpt-5",
"stop_reason":"stop",
"usage":{"input_tokens":1000,"output_tokens":200,"input_tokens_details":{"cached_tokens":800,"cache_write_tokens":0}},
"output":[{"type":"message","content":[{"type":"output_text","text":"ok"}]}]
}
}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
originalRequest := []byte(`{"messages":[]}`)
out := ConvertCodexResponseToClaudeNonStream(ctx, "", originalRequest, nil, []byte(tt.responseJSON), nil)
parsed := gjson.ParseBytes(out)
usage := parsed.Get("usage")
if got := usage.Get("input_tokens").Int(); got != tt.wantInputTokens {
t.Fatalf("input_tokens = %d, want %d", got, tt.wantInputTokens)
}
if got := usage.Get("output_tokens").Int(); got != tt.wantOutputTokens {
t.Fatalf("output_tokens = %d, want %d", got, tt.wantOutputTokens)
}
if got := usage.Get("cache_read_input_tokens").Int(); got != tt.wantCacheReadTokens {
t.Fatalf("cache_read_input_tokens = %d, want %d", got, tt.wantCacheReadTokens)
}
if tt.wantCacheWriteTokens == 0 {
if usage.Get("cache_creation_input_tokens").Exists() {
t.Fatalf("cache_creation_input_tokens should not be emitted when zero; got %v", usage.Get("cache_creation_input_tokens").Raw)
}
} else if got := usage.Get("cache_creation_input_tokens").Int(); got != tt.wantCacheWriteTokens {
t.Fatalf("cache_creation_input_tokens = %d, want %d", got, tt.wantCacheWriteTokens)
}
})
}
}

View File

@@ -310,8 +310,8 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
usage := root.Get("usage")
if usage.Exists() && usage.Type != gjson.Null {
finalizeOpenAIAnthropicContentBlocks(param, &results)
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage)
emitAnthropicMessageDelta(param, &results, inputTokens, outputTokens, cachedTokens)
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractOpenAIUsage(usage)
emitAnthropicMessageDelta(param, &results, inputTokens, outputTokens, cachedTokens, cacheWriteTokens)
emitMessageStopIfNeeded(param, &results)
}
}
@@ -326,7 +326,7 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
finalizeOpenAIAnthropicContentBlocks(param, &results)
if !param.MessageDeltaSent {
emitAnthropicMessageDelta(param, &results, 0, 0, 0)
emitAnthropicMessageDelta(param, &results, 0, 0, 0, 0)
}
emitMessageStopIfNeeded(param, &results)
@@ -400,12 +400,15 @@ func convertOpenAINonStreamingToAnthropic(rawJSON []byte) [][]byte {
// Set usage information
if usage := root.Get("usage"); usage.Exists() {
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage)
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractOpenAIUsage(usage)
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
if cachedTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
}
if cacheWriteTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_creation_input_tokens", cacheWriteTokens)
}
}
return [][]byte{out}
@@ -570,7 +573,7 @@ func finalizeOpenAIAnthropicContentBlocks(param *ConvertOpenAIResponseToAnthropi
}
}
func emitAnthropicMessageDelta(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte, inputTokens, outputTokens, cachedTokens int64) {
func emitAnthropicMessageDelta(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte, inputTokens, outputTokens, cachedTokens, cacheWriteTokens int64) {
if param == nil || param.MessageDeltaSent {
return
}
@@ -581,6 +584,9 @@ func emitAnthropicMessageDelta(param *ConvertOpenAIResponseToAnthropicParams, re
if cachedTokens > 0 {
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.cache_read_input_tokens", cachedTokens)
}
if cacheWriteTokens > 0 {
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.cache_creation_input_tokens", cacheWriteTokens)
}
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2))
param.MessageDeltaSent = true
}
@@ -748,12 +754,15 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
}
if respUsage := root.Get("usage"); respUsage.Exists() {
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(respUsage)
inputTokens, outputTokens, cachedTokens, cacheWriteTokens := extractOpenAIUsage(respUsage)
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
if cachedTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens)
}
if cacheWriteTokens > 0 {
out, _ = sjson.SetBytes(out, "usage.cache_creation_input_tokens", cacheWriteTokens)
}
}
if !stopReasonSet {
@@ -771,14 +780,18 @@ func ClaudeTokenCount(ctx context.Context, count int64) []byte {
return translatorcommon.ClaudeInputTokensJSON(count)
}
func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) {
func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64, int64) {
if !usage.Exists() || usage.Type == gjson.Null {
return 0, 0, 0
return 0, 0, 0, 0
}
inputTokens := usage.Get("prompt_tokens").Int()
outputTokens := usage.Get("completion_tokens").Int()
cachedTokens := usage.Get("prompt_tokens_details.cached_tokens").Int()
cacheWriteTokens := usage.Get("prompt_tokens_details.cache_write_tokens").Int()
if cacheWriteTokens == 0 {
cacheWriteTokens = usage.Get("prompt_tokens_details.cache_creation_tokens").Int()
}
if cachedTokens > 0 {
if inputTokens >= cachedTokens {
@@ -788,5 +801,5 @@ func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) {
}
}
return inputTokens, outputTokens, cachedTokens
return inputTokens, outputTokens, cachedTokens, cacheWriteTokens
}

View File

@@ -3,6 +3,7 @@ package claude
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
@@ -577,3 +578,165 @@ func TestStreamingTool_OmittedToolCallIndexPreservesParallelCalls(t *testing.T)
t.Fatalf("stop_reason = %q, want %q", got, "tool_use")
}
}
func TestStreamingUsage_PreservesCacheWriteTokens(t *testing.T) {
tests := []struct {
name string
usageJSON string
wantInputTokens int64
wantOutputTokens int64
wantCacheReadTokens int64
wantCacheWriteTokens int64
}{
{
name: "cache_write_tokens field",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cache_creation_tokens alias",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_creation_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cached_tokens greater than prompt_tokens clamps input_tokens to zero",
usageJSON: `{"prompt_tokens":500,"completion_tokens":100,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":50}}`,
wantInputTokens: 0,
wantOutputTokens: 100,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 50,
},
{
name: "zero cache_write_tokens does not emit cache_creation_input_tokens",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":0}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
events := runStream(t, streamReq,
`{"id":"c1","model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":"hello"}}]}`,
`{"id":"c1","model":"m","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}`,
fmt.Sprintf(`{"id":"c1","model":"m","choices":[],"usage":%s}`, tt.usageJSON),
)
var deltaEvent *sseEvent
for _, e := range events {
if e.Type == "message_delta" {
deltaEvent = &e
break
}
}
if deltaEvent == nil {
t.Fatalf("missing message_delta event")
}
if input := gjson.Get(deltaEvent.Payload, "usage.input_tokens").Int(); input != tt.wantInputTokens {
t.Fatalf("input_tokens = %d, want %d", input, tt.wantInputTokens)
}
if output := gjson.Get(deltaEvent.Payload, "usage.output_tokens").Int(); output != tt.wantOutputTokens {
t.Fatalf("output_tokens = %d, want %d", output, tt.wantOutputTokens)
}
if cacheRead := gjson.Get(deltaEvent.Payload, "usage.cache_read_input_tokens").Int(); cacheRead != tt.wantCacheReadTokens {
t.Fatalf("cache_read_input_tokens = %d, want %d", cacheRead, tt.wantCacheReadTokens)
}
if tt.wantCacheWriteTokens == 0 {
if gjson.Get(deltaEvent.Payload, "usage.cache_creation_input_tokens").Exists() {
t.Fatalf("cache_creation_input_tokens should not be emitted when zero; got %v", gjson.Get(deltaEvent.Payload, "usage.cache_creation_input_tokens").Raw)
}
} else if cacheWrite := gjson.Get(deltaEvent.Payload, "usage.cache_creation_input_tokens").Int(); cacheWrite != tt.wantCacheWriteTokens {
t.Fatalf("cache_creation_input_tokens = %d, want %d", cacheWrite, tt.wantCacheWriteTokens)
}
})
}
}
func TestNonStreamingUsage_PreservesCacheWriteTokens(t *testing.T) {
tests := []struct {
name string
usageJSON string
wantInputTokens int64
wantOutputTokens int64
wantCacheReadTokens int64
wantCacheWriteTokens int64
}{
{
name: "cache_write_tokens field",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cache_creation_tokens alias",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_creation_tokens":150}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 150,
},
{
name: "cached_tokens greater than prompt_tokens clamps input_tokens to zero",
usageJSON: `{"prompt_tokens":500,"completion_tokens":100,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":50}}`,
wantInputTokens: 0,
wantOutputTokens: 100,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 50,
},
{
name: "zero cache_write_tokens does not emit cache_creation_input_tokens",
usageJSON: `{"prompt_tokens":1000,"completion_tokens":200,"prompt_tokens_details":{"cached_tokens":800,"cache_write_tokens":0}}`,
wantInputTokens: 200,
wantOutputTokens: 200,
wantCacheReadTokens: 800,
wantCacheWriteTokens: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rawJSON := []byte(fmt.Sprintf(`{
"id":"chatcmpl-123",
"object":"chat.completion",
"created":1677652288,
"model":"gpt-5.4",
"choices":[{"index":0,"message":{"role":"assistant","content":"Hello world"},"finish_reason":"stop"}],
"usage":%s
}`, tt.usageJSON))
ctx := context.Background()
reqJSON := []byte(`{"model":"claude-3-5-sonnet-20241022","messages":[{"role":"user","content":"Hello"}]}`)
out := ConvertOpenAIResponseToClaudeNonStream(ctx, "", reqJSON, reqJSON, rawJSON, nil)
parsed := gjson.ParseBytes(out)
usage := parsed.Get("usage")
if got := usage.Get("input_tokens").Int(); got != tt.wantInputTokens {
t.Fatalf("input_tokens = %d, want %d", got, tt.wantInputTokens)
}
if got := usage.Get("output_tokens").Int(); got != tt.wantOutputTokens {
t.Fatalf("output_tokens = %d, want %d", got, tt.wantOutputTokens)
}
if got := usage.Get("cache_read_input_tokens").Int(); got != tt.wantCacheReadTokens {
t.Fatalf("cache_read_input_tokens = %d, want %d", got, tt.wantCacheReadTokens)
}
if tt.wantCacheWriteTokens == 0 {
if usage.Get("cache_creation_input_tokens").Exists() {
t.Fatalf("cache_creation_input_tokens should not be emitted when zero; got %v", usage.Get("cache_creation_input_tokens").Raw)
}
} else if got := usage.Get("cache_creation_input_tokens").Int(); got != tt.wantCacheWriteTokens {
t.Fatalf("cache_creation_input_tokens = %d, want %d", got, tt.wantCacheWriteTokens)
}
})
}
}