diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 0d3bf88705..044e07d7a2 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -68,6 +68,9 @@ const ( SubscriptionStatusSuspended = "suspended" ) +// AntigravityGemini31ProAgentModel is the upstream route for Gemini 3.1 Pro High. +const AntigravityGemini31ProAgentModel = "gemini-pro-agent" + // DefaultAntigravityModelMapping 是 Antigravity 平台的默认模型映射 // 当账号未配置 model_mapping 时使用此默认值 // 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致 @@ -103,10 +106,12 @@ var DefaultAntigravityModelMapping = map[string]string{ "gemini-3-flash-preview": "gemini-3-flash", "gemini-3-pro-preview": "gemini-3-pro-high", // Gemini 3.1 白名单 - "gemini-3.1-pro-high": "gemini-3.1-pro-high", - "gemini-3.1-pro-low": "gemini-3.1-pro-low", + AntigravityGemini31ProAgentModel: AntigravityGemini31ProAgentModel, + "gemini-3.1-pro": AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-high": AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-low": "gemini-3.1-pro-low", // Gemini 3.1 preview 映射 - "gemini-3.1-pro-preview": "gemini-3.1-pro-high", + "gemini-3.1-pro-preview": AntigravityGemini31ProAgentModel, // Gemini 3.1 image 白名单 "gemini-3.1-flash-image": "gemini-3.1-flash-image", // Gemini 3.1 image preview 映射 diff --git a/backend/internal/domain/constants_test.go b/backend/internal/domain/constants_test.go index 0b24aea915..0fb9054f7e 100644 --- a/backend/internal/domain/constants_test.go +++ b/backend/internal/domain/constants_test.go @@ -43,6 +43,28 @@ func TestDefaultAntigravityModelMapping_ContainsNewClaudeModels(t *testing.T) { } } +func TestDefaultAntigravityModelMapping_Gemini31ProAliases(t *testing.T) { + t.Parallel() + + cases := map[string]string{ + AntigravityGemini31ProAgentModel: AntigravityGemini31ProAgentModel, + "gemini-3.1-pro": AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-high": AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-preview": AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-low": "gemini-3.1-pro-low", + } + + for from, want := range cases { + got, ok := DefaultAntigravityModelMapping[from] + if !ok { + t.Fatalf("expected mapping for %q to exist", from) + } + if got != want { + t.Fatalf("unexpected mapping for %q: got %q want %q", from, got, want) + } + } +} + func TestDefaultBedrockModelMapping_ContainsNewClaudeModels(t *testing.T) { t.Parallel() diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index 7d5e70a15b..26af0a7281 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -580,6 +580,7 @@ func (a *Account) resolveModelMapping(rawMapping map[string]any) map[string]stri "gemini-3.1-pro-high", "gemini-3.1-pro-low", }) + applyAntigravityGemini31ProAliases(result) } return result } @@ -646,6 +647,61 @@ func ensureAntigravityDefaultPassthroughs(mapping map[string]string, models []st } } +func applyAntigravityGemini31ProAliases(mapping map[string]string) { + target := strings.TrimSpace(mapping[domain.AntigravityGemini31ProAgentModel]) + if target == "" { + return + } + + aliases := []struct { + model string + legacyTargets map[string]struct{} + }{ + { + model: "gemini-3.1-pro", + legacyTargets: map[string]struct{}{ + "gemini-3.1-pro": {}, + }, + }, + { + model: "gemini-3.1-pro-high", + legacyTargets: map[string]struct{}{ + "gemini-3.1-pro-high": {}, + }, + }, + { + model: "gemini-3.1-pro-preview", + legacyTargets: map[string]struct{}{ + "gemini-3.1-pro-preview": {}, + "gemini-3.1-pro-high": {}, + }, + }, + } + + for _, alias := range aliases { + current, exists := mapping[alias.model] + if exists { + if _, legacy := alias.legacyTargets[current]; legacy { + mapping[alias.model] = target + } + continue + } + if mappingHasWildcardForModel(mapping, alias.model) { + continue + } + mapping[alias.model] = target + } +} + +func mappingHasWildcardForModel(mapping map[string]string, model string) bool { + for pattern := range mapping { + if matchWildcard(pattern, model) { + return true + } + } + return false +} + func normalizeRequestedModelForLookup(platform, requestedModel string) string { trimmed := strings.TrimSpace(requestedModel) if trimmed == "" { diff --git a/backend/internal/service/account_wildcard_test.go b/backend/internal/service/account_wildcard_test.go index d903b940a5..6ce804bb16 100644 --- a/backend/internal/service/account_wildcard_test.go +++ b/backend/internal/service/account_wildcard_test.go @@ -4,6 +4,8 @@ package service import ( "testing" + + "github.com/Wei-Shaw/sub2api/internal/domain" ) func TestMatchWildcard(t *testing.T) { @@ -320,6 +322,86 @@ func TestAccountGetMappedModel(t *testing.T) { } } +func TestAccountGetModelMapping_AntigravityNormalizesGemini31ProAliases(t *testing.T) { + t.Parallel() + + account := &Account{ + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-high": "gemini-3.1-pro-high", + "gemini-3.1-pro-preview": "gemini-3.1-pro-high", + }, + }, + } + + mapping := account.GetModelMapping() + + if got := mapping["gemini-3.1-pro"]; got != domain.AntigravityGemini31ProAgentModel { + t.Fatalf("expected gemini-3.1-pro to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got) + } + if got := mapping["gemini-3.1-pro-high"]; got != domain.AntigravityGemini31ProAgentModel { + t.Fatalf("expected gemini-3.1-pro-high to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got) + } + if got := mapping["gemini-3.1-pro-preview"]; got != domain.AntigravityGemini31ProAgentModel { + t.Fatalf("expected gemini-3.1-pro-preview to map to %q, got %q", domain.AntigravityGemini31ProAgentModel, got) + } +} + +func TestAccountGetModelMapping_AntigravityPreservesGemini31ProOverrides(t *testing.T) { + t.Parallel() + + account := &Account{ + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel, + "gemini-3.1-pro-high": "custom-high", + "gemini-3.1-pro-preview": "custom-preview", + }, + }, + } + + mapping := account.GetModelMapping() + + if got := mapping["gemini-3.1-pro-high"]; got != "custom-high" { + t.Fatalf("expected gemini-3.1-pro-high override to be preserved, got %q", got) + } + if got := mapping["gemini-3.1-pro-preview"]; got != "custom-preview" { + t.Fatalf("expected gemini-3.1-pro-preview override to be preserved, got %q", got) + } + if got := mapping["gemini-3.1-pro"]; got != domain.AntigravityGemini31ProAgentModel { + t.Fatalf("expected gemini-3.1-pro alias to default to %q, got %q", domain.AntigravityGemini31ProAgentModel, got) + } +} + +func TestAccountGetModelMapping_AntigravityGemini31ProAliasesRespectWildcard(t *testing.T) { + t.Parallel() + + account := &Account{ + Platform: PlatformAntigravity, + Credentials: map[string]any{ + "model_mapping": map[string]any{ + domain.AntigravityGemini31ProAgentModel: domain.AntigravityGemini31ProAgentModel, + "gemini-3.1-*": "custom-wildcard", + }, + }, + } + + mapping := account.GetModelMapping() + + if got := mapping["gemini-3.1-pro"]; got != "" { + t.Fatalf("expected gemini-3.1-pro exact alias to stay unset when wildcard exists, got %q", got) + } + if got := mapping["gemini-3.1-pro-high"]; got != "" { + t.Fatalf("expected gemini-3.1-pro-high exact alias to stay unset when wildcard exists, got %q", got) + } + if got := mapping["gemini-3.1-pro-preview"]; got != "" { + t.Fatalf("expected gemini-3.1-pro-preview exact alias to stay unset when wildcard exists, got %q", got) + } +} + func TestAccountResolveMappedModel(t *testing.T) { tests := []struct { name string diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index 7f3157dfff..d7e70e309d 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -58,6 +58,12 @@ describe('useModelWhitelist', () => { expect(models.indexOf('gemini-2.5-flash-image')).toBeLessThan(models.indexOf('gemini-2.5-flash-lite')) }) + it('antigravity 模型列表包含 Gemini 3.1 Pro 通用别名', () => { + const models = getModelsByPlatform('antigravity') + + expect(models).toContain('gemini-3.1-pro') + }) + it('whitelist 模式会忽略通配符条目', () => { const mapping = buildModelMappingObject('whitelist', ['claude-*', 'gemini-3.1-flash-image'], []) expect(mapping).toEqual({ diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 654e1bb973..f5f933520b 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -74,6 +74,7 @@ const antigravityModels = [ 'gemini-3-pro-high', 'gemini-3-pro-low', // Gemini 3.1 系列 + 'gemini-3.1-pro', 'gemini-3.1-pro-high', 'gemini-3.1-pro-low', 'gemini-3-pro-image', @@ -311,11 +312,13 @@ const antigravityPresetMappings = [ { label: 'Sonnet3.5→4.6', from: 'claude-3-5-sonnet-20241022', to: 'claude-sonnet-4-6', color: 'bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400' }, { label: 'Opus4.5→4.6', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-6-thinking', color: 'bg-violet-100 text-violet-700 hover:bg-violet-200 dark:bg-violet-900/30 dark:text-violet-400' }, // Gemini 3→3.1 映射 - { label: '3-Pro-Preview→3.1-Pro-High', from: 'gemini-3-pro-preview', to: 'gemini-3.1-pro-high', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, - { label: '3-Pro-High→3.1-Pro-High', from: 'gemini-3-pro-high', to: 'gemini-3.1-pro-high', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, + { label: '3-Pro-Preview→Pro-Agent', from: 'gemini-3-pro-preview', to: 'gemini-pro-agent', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, + { label: '3-Pro-High→Pro-Agent', from: 'gemini-3-pro-high', to: 'gemini-pro-agent', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, { label: '3-Pro-Low→3.1-Pro-Low', from: 'gemini-3-pro-low', to: 'gemini-3.1-pro-low', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, - { label: '3.1-Pro-High passthrough', from: 'gemini-3.1-pro-high', to: 'gemini-3.1-pro-high', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, + { label: '3.1-Pro→Pro-Agent', from: 'gemini-3.1-pro', to: 'gemini-pro-agent', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, + { label: '3.1-Pro-High→Pro-Agent', from: 'gemini-3.1-pro-high', to: 'gemini-pro-agent', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' }, { label: '3.1-Pro-Low passthrough', from: 'gemini-3.1-pro-low', to: 'gemini-3.1-pro-low', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, + { label: '3.1-Pro-Preview→Pro-Agent', from: 'gemini-3.1-pro-preview', to: 'gemini-pro-agent', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }, // Gemini 通配符映射 { label: 'Gemini 3→Flash', from: 'gemini-3*', to: 'gemini-3-flash', color: 'bg-yellow-100 text-yellow-700 hover:bg-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400' }, { label: 'Gemini 2.5→Flash', from: 'gemini-2.5*', to: 'gemini-2.5-flash', color: 'bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400' },