mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-07 16:53:24 +00:00
fix(billing): price Antigravity Gemini 3.6 Flash
This commit is contained in:
1 parent
2730c1c43b
commit
6c7625800a
5 files changed
+156
-6
No files matched your search
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -698,6 +699,25 @@ func TestResolveAccountStatsCost_FallsBackToLiteLLM(t *testing.T) {
|
|||||||
require.InDelta(t, 0.2, *result, 1e-12)
|
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) {
|
func TestResolveAccountStatsCost_AllMiss_ReturnsNil(t *testing.T) {
|
||||||
channel := &Channel{
|
channel := &Channel{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
|
|||||||
@@ -272,6 +272,17 @@ func (s *BillingService) initFallbackPricing() {
|
|||||||
SupportsCacheBreakdown: false,
|
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(业务指定价格)
|
// OpenAI GPT-5.4(业务指定价格)
|
||||||
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
|
s.fallbackPrices["gpt-5.4"] = &ModelPricing{
|
||||||
InputPricePerToken: 2.5e-6, // $2.5 per MTok
|
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") {
|
if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
|
||||||
return s.fallbackPrices["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 V4 系列:仅匹配已知 V4 Pro/Flash 与官方兼容别名
|
||||||
// (deepseek-chat / deepseek-reasoner → V4 Flash),未知 deepseek-* 型号不回退,避免误计价。
|
// (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 {
|
func (s *PricingService) buildModelLookupCandidates(modelLower string) []string {
|
||||||
// Prefer canonical model name first (this also improves billing compatibility with "models/xxx").
|
rawCandidates := []string{
|
||||||
candidates := []string{
|
|
||||||
normalizeModelNameForPricing(modelLower),
|
|
||||||
modelLower,
|
modelLower,
|
||||||
}
|
|
||||||
candidates = append(candidates,
|
|
||||||
strings.TrimPrefix(modelLower, "models/"),
|
strings.TrimPrefix(modelLower, "models/"),
|
||||||
lastSegment(modelLower),
|
lastSegment(modelLower),
|
||||||
lastSegment(strings.TrimPrefix(modelLower, "models/")),
|
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))
|
seen := make(map[string]struct{}, len(candidates))
|
||||||
out := make([]string, 0, len(candidates))
|
out := make([]string, 0, len(candidates))
|
||||||
@@ -752,6 +760,20 @@ func normalizeModelNameForPricing(model string) string {
|
|||||||
}
|
}
|
||||||
return canonical
|
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
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -454,6 +454,83 @@ func TestGetModelPricing_OpenAICompactAliasUsesStaticFallback(t *testing.T) {
|
|||||||
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
|
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) {
|
func TestDefaultPricingIncludesCodexAutoReview(t *testing.T) {
|
||||||
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
|
data, err := os.ReadFile(filepath.Join("..", "..", "resources", "model-pricing", "model_prices_and_context_window.json"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -2339,6 +2339,23 @@
|
|||||||
"supports_web_search": true,
|
"supports_web_search": true,
|
||||||
"web_search_billing_unit": "per_query"
|
"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": {
|
"gemini-embedding-001": {
|
||||||
"input_cost_per_token": 1.5e-07,
|
"input_cost_per_token": 1.5e-07,
|
||||||
"litellm_provider": "vertex_ai-embedding-models",
|
"litellm_provider": "vertex_ai-embedding-models",
|
||||||
|
|||||||
Reference in new issue
Block a user