Skip to content

Commit abd9cc8

Browse files
feeeeiGo1c
authored andcommitted
feat(billing): 补齐 Gemini 4 Argon 计费(移植上游 PR Wei-Shaw#8078)
- billing_service: 新增 gemini-4-argon($2/$10/$0.10 每百万 token,优惠价至 2026-11-30)与 gemini-4-argon-cyber-permissive($4/$20/$0.20,仅 Vertex)兜底价卡;Cyber 变体先于基础型号匹配 - pricing_service: 引入 normalizeGeminiThinkingTierAlias,Antigravity 的 -high/-low/-medium/-tiered 档位名按基础型号计价 - model_prices_and_context_window.json: 快照新增两个条目(含 Priority 档 1.8 倍价) - 新增档位解析、兜底计费、快照条目三组单测 适配说明:dev 未同步上游 gemini-3.6/3.7/3.8-flash 价卡与显式缓存存储价功能,故未携带 flash 价卡、flash 兜底匹配分支及 CacheStorage 断言;gemini_cache_storage_pricing_test.go 维持 dev 的删除状态。 Upstream-PR: Wei-Shaw#8078 (cherry picked from commit c6c9935)
1 parent 1398ed7 commit abd9cc8

4 files changed

Lines changed: 177 additions & 6 deletions

File tree

‎backend/internal/service/billing_service.go‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,27 @@ func (s *BillingService) initFallbackPricing() {
366366
SupportsCacheBreakdown: false,
367367
}
368368

369+
// Gemini 4 Argon (Google AI pricing: $2 input / $10 output / $0.10 cached
370+
// input per MTok, promotional through 2026-11-30; official rates double to
371+
// $4/$20/$0.20 from 2026-12-01). One rate covers every prompt length.
372+
// Antigravity's -high/-low/-medium aliases are matched below so unavailable
373+
// remote pricing never records token-bearing requests at $0.
374+
s.fallbackPrices["gemini-4-argon"] = &ModelPricing{
375+
InputPricePerToken: 2e-6,
376+
OutputPricePerToken: 10e-6,
377+
CacheReadPricePerToken: 0.1e-6,
378+
SupportsCacheBreakdown: false,
379+
}
380+
381+
// Gemini 4 Argon Cyber Permissive (Vertex AI pricing: $4 input / $20 output /
382+
// $0.20 cached input per MTok). The Gemini 4 Argon promotion does not apply.
383+
s.fallbackPrices["gemini-4-argon-cyber-permissive"] = &ModelPricing{
384+
InputPricePerToken: 4e-6,
385+
OutputPricePerToken: 20e-6,
386+
CacheReadPricePerToken: 0.2e-6,
387+
SupportsCacheBreakdown: false,
388+
}
389+
369390
// OpenAI GPT-5.4(业务指定价格)
370391
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
371392
InputPricePerToken: 2.5e-6, // $2.5 per MTok
@@ -639,6 +660,13 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
639660
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
640661
return s.fallbackPrices["gemini-3.1-pro"]
641662
}
663+
// Cyber 变体不享受 Gemini 4 Argon 的优惠价,必须先于 "gemini-4-argon" 判。
664+
if strings.Contains(modelLower, "gemini-4-argon-cyber") {
665+
return s.fallbackPrices["gemini-4-argon-cyber-permissive"]
666+
}
667+
if strings.Contains(modelLower, "gemini-4-argon") {
668+
return s.fallbackPrices["gemini-4-argon"]
669+
}
642670

643671
// OpenAI 仅匹配已知 GPT-5/Codex 族,避免未知 OpenAI 型号误计价。
644672
if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" {

‎backend/internal/service/pricing_service.go‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -967,16 +967,24 @@ func (s *PricingService) GetIdentifiedModelPricing(modelName string) *LiteLLMMod
967967
}
968968

