diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 429486c3..a57f7067 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -71,6 +71,7 @@ const ( // 与前端 useModelWhitelist.ts 中的 antigravityDefaultMappings 保持一致 var DefaultAntigravityModelMapping = map[string]string{ // Claude 白名单 + "claude-opus-4-7": "claude-opus-4-7", // 官方模型 "claude-opus-4-6-thinking": "claude-opus-4-6-thinking", // 官方模型 "claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射 "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型 @@ -120,6 +121,7 @@ var DefaultAntigravityModelMapping = map[string]string{ // aws_region 自动调整为匹配的区域前缀(如 eu.、apac.、jp. 等) var DefaultBedrockModelMapping = map[string]string{ // Claude Opus + "claude-opus-4-7": "us.anthropic.claude-opus-4-7-v1", "claude-opus-4-6-thinking": "us.anthropic.claude-opus-4-6-v1", "claude-opus-4-6": "us.anthropic.claude-opus-4-6-v1", "claude-opus-4-5-thinking": "us.anthropic.claude-opus-4-5-20251101-v1:0", diff --git a/backend/internal/pkg/antigravity/claude_types.go b/backend/internal/pkg/antigravity/claude_types.go index ce144bb9..0b8ae5f2 100644 --- a/backend/internal/pkg/antigravity/claude_types.go +++ b/backend/internal/pkg/antigravity/claude_types.go @@ -154,6 +154,7 @@ var claudeModels = []modelDef{ {ID: "claude-sonnet-4-5-thinking", DisplayName: "Claude Sonnet 4.5 Thinking", CreatedAt: "2025-09-29T00:00:00Z"}, {ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6", CreatedAt: "2026-02-05T00:00:00Z"}, {ID: "claude-opus-4-6-thinking", DisplayName: "Claude Opus 4.6 Thinking", CreatedAt: "2026-02-05T00:00:00Z"}, + {ID: "claude-opus-4-7", DisplayName: "Claude Opus 4.7", CreatedAt: "2026-04-17T00:00:00Z"}, {ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", CreatedAt: "2026-02-17T00:00:00Z"}, } diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index d13a8498..b5de8166 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -582,8 +582,12 @@ func maxOutputTokensLimit(model string) int { return maxOutputTokensUpperBound } -func isAntigravityOpus46Model(model string) bool { - return strings.HasPrefix(strings.ToLower(model), "claude-opus-4-6") +// isAntigravityOpusHighTierModel 判断是否为高阶 Opus 模型(4.6+), +// 用于 adaptive thinking 时覆写为高预算。 +func isAntigravityOpusHighTierModel(model string) bool { + lower := strings.ToLower(model) + return strings.HasPrefix(lower, "claude-opus-4-6") || + strings.HasPrefix(lower, "claude-opus-4-7") } func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig { @@ -605,12 +609,12 @@ func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig { } // - thinking.type=enabled:budget_tokens>0 用显式预算 - // - thinking.type=adaptive:仅在 Antigravity 的 Opus 4.6 上覆写为 (24576) + // - thinking.type=adaptive:在 Antigravity 的高阶 Opus(4.6+)上覆写为 (24576) budget := -1 if req.Thinking.BudgetTokens > 0 { budget = req.Thinking.BudgetTokens } - if req.Thinking.Type == "adaptive" && isAntigravityOpus46Model(req.Model) { + if req.Thinking.Type == "adaptive" && isAntigravityOpusHighTierModel(req.Model) { budget = ClaudeAdaptiveHighThinkingBudgetTokens } diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index dfca252f..21c723d2 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -83,6 +83,12 @@ var DefaultModels = []Model{ DisplayName: "Claude Opus 4.6", CreatedAt: "2026-02-06T00:00:00Z", }, + { + ID: "claude-opus-4-7", + Type: "model", + DisplayName: "Claude Opus 4.7", + CreatedAt: "2026-04-17T00:00:00Z", + }, { ID: "claude-sonnet-4-6", Type: "model", diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 763abadb..32a54cbe 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -191,6 +191,9 @@ func (s *BillingService) initFallbackPricing() { // Claude 4.6 Opus (与4.5同价) s.fallbackPrices["claude-opus-4.6"] = s.fallbackPrices["claude-opus-4.5"] + // Claude 4.7 Opus (暂与4.6同价,待官方定价更新) + s.fallbackPrices["claude-opus-4.7"] = s.fallbackPrices["claude-opus-4.6"] + // Gemini 3.1 Pro s.fallbackPrices["gemini-3.1-pro"] = &ModelPricing{ InputPricePerToken: 2e-6, // $2 per MTok @@ -278,6 +281,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { // 按模型系列匹配 if strings.Contains(modelLower, "opus") { + if strings.Contains(modelLower, "4.7") || strings.Contains(modelLower, "4-7") { + return s.fallbackPrices["claude-opus-4.7"] + } if strings.Contains(modelLower, "4.6") || strings.Contains(modelLower, "4-6") { return s.fallbackPrices["claude-opus-4.6"] } diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index 3b3f31c3..2bf48702 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -656,65 +656,95 @@ func (s *PricingService) extractBaseName(model string) string { // matchByModelFamily 基于模型系列匹配 func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing { - // Claude模型系列匹配规则 - familyPatterns := map[string][]string{ - "opus-4.6": {"claude-opus-4.6", "claude-opus-4-6"}, - "opus-4.5": {"claude-opus-4.5", "claude-opus-4-5"}, - "opus-4": {"claude-opus-4", "claude-3-opus"}, - "sonnet-4.5": {"claude-sonnet-4.5", "claude-sonnet-4-5"}, - "sonnet-4": {"claude-sonnet-4", "claude-3-5-sonnet"}, - "sonnet-3.5": {"claude-3-5-sonnet", "claude-3.5-sonnet"}, - "sonnet-3": {"claude-3-sonnet"}, - "haiku-3.5": {"claude-3-5-haiku", "claude-3.5-haiku"}, - "haiku-3": {"claude-3-haiku"}, + // modelFamily 定义一个模型系列的匹配和定价查找规则。 + type modelFamily struct { + name string // 系列名称 + match []string // 用于将模型归类到此系列的模式(strings.Contains 匹配) + pricing []string // 用于在定价数据中查找价格的模式(nil 则复用 match;可包含低版本 fallback) } - // 确定模型属于哪个系列 - var matchedFamily string - for family, patterns := range familyPatterns { - for _, pattern := range patterns { + // 按特异性降序排列:高版本号在前,避免 "claude-opus-4"(opus-4 系列) + // 因子串关系误匹配 "claude-opus-4-7"(opus-4.7 系列)。 + // 注意:原 map 实现存在 Go map 迭代随机性导致的同类 bug,此处改为有序切片修复。 + families := []modelFamily{ + {name: "opus-4.7", match: []string{"claude-opus-4-7", "claude-opus-4.7"}, pricing: []string{"claude-opus-4-7", "claude-opus-4.7", "claude-opus-4-6"}}, + {name: "opus-4.6", match: []string{"claude-opus-4-6", "claude-opus-4.6"}}, + {name: "opus-4.5", match: []string{"claude-opus-4-5", "claude-opus-4.5"}}, + {name: "opus-4", match: []string{"claude-opus-4", "claude-3-opus"}}, + {name: "sonnet-4.5", match: []string{"claude-sonnet-4-5", "claude-sonnet-4.5"}}, + {name: "sonnet-4", match: []string{"claude-sonnet-4", "claude-3-5-sonnet"}}, + {name: "sonnet-3.5", match: []string{"claude-3-5-sonnet", "claude-3.5-sonnet"}}, + {name: "sonnet-3", match: []string{"claude-3-sonnet"}}, + {name: "haiku-3.5", match: []string{"claude-3-5-haiku", "claude-3.5-haiku"}}, + {name: "haiku-3", match: []string{"claude-3-haiku"}}, + } + + // Phase 1: 按有序切片归类(最具体的系列优先匹配) + var matched *modelFamily + for i := range families { + for _, pattern := range families[i].match { if strings.Contains(model, pattern) || strings.Contains(model, strings.ReplaceAll(pattern, "-", "")) { - matchedFamily = family + matched = &families[i] break } } - if matchedFamily != "" { + if matched != nil { break } } - if matchedFamily == "" { - // 简单的系列匹配 - if strings.Contains(model, "opus") { - if strings.Contains(model, "4.5") || strings.Contains(model, "4-5") { - matchedFamily = "opus-4.5" - } else { - matchedFamily = "opus-4" + // Phase 2: 二次兜底——当模型 ID 不含已知模式串时,按关键字粗分 + if matched == nil { + var fallbackName string + switch { + case strings.Contains(model, "opus"): + switch { + case strings.Contains(model, "4.7") || strings.Contains(model, "4-7"): + fallbackName = "opus-4.7" + case strings.Contains(model, "4.6") || strings.Contains(model, "4-6"): + fallbackName = "opus-4.6" + case strings.Contains(model, "4.5") || strings.Contains(model, "4-5"): + fallbackName = "opus-4.5" + default: + fallbackName = "opus-4" } - } else if strings.Contains(model, "sonnet") { - if strings.Contains(model, "4.5") || strings.Contains(model, "4-5") { - matchedFamily = "sonnet-4.5" - } else if strings.Contains(model, "3-5") || strings.Contains(model, "3.5") { - matchedFamily = "sonnet-3.5" - } else { - matchedFamily = "sonnet-4" + case strings.Contains(model, "sonnet"): + switch { + case strings.Contains(model, "4.5") || strings.Contains(model, "4-5"): + fallbackName = "sonnet-4.5" + case strings.Contains(model, "3-5") || strings.Contains(model, "3.5"): + fallbackName = "sonnet-3.5" + default: + fallbackName = "sonnet-4" } - } else if strings.Contains(model, "haiku") { - if strings.Contains(model, "3-5") || strings.Contains(model, "3.5") { - matchedFamily = "haiku-3.5" - } else { - matchedFamily = "haiku-3" + case strings.Contains(model, "haiku"): + switch { + case strings.Contains(model, "3-5") || strings.Contains(model, "3.5"): + fallbackName = "haiku-3.5" + default: + fallbackName = "haiku-3" + } + } + if fallbackName != "" { + for i := range families { + if families[i].name == fallbackName { + matched = &families[i] + break + } } } } - if matchedFamily == "" { + if matched == nil { return nil } - // 在价格数据中查找该系列的模型 - patterns := familyPatterns[matchedFamily] - for _, pattern := range patterns { + // Phase 3: 在定价数据中查找该系列的价格 + lookups := matched.pricing + if lookups == nil { + lookups = matched.match + } + for _, pattern := range lookups { for key, pricing := range s.pricingData { keyLower := strings.ToLower(key) if strings.Contains(keyLower, pattern) { diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index d16ed977..a282ae7d 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -43,6 +43,7 @@ export const claudeModels = [ 'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001', 'claude-opus-4-5-20251101', 'claude-opus-4-6', + 'claude-opus-4-7', 'claude-sonnet-4-6', 'claude-2.1', 'claude-2.0', 'claude-instant-1.2' ] @@ -66,6 +67,7 @@ const antigravityModels = [ // Claude 4.5+ 系列 'claude-opus-4-6', 'claude-opus-4-6-thinking', + 'claude-opus-4-7', 'claude-opus-4-5-thinking', 'claude-sonnet-4-6', 'claude-sonnet-4-5', @@ -250,6 +252,7 @@ const anthropicPresetMappings = [ { label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, { label: 'Opus 4.5', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-20251101', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, { label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4-6', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, + { label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'claude-opus-4-7', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, { label: 'Haiku 3.5', from: 'claude-3-5-haiku-20241022', to: 'claude-3-5-haiku-20241022', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, { label: 'Haiku 4.5', from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4-5-20251001', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'Opus->Sonnet', from: 'claude-opus-4-6', to: 'claude-sonnet-4-5-20250929', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' } @@ -309,12 +312,14 @@ const antigravityPresetMappings = [ { label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, - { label: 'Opus 4.6-thinking', from: 'claude-opus-4-6-thinking', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' } + { label: 'Opus 4.6-thinking', from: 'claude-opus-4-6-thinking', to: 'claude-opus-4-6-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, + { label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'claude-opus-4-7', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' } ] // Bedrock 预设映射(与后端 DefaultBedrockModelMapping 保持一致) const bedrockPresetMappings = [ { label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'us.anthropic.claude-opus-4-6-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, + { label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'us.anthropic.claude-opus-4-7-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, { label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'us.anthropic.claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' }, { label: 'Opus 4.5', from: 'claude-opus-4-5-thinking', to: 'us.anthropic.claude-opus-4-5-20251101-v1:0', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' }, { label: 'Sonnet 4.5', from: 'claude-sonnet-4-5', to: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },