mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
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:
@@ -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-* 型号不回退,避免误计价。
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user