969969
func (s *PricingService) buildModelLookupCandidates(modelLower string) []string {
970-
// Prefer canonical model name first (this also improves billing compatibility with "models/xxx").
971-
candidates := []string{
972-
normalizeModelNameForPricing(modelLower),
970+
rawCandidates := []string{
973971
modelLower,
974-
}
975-
candidates = append(candidates,
976972
strings.TrimPrefix(modelLower, "models/"),
977973
lastSegment(modelLower),
978974
lastSegment(strings.TrimPrefix(modelLower, "models/")),
979-
)
975+
}
976+
normalized := normalizeModelNameForPricing(modelLower)
977+
978+
// A tier-specific entry should take precedence when the pricing catalog gains
979+
// one later. Antigravity's Gemini thinking tiers share the base rate,
980+
// so the normalized base remains the fallback after the exact aliases.
981+
candidates := rawCandidates
982+
if normalizeGeminiThinkingTierAlias(lastSegment(modelLower)) != lastSegment(modelLower) {
983+
candidates = append(candidates, normalized)
984+
} else {
985+
// Prefer canonical model names for all other aliases (including models/xxx).
986+
candidates = append([]string{normalized}, candidates...)
987+
}
980988

981989
seen := make(map[string]struct{}, len(candidates))
982990
out := make([]string, 0, len(candidates))
@@ -1027,6 +1035,21 @@ func normalizeModelNameForPricing(model string) string {
10271035
}
10281036
return canonical
10291037
}
1038+
return normalizeGeminiThinkingTierAlias(model)
1039+
}
1040+
1041+
// normalizeGeminiThinkingTierAlias maps Antigravity's Gemini thinking-tier
1042+
// model IDs to the public base model. The tier controls reasoning behavior, not
1043+
// the published token rate, so this keeps -high/-low/-medium and -tiered
1044+
// requests on the corresponding base model's price card.
1045+
func normalizeGeminiThinkingTierAlias(model string) string {
1046+
for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "gemini-4-argon"} {
1047+
for _, tier := range []string{"-high", "-low", "-medium", "-tiered"} {
1048+
if model == baseModel+tier {
1049+
return baseModel
1050+
}
1051+
}
1052+
}
10301053
return model
10311054
}
10321055

‎backend/internal/service/pricing_service_test.go‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,92 @@ func TestGetModelPricing_OpenAICompactAliasUsesStaticFallback(t *testing.T) {
750750
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
751751
}
752752

