From 674570ca17be336f2015d229495c8ceeda9d6c04 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Fri, 14 Aug 2026 00:25:55 +0800 Subject: [PATCH] fix: preserve group pricing in auth snapshots --- .../internal/service/api_key_auth_cache.go | 2 + .../service/api_key_auth_cache_impl.go | 6 ++- .../api_key_auth_cache_pricing_test.go | 50 +++++++++++++++++++ .../service/api_key_auth_cache_profit_test.go | 2 +- 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 backend/internal/service/api_key_auth_cache_pricing_test.go diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index 5fdc96b793..30a41eece6 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -84,6 +84,8 @@ type APIKeyAuthGroupSnapshot struct { AudioRealtimePricePerMin *float64 `json:"audio_realtime_price_per_min,omitempty"` AudioTTSPricePerMillionChars *float64 `json:"audio_tts_price_per_million_chars,omitempty"` AudioSTTPricePerHour *float64 `json:"audio_stt_price_per_hour,omitempty"` + LongContextPricingEnabled bool `json:"long_context_pricing_enabled"` + ModelPricing []ChannelModelPricing `json:"model_pricing,omitempty"` ClaudeCodeOnly bool `json:"claude_code_only"` FallbackGroupID *int64 `json:"fallback_group_id,omitempty"` FallbackGroupIDOnInvalidRequest *int64 `json:"fallback_group_id_on_invalid_request,omitempty"` diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index 696f21bb25..7cb37baf48 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -14,7 +14,7 @@ import ( "github.com/dgraph-io/ristretto" ) -const apiKeyAuthSnapshotVersion = 19 // v19: group search/audio/video_model_prices billing fields (force refresh of pre-fix snapshots) +const apiKeyAuthSnapshotVersion = 20 // v20: group long-context and model pricing fields (force refresh of pre-fix snapshots) type apiKeyAuthCacheConfig struct { l1Size int @@ -406,6 +406,8 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey) AudioRealtimePricePerMin: apiKey.Group.AudioRealtimePricePerMin, AudioTTSPricePerMillionChars: apiKey.Group.AudioTTSPricePerMillionChars, AudioSTTPricePerHour: apiKey.Group.AudioSTTPricePerHour, + LongContextPricingEnabled: apiKey.Group.LongContextPricingEnabled, + ModelPricing: apiKey.Group.ModelPricing, ClaudeCodeOnly: apiKey.Group.ClaudeCodeOnly, FallbackGroupID: apiKey.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: apiKey.Group.FallbackGroupIDOnInvalidRequest, @@ -501,6 +503,8 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho AudioRealtimePricePerMin: snapshot.Group.AudioRealtimePricePerMin, AudioTTSPricePerMillionChars: snapshot.Group.AudioTTSPricePerMillionChars, AudioSTTPricePerHour: snapshot.Group.AudioSTTPricePerHour, + LongContextPricingEnabled: snapshot.Group.LongContextPricingEnabled, + ModelPricing: snapshot.Group.ModelPricing, ClaudeCodeOnly: snapshot.Group.ClaudeCodeOnly, FallbackGroupID: snapshot.Group.FallbackGroupID, FallbackGroupIDOnInvalidRequest: snapshot.Group.FallbackGroupIDOnInvalidRequest, diff --git a/backend/internal/service/api_key_auth_cache_pricing_test.go b/backend/internal/service/api_key_auth_cache_pricing_test.go new file mode 100644 index 0000000000..22aa636aa7 --- /dev/null +++ b/backend/internal/service/api_key_auth_cache_pricing_test.go @@ -0,0 +1,50 @@ +package service + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAPIKeyAuthSnapshotGroupPricingRoundtrip(t *testing.T) { + groupID := int64(50) + inputPrice := 1e-6 + outputPrice := 2e-6 + apiKey := &APIKey{ + ID: 82, UserID: 40, GroupID: &groupID, Key: "sk-pricing-roundtrip", Status: StatusActive, + User: &User{ID: 40, Status: StatusActive}, + Group: &Group{ + ID: groupID, Name: "pricing-roundtrip", Platform: PlatformAnthropic, Status: StatusActive, + LongContextPricingEnabled: true, + ModelPricing: []ChannelModelPricing{{ + Models: []string{"claude-sonnet-*"}, BillingMode: BillingModeToken, + InputPrice: &inputPrice, OutputPrice: &outputPrice, + }}, + }, + } + svc := &APIKeyService{} + + payload, err := json.Marshal(&APIKeyAuthCacheEntry{Snapshot: svc.snapshotFromAPIKey(context.Background(), apiKey)}) + require.NoError(t, err) + var cached APIKeyAuthCacheEntry + require.NoError(t, json.Unmarshal(payload, &cached)) + + materialized, used, err := svc.applyAuthCacheEntry(apiKey.Key, &cached) + require.NoError(t, err) + require.True(t, used) + require.NotNil(t, materialized.Group) + require.True(t, materialized.Group.LongContextPricingEnabled) + require.Equal(t, apiKey.Group.ModelPricing, materialized.Group.ModelPricing) + + billing := &BillingService{fallbackPrices: map[string]*ModelPricing{ + "claude-sonnet-4": {InputPricePerToken: 3e-6, OutputPricePerToken: 15e-6}, + }} + resolver := NewModelPricingResolver(nil, billing) + resolved := resolver.Resolve(context.Background(), PricingInput{Model: "claude-sonnet-4", Group: materialized.Group}) + require.Equal(t, PricingSourceGroup, resolved.Source) + require.True(t, resolved.longContextPricingEnabled) + require.InDelta(t, inputPrice, resolved.BasePricing.InputPricePerToken, 1e-12) + require.InDelta(t, outputPrice, resolved.BasePricing.OutputPricePerToken, 1e-12) +} diff --git a/backend/internal/service/api_key_auth_cache_profit_test.go b/backend/internal/service/api_key_auth_cache_profit_test.go index 40519f2896..fbcd96a42a 100644 --- a/backend/internal/service/api_key_auth_cache_profit_test.go +++ b/backend/internal/service/api_key_auth_cache_profit_test.go @@ -53,7 +53,7 @@ func TestAPIKeyAuthSnapshotProfitControlRoundtrip(t *testing.T) { snapshot := svc.snapshotFromAPIKey(context.Background(), apiKey) require.NotNil(t, snapshot) require.Equal(t, apiKeyAuthSnapshotVersion, snapshot.Version) - require.Equal(t, 19, snapshot.Version, "v19 起认证快照携带 search/audio/video_model_prices 计费字段") + require.Equal(t, 20, snapshot.Version, "v20 起认证快照携带分组长上下文与模型定价字段") // 模拟 L2 缓存的完整 JSON 往返(与 apiKeyCache.SetAuthCache/GetAuthCache 同构)。 payload, err := json.Marshal(&APIKeyAuthCacheEntry{Snapshot: snapshot})