From 0416c9ede47e8fbab4a705dc3eeb8b21bf928d73 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 23 Sep 2026 15:27:53 +0800 Subject: [PATCH] fix(gateway): avoid ambiguous late-cache input subtraction --- .../anthropic_chat_stream_usage_test.go | 9 +++++---- .../service/gateway_forward_as_responses.go | 15 +++++---------- .../gateway_forward_as_responses_test.go | 18 +++++++++--------- .../service/openai_gateway_cn_fixes_test.go | 8 ++++---- 4 files changed, 23 insertions(+), 27 deletions(-) diff --git a/backend/internal/service/anthropic_chat_stream_usage_test.go b/backend/internal/service/anthropic_chat_stream_usage_test.go index ff1558b073..2397a5802d 100644 --- a/backend/internal/service/anthropic_chat_stream_usage_test.go +++ b/backend/internal/service/anthropic_chat_stream_usage_test.go @@ -29,10 +29,11 @@ func TestAnthropicChatStreamAuthoritativeUsage(t *testing.T) { {"cache", `,"usage":{"input_tokens":308,"cache_read_input_tokens":241,"cache_creation_input_tokens":17}`, `,"usage":{"output_tokens":49}`, "", true, 566, 308, 566, 49, 241, 17}, {"deepseek_partial_cache", "", `,"usage":{"input_tokens":1200,"output_tokens":30,"prompt_cache_hit_tokens":800,"prompt_cache_miss_tokens":400}`, "", true, 1200, 400, 1200, 30, 800, 0}, {"openai_partial_cache", "", `,"usage":{"prompt_tokens":1200,"output_tokens":30,"prompt_tokens_details":{"cached_tokens":800}}`, "", true, 1200, 400, 1200, 30, 800, 0}, - {"repeated_cache_bucket", `,"usage":{"input_tokens":1200}`, `,"usage":{"output_tokens":30,"cache_read_input_tokens":800}`, `,"usage":{"output_tokens":30,"cache_read_input_tokens":800}`, true, 1200, 400, 1200, 30, 800, 0}, - {"kimi_full_cache", `,"usage":{"input_tokens":173306}`, `,"usage":{"output_tokens":49,"cache_read_input_tokens":173306}`, "", true, 173306, 0, 173306, 49, 173306, 0}, - {"kimi_full_cache_cached_tokens", `,"usage":{"input_tokens":173306}`, `,"usage":{"output_tokens":49,"cached_tokens":173306}`, "", true, 173306, 0, 173306, 49, 173306, 0}, - {"kimi_full_cache_prompt_details", `,"usage":{"input_tokens":173306}`, `,"usage":{"output_tokens":49,"prompt_tokens_details":{"cached_tokens":173306}}`, "", true, 173306, 0, 173306, 49, 173306, 0}, + {"repeated_authoritative_cache_bucket", `,"usage":{"input_tokens":1200,"prompt_tokens":1200}`, `,"usage":{"output_tokens":30,"prompt_tokens":1200,"cache_read_input_tokens":800}`, `,"usage":{"output_tokens":30,"prompt_tokens":1200,"cache_read_input_tokens":800}`, true, 1200, 400, 1200, 30, 800, 0}, + {"independent_input_with_late_cache", `,"usage":{"input_tokens":308,"cache_read_input_tokens":241,"cache_creation_input_tokens":17}`, `,"usage":{"input_tokens":0,"output_tokens":49,"cache_read_input_tokens":300,"cache_creation_input_tokens":17}`, "", true, 625, 308, 625, 49, 300, 17}, + {"kimi_full_cache", `,"usage":{"input_tokens":173306,"prompt_tokens":173306}`, `,"usage":{"input_tokens":0,"output_tokens":49,"prompt_tokens":173306,"cache_read_input_tokens":173306}`, "", true, 173306, 0, 173306, 49, 173306, 0}, + {"kimi_full_cache_cached_tokens", `,"usage":{"input_tokens":173306,"prompt_tokens":173306}`, `,"usage":{"input_tokens":0,"output_tokens":49,"prompt_tokens":173306,"cached_tokens":173306}`, "", true, 173306, 0, 173306, 49, 173306, 0}, + {"kimi_full_cache_prompt_details", `,"usage":{"input_tokens":173306,"prompt_tokens":173306}`, `,"usage":{"input_tokens":0,"output_tokens":49,"prompt_tokens":173306,"prompt_tokens_details":{"cached_tokens":173306}}`, "", true, 173306, 0, 173306, 49, 173306, 0}, {"zero_start", `,"usage":{"input_tokens":0,"output_tokens":0}`, "", "", true, 0, 0, 0, 0, 0, 0}, {"zero_delta", "", `,"usage":{"input_tokens":0,"output_tokens":0}`, "", true, 0, 0, 0, 0, 0, 0}, {"absent", "", "", "", false, 0, 0, 0, 0, 0, 0}, diff --git a/backend/internal/service/gateway_forward_as_responses.go b/backend/internal/service/gateway_forward_as_responses.go index 6903e659ad..0c976838c7 100644 --- a/backend/internal/service/gateway_forward_as_responses.go +++ b/backend/internal/service/gateway_forward_as_responses.go @@ -303,19 +303,14 @@ func mergeAnthropicUsage(dst *ClaudeUsage, src apicompat.AnthropicUsage) { dst.CacheReadInputTokens = cacheReadTokens dst.CacheCreationInputTokens = src.CacheCreationInputTokens } else { - previousCacheReadTokens := dst.CacheReadInputTokens - previousCacheCreationTokens := dst.CacheCreationInputTokens + // Without an authoritative prompt total or miss bucket, input_tokens is + // provider-specific: it may already be the uncached bucket, or it may be + // a total from an earlier event. Do not infer a subtraction merely because + // a later event contains cache buckets; that would corrupt providers whose + // stream uses independent input and cache fields. if src.InputTokens > 0 { dst.InputTokens = src.InputTokens } - if src.InputTokens == 0 && dst.InputTokens > 0 && (cacheReadTokens > 0 || src.CacheCreationInputTokens > 0) { - // Some compatible streams put the total prompt count in message_start, - // then provide only cumulative cache buckets in message_delta. Subtract - // only newly observed buckets so repeated deltas are idempotent. - newCacheReadTokens := max(cacheReadTokens-previousCacheReadTokens, 0) - newCacheCreationTokens := max(src.CacheCreationInputTokens-previousCacheCreationTokens, 0) - dst.InputTokens = max(dst.InputTokens-newCacheReadTokens-newCacheCreationTokens, 0) - } if cacheReadTokens > 0 { dst.CacheReadInputTokens = cacheReadTokens } diff --git a/backend/internal/service/gateway_forward_as_responses_test.go b/backend/internal/service/gateway_forward_as_responses_test.go index 353eeb8846..cafee5a71b 100644 --- a/backend/internal/service/gateway_forward_as_responses_test.go +++ b/backend/internal/service/gateway_forward_as_responses_test.go @@ -321,22 +321,22 @@ func TestHandleResponsesStreamingResponse_NormalizesTerminalUsage(t *testing.T) }{ { name: "cache_read_input_tokens full cache", - startUsage: `"input_tokens":173306`, - deltaUsage: `"output_tokens":8,"cache_read_input_tokens":173306`, + startUsage: `"input_tokens":173306,"prompt_tokens":173306`, + deltaUsage: `"input_tokens":0,"output_tokens":8,"prompt_tokens":173306,"cache_read_input_tokens":173306`, wantOutput: 8, wantCached: 173306, }, { name: "cached_tokens full cache", - startUsage: `"input_tokens":173306`, - deltaUsage: `"output_tokens":8,"cached_tokens":173306`, + startUsage: `"input_tokens":173306,"prompt_tokens":173306`, + deltaUsage: `"input_tokens":0,"output_tokens":8,"prompt_tokens":173306,"cached_tokens":173306`, wantOutput: 8, wantCached: 173306, }, { name: "prompt_tokens_details full cache", - startUsage: `"input_tokens":173306`, - deltaUsage: `"output_tokens":8,"prompt_tokens_details":{"cached_tokens":173306}`, + startUsage: `"input_tokens":173306,"prompt_tokens":173306`, + deltaUsage: `"input_tokens":0,"output_tokens":8,"prompt_tokens":173306,"prompt_tokens_details":{"cached_tokens":173306}`, wantOutput: 8, wantCached: 173306, }, @@ -357,10 +357,10 @@ func TestHandleResponsesStreamingResponse_NormalizesTerminalUsage(t *testing.T) wantCached: 800, }, { - name: "cache creation only without prompt total or miss bucket", + name: "cache creation only without prompt total or miss bucket keeps independent input", startUsage: `"input_tokens":1200`, - deltaUsage: `"output_tokens":30,"cache_creation_input_tokens":800`, - wantInput: 400, + deltaUsage: `"input_tokens":0,"output_tokens":30,"cache_creation_input_tokens":800`, + wantInput: 1200, wantOutput: 30, wantCacheCreation: 800, }, diff --git a/backend/internal/service/openai_gateway_cn_fixes_test.go b/backend/internal/service/openai_gateway_cn_fixes_test.go index 470c789c74..da532536b1 100644 --- a/backend/internal/service/openai_gateway_cn_fixes_test.go +++ b/backend/internal/service/openai_gateway_cn_fixes_test.go @@ -125,10 +125,10 @@ func TestResponsesStreamingFromNativeAnthropic_NormalizesTerminalUsage(t *testin wantCached int wantCacheCreation int }{ - {name: "full cache", startUsage: `"input_tokens":1200`, deltaUsage: `"output_tokens":30,"cache_read_input_tokens":1200`, wantOutput: 30, wantCached: 1200}, - {name: "partial cache", startUsage: `"input_tokens":1200`, deltaUsage: `"output_tokens":30,"cache_read_input_tokens":800`, wantInput: 400, wantOutput: 30, wantCached: 800}, - {name: "cache creation", startUsage: `"input_tokens":1200`, deltaUsage: `"output_tokens":30,"cache_creation_input_tokens":800`, wantInput: 400, wantOutput: 30, wantCacheCreation: 800}, - {name: "repeated cumulative cache bucket", startUsage: `"input_tokens":1200`, deltaUsage: `"output_tokens":30,"cache_read_input_tokens":800`, repeatDelta: true, wantInput: 400, wantOutput: 30, wantCached: 800}, + {name: "full cache", startUsage: `"input_tokens":1200,"prompt_tokens":1200`, deltaUsage: `"input_tokens":0,"output_tokens":30,"prompt_tokens":1200,"cache_read_input_tokens":1200`, wantOutput: 30, wantCached: 1200}, + {name: "partial cache", startUsage: `"input_tokens":1200,"prompt_tokens":1200`, deltaUsage: `"input_tokens":0,"output_tokens":30,"prompt_tokens":1200,"cache_read_input_tokens":800`, wantInput: 400, wantOutput: 30, wantCached: 800}, + {name: "cache creation", startUsage: `"input_tokens":1200,"prompt_tokens":1200`, deltaUsage: `"input_tokens":0,"output_tokens":30,"prompt_tokens":1200,"cache_creation_input_tokens":800`, wantInput: 400, wantOutput: 30, wantCacheCreation: 800}, + {name: "repeated cumulative cache bucket", startUsage: `"input_tokens":1200,"prompt_tokens":1200`, deltaUsage: `"input_tokens":0,"output_tokens":30,"prompt_tokens":1200,"cache_read_input_tokens":800`, repeatDelta: true, wantInput: 400, wantOutput: 30, wantCached: 800}, } for _, tt := range tests {