Merge pull request #4875 from StarryKira/codex/fix-4859-gemini-36-flash-billing

fix(billing): price Antigravity Gemini 3.6 Flash
This commit is contained in:
Wesley Liddick
2026-07-27 11:43:49 +08:00
committed by GitHub
5 changed files with 156 additions and 6 deletions
@@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
@@ -698,6 +699,25 @@ func TestResolveAccountStatsCost_FallsBackToLiteLLM(t *testing.T) {
require.InDelta(t, 0.2, *result, 1e-12)
}
func TestResolveAccountStatsCost_Gemini36FlashTierUsesFallbackPricing(t *testing.T) {
channel := &Channel{
ID: 1,
Status: StatusActive,
ApplyPricingToAccountStats: false,
}
cs := newTestChannelServiceForStats(t, channel, 10, "antigravity")
bs := NewBillingService(&config.Config{}, nil)
result := resolveAccountStatsCost(
context.Background(),
cs, bs,
1, 10, "gemini-3.6-flash-low",
UsageTokens{InputTokens: 1_000_000, OutputTokens: 1_000_000, CacheReadTokens: 1_000_000}, 1, 0,
)
require.NotNil(t, result)
require.InDelta(t, 9.15, *result, 1e-12)
}
func TestResolveAccountStatsCost_AllMiss_ReturnsNil(t *testing.T) {
channel := &Channel{
ID: 1,
@@ -272,6 +272,17 @@ func (s *BillingService) initFallbackPricing() {
SupportsCacheBreakdown: false,
}
// Gemini 3.6 Flash (Google AI pricing: $1.50 input / $7.50 output /
// $0.15 cached input per MTok). Antigravity's -high/-low/-medium/-tiered
// aliases are matched below so unavailable remote pricing never records
// token-bearing requests at $0.
s.fallbackPrices["gemini-3.6-flash"] = &ModelPricing{
InputPricePerToken: 1.5e-6,
OutputPricePerToken: 7.5e-6,
CacheReadPricePerToken: 0.15e-6,
SupportsCacheBreakdown: false,
}
// OpenAI GPT-5.4(业务指定价格)
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
InputPricePerToken: 2.5e-6, // $2.5 per MTok
@@ -630,6 +641,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
return s.fallbackPrices["gemini-3.1-pro"]
}
if strings.Contains(modelLower, "gemini-3.6-flash") || strings.Contains(modelLower, "gemini-3-6-flash") {
return s.fallbackPrices["gemini-3.6-flash"]
}
// DeepSeek V4 系列:仅匹配已知 V4 Pro/Flash 与官方兼容别名
// (deepseek-chat / deepseek-reasoner → V4 Flash),未知 deepseek-* 型号不回退,避免误计价。
+28 -6
View File
@@ -695,16 +695,24 @@ func (s *PricingService) GetModelPricing(modelName string) *LiteLLMModelPricing
}
func (s *PricingService) buildModelLookupCandidates(modelLower string) []string {
// Prefer canonical model name first (this also improves billing compatibility with "models/xxx").
candidates := []string{
normalizeModelNameForPricing(modelLower),
rawCandidates := []string{
modelLower,
}
candidates = append(candidates,
strings.TrimPrefix(modelLower, "models/"),
lastSegment(modelLower),
lastSegment(strings.TrimPrefix(modelLower, "models/")),
)
}
normalized := normalizeModelNameForPricing(modelLower)
// A tier-specific entry should take precedence when the pricing catalog gains
// one later. Today Antigravity's Gemini 3.6 Flash tiers share the base rate,
// so the normalized base remains the fallback after the exact aliases.
candidates := rawCandidates
if normalizeGeminiThinkingTierAlias(lastSegment(modelLower)) != lastSegment(modelLower) {
candidates = append(candidates, normalized)
} else {
// Prefer canonical model names for all other aliases (including models/xxx).
candidates = append([]string{normalized}, candidates...)
}
seen := make(map[string]struct{}, len(candidates))
out := make([]string, 0, len(candidates))
@@ -752,6 +760,20 @@ func normalizeModelNameForPricing(model string) string {
}
return canonical
}
return normalizeGeminiThinkingTierAlias(model)
}
// normalizeGeminiThinkingTierAlias maps Antigravity's Gemini 3.6 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 same price card as gemini-3.6-flash.
func normalizeGeminiThinkingTierAlias(model string) string {
const baseModel = "gemini-3.6-flash"
for _, tier := range []string{"-high", "-low", "-medium", "-tiered"} {
if model == baseModel+tier {
return baseModel
}
}
return model
}
@@ -454,6 +454,83 @@ func TestGetModelPricing_OpenAICompactAliasUsesStaticFallback(t *testing.T) {
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
}
func TestPricingService_Gemini36FlashThinkingTiersUseBasePricing(t *testing.T) {
basePricing := &LiteLLMModelPricing{
InputCostPerToken: 1.5e-6,
OutputCostPerToken: 7.5e-6,
CacheReadInputTokenCost: 0.15e-6,
}
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
"gemini-3.6-flash": basePricing,
}}
for _, model := range []string{
"gemini-3.6-flash",
"gemini-3.6-flash-high",
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-tiered",
} {
t.Run(model, func(t *testing.T) {
require.Same(t, basePricing, svc.GetModelPricing(model))
})
}
}
func TestPricingService_Gemini36FlashTierSpecificPricingTakesPrecedence(t *testing.T) {
basePricing := &LiteLLMModelPricing{InputCostPerToken: 1.5e-6}
tierPricing := &LiteLLMModelPricing{InputCostPerToken: 2e-6}
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
"gemini-3.6-flash": basePricing,
"gemini-3.6-flash-low": tierPricing,
}}
require.Same(t, tierPricing, svc.GetModelPricing("models/gemini-3.6-flash-low"))
}
func TestBillingService_Gemini36FlashThinkingTierFallbacksAreBillable(t *testing.T) {
svc := NewBillingService(&config.Config{}, nil)
tokens := UsageTokens{InputTokens: 1_000_000, OutputTokens: 1_000_000, CacheReadTokens: 1_000_000}
for _, model := range []string{
"gemini-3.6-flash",
"gemini-3.6-flash-high",
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-tiered",
} {
t.Run(model, func(t *testing.T) {
cost, err := svc.CalculateCost(model, tokens, 1)
require.NoError(t, err)
require.InDelta(t, 1.5, cost.InputCost, 1e-12)
require.InDelta(t, 7.5, cost.OutputCost, 1e-12)
require.InDelta(t, 0.15, cost.CacheReadCost, 1e-12)
require.InDelta(t, 9.15, cost.TotalCost, 1e-12)
})
}
}
func TestDefaultPricingIncludesGemini36FlashRates(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
billingSvc := NewBillingService(&config.Config{}, pricingSvc)
for _, model := range []string{"gemini-3.6-flash", "gemini-3.6-flash-low", "gemini-3.6-flash-high"} {
t.Run(model, func(t *testing.T) {
pricing, err := billingSvc.GetModelPricing(model)
require.NoError(t, err)
require.InDelta(t, 1.5e-6, pricing.InputPricePerToken, 1e-12)
require.InDelta(t, 7.5e-6, pricing.OutputPricePerToken, 1e-12)
require.InDelta(t, 0.15e-6, pricing.CacheReadPricePerToken, 1e-12)
})
}
}
func TestDefaultPricingIncludesCodexAutoReview(t *testing.T) {
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
require.NoError(t, err)
@@ -2339,6 +2339,23 @@
"supports_web_search": true,
"web_search_billing_unit": "per_query"
},
"gemini-3.6-flash": {
"cache_read_input_token_cost": 1.5e-07,
"cache_read_input_token_cost_priority": 2.7e-07,
"input_cost_per_token": 1.5e-06,
"input_cost_per_token_priority": 2.7e-06,
"litellm_provider": "vertex_ai-language-models",
"max_input_tokens": 1048576,
"max_output_tokens": 65536,
"max_tokens": 65536,
"mode": "chat",
"output_cost_per_token": 7.5e-06,
"output_cost_per_token_priority": 1.35e-05,
"source": "https://ai.google.dev/gemini-api/docs/latest-model",
"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",