753+
func TestPricingService_Gemini4ArgonThinkingTiersUseBasePricing(t *testing.T) {
754+
basePricing := &LiteLLMModelPricing{InputCostPerToken: 2e-6, OutputCostPerToken: 10e-6, CacheReadInputTokenCost: 0.1e-6}
755+
cyberPricing := &LiteLLMModelPricing{InputCostPerToken: 4e-6, OutputCostPerToken: 20e-6, CacheReadInputTokenCost: 0.2e-6}
756+
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
757+
"gemini-4-argon": basePricing,
758+
"gemini-4-argon-cyber-permissive": cyberPricing,
759+
}}
760+
761+
for _, model := range []string{
762+
"gemini-4-argon",
763+
"gemini-4-argon-high",
764+
"gemini-4-argon-low",
765+
"gemini-4-argon-medium",
766+
"models/gemini-4-argon-high",
767+
"projects/p/locations/global/publishers/google/models/gemini-4-argon",
768+
} {
769+
t.Run(model, func(t *testing.T) {
770+
require.Same(t, basePricing, svc.GetModelPricing(model))
771+
})
772+
}
773+
require.Same(t, cyberPricing, svc.GetModelPricing("gemini-4-argon-cyber-permissive"))
774+
}
775+
776+
func TestBillingService_Gemini4ArgonFallbacksAreBillable(t *testing.T) {
777+
svc := NewBillingService(&config.Config{}, nil)
778+
tokens := UsageTokens{InputTokens: 1_000_000, OutputTokens: 1_000_000, CacheReadTokens: 1_000_000}
779+
780+
for _, tc := range []struct {
781+
model string
782+
input, output, read float64
783+
}{
784+
{"gemini-4-argon", 2, 10, 0.1},
785+
{"gemini-4-argon-high", 2, 10, 0.1},
786+
{"gemini-4-argon-low", 2, 10, 0.1},
787+
{"gemini-4-argon-medium", 2, 10, 0.1},
788+
{"models/gemini-4-argon", 2, 10, 0.1},
789+
{"gemini-4-argon-cyber-permissive", 4, 20, 0.2},
790+
{"gemini-4-argon-cyber", 4, 20, 0.2},
791+
{"publishers/google/models/gemini-4-argon-cyber-permissive", 4, 20, 0.2},
792+
} {
793+
t.Run(tc.model, func(t *testing.T) {
794+
cost, err := svc.CalculateCost(tc.model, tokens, 1)
795+
require.NoError(t, err)
796+
require.InDelta(t, tc.input, cost.InputCost, 1e-12)
797+
require.InDelta(t, tc.output, cost.OutputCost, 1e-12)
798+
require.InDelta(t, tc.read, cost.CacheReadCost, 1e-12)
799+
require.InDelta(t, tc.input+tc.output+tc.read, cost.TotalCost, 1e-12)
800+
})
801+
}
802+
}
803+
804+
func TestDefaultPricingIncludesGemini4ArgonRates(t *testing.T) {
805+
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
806+
require.NoError(t, err)
807+
808+
pricingSvc := &PricingService{}
809+
pricingData, err := pricingSvc.parsePricingData(data)
810+
require.NoError(t, err)
811+
pricingSvc.pricingData = pricingData
812+
813+
for _, tc := range []struct {
814+
model string
815+
provider string
816+
input, output, read float64
817+
}{
818+
{"gemini-4-argon", "gemini", 2e-6, 10e-6, 0.1e-6},
819+
{"gemini-4-argon-high", "gemini", 2e-6, 10e-6, 0.1e-6},
820+
{"gemini-4-argon-cyber-permissive", "vertex_ai-language-models", 4e-6, 20e-6, 0.2e-6},
821+
} {
822+
t.Run(tc.model, func(t *testing.T) {
823+
pricing := pricingSvc.GetModelPricing(tc.model)
824+
require.NotNil(t, pricing)
825+
require.Equal(t, tc.provider, pricing.LiteLLMProvider)
826+
require.InDelta(t, tc.input, pricing.InputCostPerToken, 1e-15)
827+
require.InDelta(t, tc.output, pricing.OutputCostPerToken, 1e-15)
828+
require.InDelta(t, tc.read, pricing.CacheReadInputTokenCost, 1e-15)
829+
// Priority 档为标准价的 1.8 倍;单一价档不分提示词长度。
830+
require.InDelta(t, tc.input*1.8, pricing.InputCostPerTokenPriority, 1e-15)
831+
require.InDelta(t, tc.output*1.8, pricing.OutputCostPerTokenPriority, 1e-15)
832+
require.InDelta(t, tc.read*1.8, pricing.CacheReadInputTokenCostPriority, 1e-15)
833+
require.Zero(t, pricing.LongContextInputTokenThreshold)
834+
})
835+
}
836+
require.Contains(t, pricingSvc.ListModelNamesByProvider("gemini"), "gemini-4-argon")
837+
}
838+
753839
func TestDefaultPricingIncludesCodexAutoReview(t *testing.T) {
754840
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
755841
require.NoError(t, err)

‎backend/resources/model-pricing/model_prices_and_context_window.json‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2579,6 +2579,40 @@
25792579
"supports_web_search": true,
25802580
"web_search_billing_unit": "per_query"
25812581
},
2582+
"gemini-4-argon": {
2583+
"cache_read_input_token_cost": 1e-07,
2584+
"cache_read_input_token_cost_priority": 1.8e-07,
2585+
"input_cost_per_token": 2e-06,
2586+
"input_cost_per_token_priority": 3.6e-06,
2587+
"litellm_provider": "gemini",
2588+
"max_input_tokens": 1048576,
2589+
"max_output_tokens": 65536,
2590+
"max_tokens": 65536,
2591+
"mode": "chat",
2592+
"output_cost_per_token": 1e-05,
2593+
"output_cost_per_token_priority": 1.8e-05,
2594+
"source": "https://ai.google.dev/gemini-api/docs/pricing",
2595+
"supports_prompt_caching": true,
2596+
"supports_reasoning": true,
2597+
"supports_service_tier": true
2598+
},
2599+
"gemini-4-argon-cyber-permissive": {
2600+
"cache_read_input_token_cost": 2e-07,
2601+
"cache_read_input_token_cost_priority": 3.6e-07,
2602+
"input_cost_per_token": 4e-06,
2603+
"input_cost_per_token_priority": 7.2e-06,
2604+
"litellm_provider": "vertex_ai-language-models",
2605+
"max_input_tokens": 1048576,
2606+
"max_output_tokens": 65536,
2607+
"max_tokens": 65536,
2608+
"mode": "chat",
2609+
"output_cost_per_token": 2e-05,
2610+
"output_cost_per_token_priority": 3.6e-05,
2611+
"source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing",
2612+
"supports_prompt_caching": true,
2613+
"supports_reasoning": true,
2614+
"supports_service_tier": true
2615+
},
25822616
"gemini-embedding-001": {
25832617
"input_cost_per_token": 1.5e-07,
25842618
"litellm_provider": "vertex_ai-embedding-models",

0 commit comments

Comments
 (0)