From 6577f2ef03fba15ed54f18a9a4f81527551f91e9 Mon Sep 17 00:00:00 2001 From: yangjianbo Date: Mon, 16 Feb 2026 13:23:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20=E9=81=BF=E5=85=8DSSE=20delta?= =?UTF-8?q?=E5=B0=86=E7=BC=93=E5=AD=98=E5=88=9B=E5=BB=BA=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E4=B8=BA0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 仅在 delta 中 5m/1h 值大于0时覆盖 usage 明细 - 新增回归测试覆盖 delta 默认 0 不应覆盖 message_start 非零值 - 迁移 054 在删除 legacy 字段前追加一次回填,避免升级实例丢失历史写入 Co-Authored-By: Claude Opus 4.6 --- backend/internal/service/gateway_service.go | 4 ++- .../service/gateway_streaming_test.go | 16 ++++++++++ .../054_drop_legacy_cache_columns.sql | 32 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 9d33dc08..839fad33 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -4474,8 +4474,10 @@ func (s *GatewayService) parseSSEUsage(data string, usage *ClaudeUsage) { // 解析嵌套的 cache_creation 对象中的 5m/1h 明细 cc5m := gjson.Get(data, "usage.cache_creation.ephemeral_5m_input_tokens") cc1h := gjson.Get(data, "usage.cache_creation.ephemeral_1h_input_tokens") - if cc5m.Exists() || cc1h.Exists() { + if cc5m.Exists() && cc5m.Int() > 0 { usage.CacheCreation5mTokens = int(cc5m.Int()) + } + if cc1h.Exists() && cc1h.Int() > 0 { usage.CacheCreation1hTokens = int(cc1h.Int()) } } diff --git a/backend/internal/service/gateway_streaming_test.go b/backend/internal/service/gateway_streaming_test.go index 50b998a3..cd690cbd 100644 --- a/backend/internal/service/gateway_streaming_test.go +++ b/backend/internal/service/gateway_streaming_test.go @@ -79,6 +79,22 @@ func TestParseSSEUsage_DeltaOverwritesWithNonZero(t *testing.T) { require.Equal(t, 60, usage.CacheReadInputTokens) } +func TestParseSSEUsage_DeltaDoesNotResetCacheCreationBreakdown(t *testing.T) { + svc := newMinimalGatewayService() + usage := &ClaudeUsage{} + + // 先在 message_start 中写入非零 5m/1h 明细 + svc.parseSSEUsage(`{"type":"message_start","message":{"usage":{"input_tokens":100,"cache_creation":{"ephemeral_5m_input_tokens":30,"ephemeral_1h_input_tokens":70}}}}`, usage) + require.Equal(t, 30, usage.CacheCreation5mTokens) + require.Equal(t, 70, usage.CacheCreation1hTokens) + + // 后续 delta 带默认 0,不应覆盖已有非零值 + svc.parseSSEUsage(`{"type":"message_delta","usage":{"output_tokens":12,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0}}}`, usage) + require.Equal(t, 30, usage.CacheCreation5mTokens, "delta 的 0 值不应重置 5m 明细") + require.Equal(t, 70, usage.CacheCreation1hTokens, "delta 的 0 值不应重置 1h 明细") + require.Equal(t, 12, usage.OutputTokens) +} + func TestParseSSEUsage_InvalidJSON(t *testing.T) { svc := newMinimalGatewayService() usage := &ClaudeUsage{} diff --git a/backend/migrations/054_drop_legacy_cache_columns.sql b/backend/migrations/054_drop_legacy_cache_columns.sql index ac73cd28..040828c2 100644 --- a/backend/migrations/054_drop_legacy_cache_columns.sql +++ b/backend/migrations/054_drop_legacy_cache_columns.sql @@ -8,7 +8,37 @@ -- cache_creation_1h_tokens (defined in 001_init.sql) -- -- Migration 009 already copied data from legacy → canonical columns. --- This migration drops the legacy columns to avoid confusion. +-- But upgraded instances may still have post-009 writes in legacy columns. +-- Backfill once more before dropping to prevent data loss. + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'usage_logs' + AND column_name = 'cache_creation5m_tokens' + ) THEN + UPDATE usage_logs + SET cache_creation_5m_tokens = cache_creation5m_tokens + WHERE cache_creation_5m_tokens = 0 + AND cache_creation5m_tokens <> 0; + END IF; + + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'usage_logs' + AND column_name = 'cache_creation1h_tokens' + ) THEN + UPDATE usage_logs + SET cache_creation_1h_tokens = cache_creation1h_tokens + WHERE cache_creation_1h_tokens = 0 + AND cache_creation1h_tokens <> 0; + END IF; +END $$; ALTER TABLE usage_logs DROP COLUMN IF EXISTS cache_creation5m_tokens; ALTER TABLE usage_logs DROP COLUMN IF EXISTS cache_creation1h_tokens;