From 0d28f7f90d80bfdbf9d44e3efe2ddbfc5a58f7e0 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 10 Jul 2026 20:40:45 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(apicompat):=20Responses=E2=86=94Anthrop?= =?UTF-8?q?ic=20=E8=BD=AC=E6=8D=A2=E8=A1=A5=E9=BD=90=20cache=5Fcreation=5F?= =?UTF-8?q?input=5Ftokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3935 --- .../anthropic_to_responses_response.go | 14 ++-- ...responses_anthropic_cache_creation_test.go | 64 +++++++++++++++++++ .../pkg/apicompat/responses_to_anthropic.go | 9 +-- 3 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go diff --git a/backend/internal/pkg/apicompat/anthropic_to_responses_response.go b/backend/internal/pkg/apicompat/anthropic_to_responses_response.go index de8ab78df8..67c161bdd3 100644 --- a/backend/internal/pkg/apicompat/anthropic_to_responses_response.go +++ b/backend/internal/pkg/apicompat/anthropic_to_responses_response.go @@ -102,9 +102,10 @@ func AnthropicToResponsesResponse(resp *AnthropicResponse) *ResponsesResponse { resp.Usage.CacheReadInputTokens + resp.Usage.CacheCreationInputTokens out.Usage = &ResponsesUsage{ - InputTokens: totalInputTokens, - OutputTokens: resp.Usage.OutputTokens, - TotalTokens: totalInputTokens + resp.Usage.OutputTokens, + InputTokens: totalInputTokens, + OutputTokens: resp.Usage.OutputTokens, + TotalTokens: totalInputTokens + resp.Usage.OutputTokens, + CacheCreationInputTokens: resp.Usage.CacheCreationInputTokens, } if resp.Usage.CacheReadInputTokens > 0 { out.Usage.InputTokensDetails = &ResponsesInputTokensDetails{ @@ -497,9 +498,10 @@ func makeResponsesCompletedEvent( // back to match OpenAI Responses semantics where input_tokens is the total. totalInputTokens := state.InputTokens + state.CacheReadInputTokens + state.CacheCreationInputTokens usage := &ResponsesUsage{ - InputTokens: totalInputTokens, - OutputTokens: state.OutputTokens, - TotalTokens: totalInputTokens + state.OutputTokens, + InputTokens: totalInputTokens, + OutputTokens: state.OutputTokens, + TotalTokens: totalInputTokens + state.OutputTokens, + CacheCreationInputTokens: state.CacheCreationInputTokens, } if state.CacheReadInputTokens > 0 { usage.InputTokensDetails = &ResponsesInputTokensDetails{ diff --git a/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go b/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go new file mode 100644 index 0000000000..a5856845c9 --- /dev/null +++ b/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go @@ -0,0 +1,64 @@ +package apicompat + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnthropicUsageFromResponsesUsage_CacheCreation(t *testing.T) { + usage := &ResponsesUsage{ + InputTokens: 20, + OutputTokens: 5, + CacheCreationInputTokens: 6, + InputTokensDetails: &ResponsesInputTokensDetails{ + CachedTokens: 4, + }, + } + + got := anthropicUsageFromResponsesUsage(usage) + + assert.Equal(t, 10, got.InputTokens, "input = total(20) - cache_read(4) - cache_creation(6)") + assert.Equal(t, 5, got.OutputTokens) + assert.Equal(t, 4, got.CacheReadInputTokens) + assert.Equal(t, 6, got.CacheCreationInputTokens, "cache creation must be preserved") +} + +func TestAnthropicUsageFromResponsesUsage_NoCacheCreation(t *testing.T) { + usage := &ResponsesUsage{ + InputTokens: 10, + OutputTokens: 5, + InputTokensDetails: &ResponsesInputTokensDetails{ + CachedTokens: 3, + }, + } + + got := anthropicUsageFromResponsesUsage(usage) + + assert.Equal(t, 7, got.InputTokens) + assert.Equal(t, 3, got.CacheReadInputTokens) + assert.Equal(t, 0, got.CacheCreationInputTokens) +} + +func TestAnthropicToResponsesResponse_CacheCreation(t *testing.T) { + resp := AnthropicResponse{ + ID: "msg_test", + Type: "message", + Role: "assistant", + Model: "claude-opus-4-6", + Usage: AnthropicUsage{ + InputTokens: 10, + OutputTokens: 5, + CacheReadInputTokens: 4, + CacheCreationInputTokens: 6, + }, + StopReason: "end_turn", + } + + out := AnthropicToResponsesResponse(&resp) + + require.NotNil(t, out.Usage) + assert.Equal(t, 20, out.Usage.InputTokens, "total = input(10) + cache_read(4) + cache_creation(6)") + assert.Equal(t, 6, out.Usage.CacheCreationInputTokens, "cache creation must round-trip") +} diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic.go b/backend/internal/pkg/apicompat/responses_to_anthropic.go index 037e16b652..9cc6e49c75 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic.go @@ -100,15 +100,16 @@ func anthropicUsageFromResponsesUsage(usage *ResponsesUsage) AnthropicUsage { cachedTokens = usage.InputTokensDetails.CachedTokens } - inputTokens := usage.InputTokens - cachedTokens + inputTokens := usage.InputTokens - cachedTokens - usage.CacheCreationInputTokens if inputTokens < 0 { inputTokens = 0 } return AnthropicUsage{ - InputTokens: inputTokens, - OutputTokens: usage.OutputTokens, - CacheReadInputTokens: cachedTokens, + InputTokens: inputTokens, + OutputTokens: usage.OutputTokens, + CacheReadInputTokens: cachedTokens, + CacheCreationInputTokens: usage.CacheCreationInputTokens, } } From 83f169e4fa815f7083de23e301d1a1560dc71ca8 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 10 Jul 2026 20:49:44 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(apicompat):=20=E8=A1=A5=E9=BD=90=20Resp?= =?UTF-8?q?onses=E2=86=92Anthropic=20=E6=B5=81=E5=BC=8F=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=9A=84=20cache=5Fcreation=5Finput=5Ftokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...responses_anthropic_cache_creation_test.go | 35 +++++++++++++++++++ .../pkg/apicompat/responses_to_anthropic.go | 23 +++++++----- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go b/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go index a5856845c9..b8c7916d2d 100644 --- a/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go +++ b/backend/internal/pkg/apicompat/responses_anthropic_cache_creation_test.go @@ -41,6 +41,41 @@ func TestAnthropicUsageFromResponsesUsage_NoCacheCreation(t *testing.T) { assert.Equal(t, 0, got.CacheCreationInputTokens) } +func TestResponsesEventToAnthropicEvents_StreamingCacheCreation(t *testing.T) { + state := NewResponsesEventToAnthropicState() + state.MessageStartSent = true + + completedEvt := &ResponsesStreamEvent{ + Type: "response.completed", + Response: &ResponsesResponse{ + Status: "completed", + Usage: &ResponsesUsage{ + InputTokens: 20, + OutputTokens: 5, + CacheCreationInputTokens: 6, + InputTokensDetails: &ResponsesInputTokensDetails{ + CachedTokens: 4, + }, + }, + }, + } + + events := ResponsesEventToAnthropicEvents(completedEvt, state) + + var deltaEvt *AnthropicStreamEvent + for i := range events { + if events[i].Type == "message_delta" { + deltaEvt = &events[i] + break + } + } + require.NotNil(t, deltaEvt, "should have message_delta event") + require.NotNil(t, deltaEvt.Usage) + assert.Equal(t, 6, deltaEvt.Usage.CacheCreationInputTokens, "streaming cache_creation must be preserved") + assert.Equal(t, 10, deltaEvt.Usage.InputTokens, "input = 20 - 4(read) - 6(creation)") + assert.Equal(t, 4, deltaEvt.Usage.CacheReadInputTokens) +} + func TestAnthropicToResponsesResponse_CacheCreation(t *testing.T) { resp := AnthropicResponse{ ID: "msg_test", diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic.go b/backend/internal/pkg/apicompat/responses_to_anthropic.go index 9cc6e49c75..9c3b85b2ef 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic.go @@ -182,9 +182,10 @@ type ResponsesEventToAnthropicState struct { // OutputIndexToBlockIdx maps Responses output_index → Anthropic content block index. OutputIndexToBlockIdx map[int]int - InputTokens int - OutputTokens int - CacheReadInputTokens int + InputTokens int + OutputTokens int + CacheReadInputTokens int + CacheCreationInputTokens int ResponseID string Model string @@ -259,9 +260,10 @@ func FinalizeResponsesAnthropicStream(state *ResponsesEventToAnthropicState) []A StopReason: stopReason, }, Usage: &AnthropicUsage{ - InputTokens: state.InputTokens, - OutputTokens: state.OutputTokens, - CacheReadInputTokens: state.CacheReadInputTokens, + InputTokens: state.InputTokens, + OutputTokens: state.OutputTokens, + CacheReadInputTokens: state.CacheReadInputTokens, + CacheCreationInputTokens: state.CacheCreationInputTokens, }, }, AnthropicStreamEvent{Type: "message_stop"}, @@ -579,6 +581,7 @@ func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventTo state.InputTokens = usage.InputTokens state.OutputTokens = usage.OutputTokens state.CacheReadInputTokens = usage.CacheReadInputTokens + state.CacheCreationInputTokens = usage.CacheCreationInputTokens } if evt.Response != nil { if evt.Response.Usage != nil { @@ -586,6 +589,7 @@ func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventTo state.InputTokens = usage.InputTokens state.OutputTokens = usage.OutputTokens state.CacheReadInputTokens = usage.CacheReadInputTokens + state.CacheCreationInputTokens = usage.CacheCreationInputTokens } switch evt.Response.Status { case "incomplete": @@ -606,9 +610,10 @@ func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventTo StopReason: stopReason, }, Usage: &AnthropicUsage{ - InputTokens: state.InputTokens, - OutputTokens: state.OutputTokens, - CacheReadInputTokens: state.CacheReadInputTokens, + InputTokens: state.InputTokens, + OutputTokens: state.OutputTokens, + CacheReadInputTokens: state.CacheReadInputTokens, + CacheCreationInputTokens: state.CacheCreationInputTokens, }, }, AnthropicStreamEvent{Type: "message_stop"},