fix(billing): resolve Gemini 3.7/3.8 Flash thinking-tier prices

This commit is contained in:
Varrge
2026-09-05 15:54:38 +08:00
parent 07bf8b92fd
commit 3bdb869fca
3 changed files with 64 additions and 28 deletions
@@ -194,6 +194,42 @@ func TestGatewayServiceRecordUsage_PreservesRequestedAndUpstreamModels(t *testin
require.Equal(t, mappedModel, *usageRepo.lastLog.UpstreamModel)
}
func TestGatewayServiceRecordUsage_GeminiFlashThinkingTierUsesCatalogPrice(t *testing.T) {
for _, baseModel := range []string{"gemini-3.7-flash", "gemini-3.8-flash"} {
t.Run(baseModel, func(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
svc := newGatewayRecordUsageServiceForTest(usageRepo, userRepo, &openAIRecordUsageSubRepoStub{})
svc.billingService = NewBillingService(svc.cfg, &PricingService{pricingData: map[string]*LiteLLMModelPricing{
baseModel: {InputCostPerToken: 0.75e-6, OutputCostPerToken: 3.75e-6, CacheReadInputTokenCost: 0.075e-6},
}})
svc.resolver = NewModelPricingResolver(nil, svc.billingService)
group := &Group{ID: 27, Platform: PlatformGemini, RateMultiplier: 0.15}
model := baseModel + "-medium"
err := svc.RecordUsage(context.Background(), &RecordUsageInput{
Result: &ForwardResult{
RequestID: "gemini_thinking_tier",
Model: model,
UpstreamModel: model,
Usage: ClaudeUsage{InputTokens: 8498, OutputTokens: 469, CacheReadInputTokens: 159248},
Duration: time.Second,
},
APIKey: &APIKey{ID: 501, GroupID: &group.ID, Group: group},
User: &User{ID: 601},
Account: &Account{ID: 701, Platform: PlatformGemini, Type: AccountTypeAPIKey},
})
require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog)
require.Equal(t, model, usageRepo.lastLog.Model)
require.InDelta(t, 0.02007585, usageRepo.lastLog.TotalCost, 1e-12)
require.InDelta(t, 0.0030113775, usageRepo.lastLog.ActualCost, 1e-12)
require.InDelta(t, 0.0030113775, userRepo.lastAmount, 1e-12)
})
}
}
func TestGatewayServiceRecordUsage_PreservesChannelMappedUpstreamModel(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
svc := newGatewayRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{})
+8 -7
View File
@@ -1190,7 +1190,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. Today Antigravity's Gemini 3.6 Flash tiers share the base rate,
// one later. Antigravity's Gemini Flash 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) {
@@ -1249,15 +1249,16 @@ func normalizeModelNameForPricing(model string) string {
return normalizeGeminiThinkingTierAlias(model)
}
// normalizeGeminiThinkingTierAlias maps Antigravity's Gemini 3.6 Flash
// 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 same price card as gemini-3.6-flash.
// -tiered requests on the corresponding base model's price card.
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
for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"} {
for _, tier := range []string{"-high", "-low", "-medium", "-tiered"} {
if model == baseModel+tier {
return baseModel
}
}
}
return model
@@ -477,38 +477,37 @@ func TestGetModelPricing_OpenAICompactAliasUsesStaticFallback(t *testing.T) {
require.InDelta(t, 1.5e-5, got.OutputCostPerToken, 1e-12)
}
func TestPricingService_Gemini36FlashThinkingTiersUseBasePricing(t *testing.T) {
func TestPricingService_GeminiFlashThinkingTiersUseBasePricing(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))
})
for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"} {
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{baseModel: basePricing}}
for _, tier := range []string{"", "-high", "-low", "-medium", "-tiered"} {
model := baseModel + tier
t.Run(model, func(t *testing.T) {
require.Same(t, basePricing, svc.GetModelPricing(model))
require.Same(t, basePricing, svc.GetIdentifiedModelPricing("models/"+model))
})
}
}
}
func TestPricingService_Gemini36FlashTierSpecificPricingTakesPrecedence(t *testing.T) {
func TestPricingService_GeminiFlashTierSpecificPricingTakesPrecedence(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,
}}
for _, baseModel := range []string{"gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"} {
t.Run(baseModel, func(t *testing.T) {
svc := &PricingService{pricingData: map[string]*LiteLLMModelPricing{
baseModel: basePricing,
baseModel + "-low": tierPricing,
}}
require.Same(t, tierPricing, svc.GetModelPricing("models/gemini-3.6-flash-low"))
require.Same(t, tierPricing, svc.GetModelPricing("models/"+baseModel+"-low"))
})
}
}
func TestBillingService_Gemini36FlashThinkingTierFallbacksAreBillable(t *testing.T) {