diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index a1fe74ebee..45014870ee 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -524,6 +524,27 @@ func (s *BillingService) initFallbackPricing() { SupportsCacheBreakdown: false, } + // Gemini 4 Argon (Google AI pricing: $2 input / $10 output / $0.10 cached + // input per MTok, promotional through 2026-11-30; official rates double to + // $4/$20/$0.20 from 2026-12-01). One rate covers every prompt length. + // Antigravity's -high/-low/-medium aliases are matched below so unavailable + // remote pricing never records token-bearing requests at $0. + s.fallbackPrices["gemini-4-argon"] = &ModelPricing{ + InputPricePerToken: 2e-6, + OutputPricePerToken: 10e-6, + CacheReadPricePerToken: 0.1e-6, + SupportsCacheBreakdown: false, + } + + // Gemini 4 Argon Cyber Permissive (Vertex AI pricing: $4 input / $20 output / + // $0.20 cached input per MTok). The Gemini 4 Argon promotion does not apply. + s.fallbackPrices["gemini-4-argon-cyber-permissive"] = &ModelPricing{ + InputPricePerToken: 4e-6, + OutputPricePerToken: 20e-6, + CacheReadPricePerToken: 0.2e-6, + SupportsCacheBreakdown: false, + } + // OpenAI GPT-5.4(业务指定价格) s.fallbackPrices["gpt-5.4"] = &ModelPricing{ InputPricePerToken: 2.5e-6, // $2.5 per MTok @@ -1082,6 +1103,13 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { if strings.Contains(modelLower, "gemini-3.8-flash") || strings.Contains(modelLower, "gemini-3-8-flash") { return s.fallbackPrices["gemini-3.8-flash"] } + // Cyber 变体不享受 Gemini 4 Argon 的优惠价,必须先于 "gemini-4-argon" 判。 + if strings.Contains(modelLower, "gemini-4-argon-cyber") { + return s.fallbackPrices["gemini-4-argon-cyber-permissive"] + } + if strings.Contains(modelLower, "gemini-4-argon") { + return s.fallbackPrices["gemini-4-argon"] + } // DeepSeek 系列:官方模型 V4 Pro/Flash(含 vision-exp)按各自价卡; // 其余 deepseek-*(含已停服的 deepseek-chat / deepseek-reasoner 与未知型号) diff --git a/backend/internal/service/gemini_cache_storage_pricing_test.go b/backend/internal/service/gemini_cache_storage_pricing_test.go index 846f7b56ec..97a8aef6a2 100644 --- a/backend/internal/service/gemini_cache_storage_pricing_test.go +++ b/backend/internal/service/gemini_cache_storage_pricing_test.go @@ -43,7 +43,10 @@ func TestBuiltinGeminiCacheStoragePrice(t *testing.T) { {"gemini-flash-latest", 1e-6}, {"gemini-flash-lite-latest", 1e-6}, {"gemini-pro-latest", 4.5e-6}, - // 无存储价 + // 无存储价(Gemini 4 Argon 仅支持隐式缓存) + {"gemini-4-argon", 0}, + {"gemini-4-argon-high", 0}, + {"gemini-4-argon-cyber-permissive", 0}, {"gemini-embedding-001", 0}, {"gemini-unknown-model", 0}, {"claude-sonnet-5-5", 0}, diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index 408de5d8b5..5df65b5be8 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -1306,7 +1306,7 @@ func (s *PricingService) buildModelLookupCandidates(modelLower string) []string normalized := normalizeModelNameForPricing(modelLower) // A tier-specific entry should take precedence when the pricing catalog gains - // one later. Antigravity's Gemini Flash thinking tiers share the base rate, + // one later. Antigravity's Gemini thinking tiers share the base rate, // so the normalized base remains the fallback after the exact aliases. candidates := rawCandidates if normalizeGeminiThinkingTierAlias(lastSegment(modelLower)) != lastSegment(modelLower) { @@ -1368,12 +1368,12 @@ func normalizeModelNameForPricing(model string) string { return normalizeGeminiThinkingTierAlias(model) } -// normalizeGeminiThinkingTierAlias maps Antigravity's Gemini Flash -// thinking-tier model IDs to the public base model. The tier controls reasoning -// behavior, not the published token rate, so this keeps -high/-low/-medium and -// -tiered requests on the corresponding base model's price card. +// normalizeGeminiThinkingTierAlias maps Antigravity's Gemini thinking-tier +// model IDs to the public base model. The tier controls reasoning behavior, not +// the published token rate, so this keeps -high/-low/-medium and -tiered +// requests on the corresponding base model's price card. func normalizeGeminiThinkingTierAlias(model string) string { - for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"} { + for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "gemini-4-argon"} { for _, tier := range []string{"-high", "-low", "-medium", "-tiered"} { if model == baseModel+tier { return baseModel diff --git a/backend/internal/service/pricing_service_test.go b/backend/internal/service/pricing_service_test.go index 3d9f0cb09e..4466be1f3f 100644 --- a/backend/internal/service/pricing_service_test.go +++ b/backend/internal/service/pricing_service_test.go @@ -732,6 +732,93 @@ func TestDefaultPricingIncludesGemini36FlashRates(t *testing.T) { } } +func TestPricingService_Gemini4ArgonThinkingTiersUseBasePricing(t *testing.T) { + basePricing := &LiteLLMModelPricing{InputCostPerToken: 2e-6, OutputCostPerToken: 10e-6, CacheReadInputTokenCost: 0.1e-6} + cyberPricing := &LiteLLMModelPricing{InputCostPerToken: 4e-6, OutputCostPerToken: 20e-6, CacheReadInputTokenCost: 0.2e-6} + svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{ + "gemini-4-argon": basePricing, + "gemini-4-argon-cyber-permissive": cyberPricing, + }} + + for _, model := range []string{ + "gemini-4-argon", + "gemini-4-argon-high", + "gemini-4-argon-low", + "gemini-4-argon-medium", + "models/gemini-4-argon-high", + "projects/p/locations/global/publishers/google/models/gemini-4-argon", + } { + t.Run(model, func(t *testing.T) { + require.Same(t, basePricing, svc.GetModelPricing(model)) + }) + } + require.Same(t, cyberPricing, svc.GetModelPricing("gemini-4-argon-cyber-permissive")) +} + +func TestBillingService_Gemini4ArgonFallbacksAreBillable(t *testing.T) { + svc := NewBillingService(&config.Config{}, nil) + tokens := UsageTokens{InputTokens: 1_000_000, OutputTokens: 1_000_000, CacheReadTokens: 1_000_000} + + for _, tc := range []struct { + model string + input, output, read float64 + }{ + {"gemini-4-argon", 2, 10, 0.1}, + {"gemini-4-argon-high", 2, 10, 0.1}, + {"gemini-4-argon-low", 2, 10, 0.1}, + {"gemini-4-argon-medium", 2, 10, 0.1}, + {"models/gemini-4-argon", 2, 10, 0.1}, + {"gemini-4-argon-cyber-permissive", 4, 20, 0.2}, + {"gemini-4-argon-cyber", 4, 20, 0.2}, + {"publishers/google/models/gemini-4-argon-cyber-permissive", 4, 20, 0.2}, + } { + t.Run(tc.model, func(t *testing.T) { + cost, err := svc.CalculateCost(tc.model, tokens, 1) + require.NoError(t, err) + require.InDelta(t, tc.input, cost.InputCost, 1e-12) + require.InDelta(t, tc.output, cost.OutputCost, 1e-12) + require.InDelta(t, tc.read, cost.CacheReadCost, 1e-12) + require.InDelta(t, tc.input+tc.output+tc.read, cost.TotalCost, 1e-12) + }) + } +} + +func TestDefaultPricingIncludesGemini4ArgonRates(t *testing.T) { + data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json")) + require.NoError(t, err) + + pricingSvc := &PricingService{} + pricingData, err := pricingSvc.parsePricingData(data) + require.NoError(t, err) + pricingSvc.pricingData = pricingData + + for _, tc := range []struct { + model string + provider string + input, output, read float64 + }{ + {"gemini-4-argon", "gemini", 2e-6, 10e-6, 0.1e-6}, + {"gemini-4-argon-high", "gemini", 2e-6, 10e-6, 0.1e-6}, + {"gemini-4-argon-cyber-permissive", "vertex_ai-language-models", 4e-6, 20e-6, 0.2e-6}, + } { + t.Run(tc.model, func(t *testing.T) { + pricing := pricingSvc.GetModelPricing(tc.model) + require.NotNil(t, pricing) + require.Equal(t, tc.provider, pricing.LiteLLMProvider) + require.InDelta(t, tc.input, pricing.InputCostPerToken, 1e-15) + require.InDelta(t, tc.output, pricing.OutputCostPerToken, 1e-15) + require.InDelta(t, tc.read, pricing.CacheReadInputTokenCost, 1e-15) + // Priority 档为标准价的 1.8 倍;单一价档不分提示词长度,显式缓存不可用故无存储价。 + require.InDelta(t, tc.input*1.8, pricing.InputCostPerTokenPriority, 1e-15) + require.InDelta(t, tc.output*1.8, pricing.OutputCostPerTokenPriority, 1e-15) + require.InDelta(t, tc.read*1.8, pricing.CacheReadInputTokenCostPriority, 1e-15) + require.Zero(t, pricing.LongContextInputTokenThreshold) + require.Zero(t, pricing.CacheStorageCostPerTokenPerHour) + }) + } + require.Contains(t, pricingSvc.ListModelNamesByProvider("gemini"), "gemini-4-argon") +} + func TestDefaultPricingUsesCurrentCodexAutoReviewBaseRates(t *testing.T) { data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json")) require.NoError(t, err) diff --git a/backend/resources/model-pricing/model_prices_and_context_window.json b/backend/resources/model-pricing/model_prices_and_context_window.json index b11a75f688..5e0f608d2f 100644 --- a/backend/resources/model-pricing/model_prices_and_context_window.json +++ b/backend/resources/model-pricing/model_prices_and_context_window.json @@ -2666,6 +2666,40 @@ "supports_reasoning": true, "supports_service_tier": true }, + "gemini-4-argon": { + "cache_read_input_token_cost": 1e-07, + "cache_read_input_token_cost_priority": 1.8e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_priority": 3.6e-06, + "litellm_provider": "gemini", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1e-05, + "output_cost_per_token_priority": 1.8e-05, + "source": "https://ai.google.dev/gemini-api/docs/pricing", + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_service_tier": true + }, + "gemini-4-argon-cyber-permissive": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_priority": 3.6e-07, + "input_cost_per_token": 4e-06, + "input_cost_per_token_priority": 7.2e-06, + "litellm_provider": "vertex_ai-language-models", + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 2e-05, + "output_cost_per_token_priority": 3.6e-05, + "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing", + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_service_tier": true + }, "gemini-embedding-001": { "input_cost_per_token": 1.5e-07, "litellm_provider": "vertex_ai-embedding-models",