diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index b89353fc9e..1d39fa1e31 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -143,7 +143,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig) antigravityTokenProvider := service.ProvideAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService, oAuthRefreshAPI, tempUnschedCache) internal500CounterCache := repository.NewInternal500CounterCache(redisClient) - antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache, accountUsageService) + antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, schedulerSnapshotService, antigravityTokenProvider, rateLimitService, httpUpstream, settingService, internal500CounterCache) accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig, tlsFingerprintProfileService) crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig) accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, rpmCache, compositeTokenCacheInvalidator) diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 8168f7ec0e..429486c3bf 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -75,11 +75,11 @@ var DefaultAntigravityModelMapping = map[string]string{ "claude-opus-4-6": "claude-opus-4-6-thinking", // 简称映射 "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", // 迁移旧模型 "claude-sonnet-4-6": "claude-sonnet-4-6", - "claude-sonnet-4-5": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6 - "claude-sonnet-4-5-thinking": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6 + "claude-sonnet-4-5": "claude-sonnet-4-5", + "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", // Claude 详细版本 ID 映射 "claude-opus-4-5-20251101": "claude-opus-4-6-thinking", // 迁移旧模型 - "claude-sonnet-4-5-20250929": "claude-sonnet-4-6", // Sonnet 4.5 → 4.6 + "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", // Claude Haiku → Sonnet(无 Haiku 支持) "claude-haiku-4-5": "claude-sonnet-4-6", "claude-haiku-4-5-20251001": "claude-sonnet-4-6", diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 50deba8bd0..0e5741d818 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -846,22 +846,6 @@ func (s *AccountUsageService) getAntigravityUsage(ctx context.Context, account * return usage, nil } -// GetAntigravityCredits 返回账号的 AI Credits 信息,复用 getAntigravityUsage 的缓存。 -// 如果缓存存在且 TTL 充足则直接返回;TTL 不足时自动刷新。 -func (s *AccountUsageService) GetAntigravityCredits(ctx context.Context, account *Account) (*UsageInfo, error) { - if account == nil || account.Platform != PlatformAntigravity { - return nil, nil - } - return s.getAntigravityUsage(ctx, account) -} - -// InvalidateAntigravityCreditsCache 清除指定账号的 Antigravity 用量缓存, -// 使下次调用 GetAntigravityCredits 时强制重新拉取。 -// 用于 credits 降级响应重试场景:避免重试命中同一个降级缓存。 -func (s *AccountUsageService) InvalidateAntigravityCreditsCache(accountID int64) { - s.cache.antigravityCache.Delete(accountID) -} - // recalcAntigravityRemainingSeconds 重新计算 Antigravity UsageInfo 中各窗口的 RemainingSeconds // 用于从缓存取出时更新倒计时,避免返回过时的剩余秒数 func recalcAntigravityRemainingSeconds(info *UsageInfo) { diff --git a/backend/internal/service/antigravity_credits_overages.go b/backend/internal/service/antigravity_credits_overages.go index e1b787415d..ec3650859c 100644 --- a/backend/internal/service/antigravity_credits_overages.go +++ b/backend/internal/service/antigravity_credits_overages.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "io" - "log/slog" "net/http" "strings" "time" @@ -18,140 +17,8 @@ const ( // 与普通模型限流完全同构:通过 SetModelRateLimit / isRateLimitActiveForKey 读写。 creditsExhaustedKey = "AICredits" creditsExhaustedDuration = 5 * time.Hour - - // credits 降级响应重试参数 - creditsRetryMaxAttempts = 3 - creditsRetryBaseInterval = 500 * time.Millisecond ) -// creditsRetryableErrorCodes 是降级响应中可重试的错误码集合。 -// forbidden 是稳定的封号状态,不属于可恢复的瞬态错误,不重试。 -var creditsRetryableErrorCodes = map[string]bool{ - errorCodeUnauthenticated: true, - errorCodeRateLimited: true, - errorCodeNetworkError: true, -} - -// isAntigravityDegradedResponse 检查 UsageInfo 是否为可重试的降级响应。 -// 仅检测 3 个瞬态错误码(unauthenticated/rate_limited/network_error), -// forbidden 是稳定的封号状态,不属于降级。 -func isAntigravityDegradedResponse(info *UsageInfo) bool { - if info == nil || info.ErrorCode == "" { - return false - } - return creditsRetryableErrorCodes[info.ErrorCode] -} - -// checkAccountCredits 通过共享的 AccountUsageService 缓存检查账号是否有足够的 AI Credits。 -// 缓存 TTL 不足时会自动从 Google loadCodeAssist API 刷新。 -// 检测到降级响应时会清除缓存并重试,最终 fail-open(返回 true)。 -func (s *AntigravityGatewayService) checkAccountCredits( - ctx context.Context, account *Account, -) bool { - if account == nil || account.ID == 0 { - return false - } - if s.accountUsageService == nil { - return true // 无 usage service 时不阻断 - } - - usageInfo, err := s.accountUsageService.GetAntigravityCredits(ctx, account) - if err != nil { - slog.Error("check_credits: get_credits_failed", - "account_id", account.ID, "error", err) - return true // 出错时 fail-open - } - - // 非降级响应:直接检查积分余额 - if !isAntigravityDegradedResponse(usageInfo) { - return s.logCreditsResult(account, usageInfo) - } - - // 降级响应:清除缓存后重试 - return s.retryCreditsOnDegraded(ctx, account, usageInfo) -} - -// retryCreditsOnDegraded 在检测到降级响应后,清除缓存并重试获取 credits。 -// 使用指数退避(500ms → 1s → 2s),最多重试 creditsRetryMaxAttempts 次。 -// 所有重试失败后 fail-open(返回 true),不做熔断。 -func (s *AntigravityGatewayService) retryCreditsOnDegraded( - ctx context.Context, account *Account, lastInfo *UsageInfo, -) bool { - for attempt := 1; attempt <= creditsRetryMaxAttempts; attempt++ { - delay := creditsRetryBaseInterval << (attempt - 1) // 指数退避:500ms, 1s, 2s - slog.Warn("check_credits: degraded response, retrying", - "account_id", account.ID, - "attempt", attempt, - "max_attempts", creditsRetryMaxAttempts, - "error_code", lastInfo.ErrorCode, - "delay", delay, - ) - - select { - case <-ctx.Done(): - slog.Warn("check_credits: context cancelled during retry, fail-open", - "account_id", account.ID, "attempt", attempt) - return true - case <-time.After(delay): - } - - // 清除缓存,强制下次 GetAntigravityCredits 重新拉取 - s.accountUsageService.InvalidateAntigravityCreditsCache(account.ID) - - info, err := s.accountUsageService.GetAntigravityCredits(ctx, account) - if err != nil { - slog.Error("check_credits: retry get_credits_failed", - "account_id", account.ID, "attempt", attempt, "error", err) - continue - } - - // 重试成功(不再是降级响应):检查积分余额 - if !isAntigravityDegradedResponse(info) { - slog.Info("check_credits: retry succeeded", - "account_id", account.ID, "attempt", attempt) - return s.logCreditsResult(account, info) - } - lastInfo = info - } - - // 所有重试失败:fail-open,不做熔断 - slog.Warn("check_credits: all retries exhausted, fail-open", - "account_id", account.ID, - "last_error_code", lastInfo.ErrorCode, - ) - return true -} - -// logCreditsResult 检查积分并记录不足日志,返回是否有积分。 -func (s *AntigravityGatewayService) logCreditsResult(account *Account, info *UsageInfo) bool { - hasCredits := hasEnoughCredits(info) - if !hasCredits { - slog.Warn("check_credits: insufficient credits", - "account_id", account.ID) - } - return hasCredits -} - -// hasEnoughCredits 检查 UsageInfo 中是否有足够的 GOOGLE_ONE_AI 积分。 -// 返回 true 表示积分可用,false 表示积分不足或无积分信息。 -func hasEnoughCredits(info *UsageInfo) bool { - if info == nil || len(info.AICredits) == 0 { - return false - } - - for _, credit := range info.AICredits { - if credit.CreditType == "GOOGLE_ONE_AI" { - minimum := credit.MinimumBalance - if minimum <= 0 { - minimum = 5 - } - return credit.Amount >= minimum - } - } - - return false -} - type antigravity429Category string const ( @@ -224,10 +91,6 @@ func (s *AntigravityGatewayService) clearCreditsExhausted(ctx context.Context, a }); err != nil { logger.LegacyPrintf("service.antigravity_gateway", "clear credits exhausted failed: account=%d err=%v", account.ID, err) } - // 同步更新 Redis 调度快照,避免其他节点/请求延迟感知 - if s.schedulerSnapshot != nil { - _ = s.schedulerSnapshot.UpdateAccountInCache(ctx, account) - } } // classifyAntigravity429 将 Antigravity 的 429 响应归类为配额耗尽、限流或未知。 @@ -278,14 +141,6 @@ func resolveCreditsOveragesModelKey(ctx context.Context, account *Account, upstr } // shouldMarkCreditsExhausted 判断一次 credits 请求失败是否应标记为 credits 耗尽。 -// 此函数在积分注入后失败时调用(预检查注入 + attemptCreditsOveragesRetry 两条路径)。 -// - 429 + 单模型限流("exhausted your capacity on this model"):该模型免费配额用完, -// 积分注入对此无效,但账号积分对其他模型可能仍可用 → 不标记积分耗尽。 -// - 429 + 积分关键词(如 "insufficient credits"):积分确实不足 → 标记耗尽。 -// - 429 + 无积分关键词(节点限流、瞬时 rate limit 等):与积分无关 → 不标记。 -// - 403 等其他 4xx:检查 body 是否包含积分不足的关键词。 -// -// clearCreditsExhausted 会在后续成功时自动清除。 func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr error) bool { if reqErr != nil || resp == nil { return false @@ -293,23 +148,13 @@ func shouldMarkCreditsExhausted(resp *http.Response, respBody []byte, reqErr err if resp.StatusCode >= 500 || resp.StatusCode == http.StatusRequestTimeout { return false } - bodyLower := strings.ToLower(string(respBody)) - // 积分注入后仍 429 - if resp.StatusCode == http.StatusTooManyRequests { - // 单模型配额耗尽:积分注入对此无效,不标记整个账号积分耗尽 - if strings.Contains(bodyLower, "exhausted your capacity on this model") { - return false - } - // 仅当 body 包含积分相关关键词时才标记耗尽; - // 节点限流、瞬时 rate limit 等非积分 429 不应触发 5 小时熔断。 - return containsCreditsExhaustedKeyword(bodyLower) + // 注意:不再检查 isURLLevelRateLimit。此函数仅在积分重试失败后调用, + // 如果注入 enabledCreditTypes 后仍返回 "Resource has been exhausted", + // 说明积分也已耗尽,应该标记。clearCreditsExhausted 会在后续成功时自动清除。 + if info := parseAntigravitySmartRetryInfo(respBody); info != nil { + return false } - // 其他 4xx:关键词匹配(如 403 + "Insufficient credits") - return containsCreditsExhaustedKeyword(bodyLower) -} - -// containsCreditsExhaustedKeyword 检查响应体是否包含积分耗尽的关键词。 -func containsCreditsExhaustedKeyword(bodyLower string) bool { + bodyLower := strings.ToLower(string(respBody)) for _, keyword := range creditsExhaustedKeywords { if strings.Contains(bodyLower, keyword) { return true @@ -336,16 +181,6 @@ func (s *AntigravityGatewayService) attemptCreditsOveragesRetry( if creditsBody == nil { return &creditsOveragesRetryResult{handled: false} } - - // Check actual credits balance before attempting retry - if !s.checkAccountCredits(p.ctx, p.account) { - s.setCreditsExhausted(p.ctx, p.account) - modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel) - logger.LegacyPrintf("service.antigravity_gateway", "%s credit_overages_no_credits model=%s account=%d (skipping credits retry)", - p.prefix, modelKey, p.account.ID) - return &creditsOveragesRetryResult{handled: true} - } - modelKey := resolveCreditsOveragesModelKey(p.ctx, p.account, modelName, p.requestedModel) logger.LegacyPrintf("service.antigravity_gateway", "%s status=429 credit_overages_retry model=%s account=%d (injecting enabledCreditTypes)", p.prefix, modelKey, p.account.ID) diff --git a/backend/internal/service/antigravity_credits_overages_test.go b/backend/internal/service/antigravity_credits_overages_test.go index 1e9106b41d..7a5224daf3 100644 --- a/backend/internal/service/antigravity_credits_overages_test.go +++ b/backend/internal/service/antigravity_credits_overages_test.go @@ -239,7 +239,7 @@ func TestAntigravityRetryLoop_ModelRateLimited_InjectsCredits(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, @@ -290,7 +290,7 @@ func TestAntigravityRetryLoop_CreditsExhausted_DoesNotInject(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, @@ -355,7 +355,7 @@ func TestAntigravityRetryLoop_CreditErrorMarksExhausted(t *testing.T) { Extra: map[string]any{ "allow_overages": true, modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limited_at": time.Now().UTC().Format(time.RFC3339), "rate_limit_reset_at": time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339), }, @@ -418,25 +418,13 @@ func TestShouldMarkCreditsExhausted(t *testing.T) { require.True(t, shouldMarkCreditsExhausted(resp, body, nil)) }) - t.Run("单模型配额耗尽不标记(积分对此无效)", func(t *testing.T) { - resp := &http.Response{StatusCode: http.StatusTooManyRequests} - body := []byte(`{"error":{"code":429,"message":"You have exhausted your capacity on this model. Your quota will reset after 146h11m17s.","status":"RESOURCE_EXHAUSTED"}}`) - require.False(t, shouldMarkCreditsExhausted(resp, body, nil)) - }) - - t.Run("429 结构化限流不标记(节点限流/瞬时 rate limit 与积分无关)", func(t *testing.T) { + t.Run("结构化限流不标记", func(t *testing.T) { resp := &http.Response{StatusCode: http.StatusTooManyRequests} body := []byte(`{"error":{"status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"RATE_LIMIT_EXCEEDED"},{"@type":"type.googleapis.com/google.rpc.RetryInfo","retryDelay":"0.5s"}]}}`) require.False(t, shouldMarkCreditsExhausted(resp, body, nil)) }) - t.Run("429 含积分关键词时标记", func(t *testing.T) { - resp := &http.Response{StatusCode: http.StatusTooManyRequests} - body := []byte(`{"error":{"message":"Insufficient credits to complete this request"}}`) - require.True(t, shouldMarkCreditsExhausted(resp, body, nil)) - }) - - t.Run("含 credits 关键词时标记(403 场景)", func(t *testing.T) { + t.Run("含 credits 关键词时标记", func(t *testing.T) { resp := &http.Response{StatusCode: http.StatusForbidden} for _, keyword := range []string{ "Insufficient GOOGLE_ONE_AI credits", @@ -515,7 +503,7 @@ func TestClearCreditsExhausted(t *testing.T) { ID: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limited_at": "2026-03-15T00:00:00Z", "rate_limit_reset_at": "2099-03-15T00:00:00Z", }, @@ -532,7 +520,7 @@ func TestClearCreditsExhausted(t *testing.T) { ID: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limited_at": "2026-03-15T00:00:00Z", "rate_limit_reset_at": "2099-03-15T00:00:00Z", }, @@ -550,109 +538,7 @@ func TestClearCreditsExhausted(t *testing.T) { _, exists := rawLimits[creditsExhaustedKey] require.False(t, exists, "AICredits key 应被删除") // 普通模型限流应保留 - _, exists = rawLimits["claude-sonnet-4-6"] + _, exists = rawLimits["claude-sonnet-4-5"] require.True(t, exists, "普通模型限流应保留") }) } - -// =========================================================================== -// hasEnoughCredits — standalone credits balance check -// =========================================================================== - -func TestHasEnoughCredits(t *testing.T) { - tests := []struct { - name string - info *UsageInfo - want bool - }{ - { - name: "nil UsageInfo", - info: nil, - want: false, - }, - { - name: "empty AICredits list", - info: &UsageInfo{AICredits: []AICredit{}}, - want: false, - }, - { - name: "GOOGLE_ONE_AI with enough credits (amount=18778, minimum=50)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 18778, MinimumBalance: 50}, - }, - }, - want: true, - }, - { - name: "GOOGLE_ONE_AI below minimum (amount=3, minimum=5)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 3, MinimumBalance: 5}, - }, - }, - want: false, - }, - { - name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=6", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: 0}, - }, - }, - want: true, - }, - { - name: "GOOGLE_ONE_AI with zero MinimumBalance defaults to 5, amount=4", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 4, MinimumBalance: 0}, - }, - }, - want: false, - }, - { - name: "GOOGLE_ONE_AI exactly at minimum (amount=5, minimum=5)", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 5, MinimumBalance: 5}, - }, - }, - want: true, - }, - { - name: "no GOOGLE_ONE_AI credit type", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "OTHER_CREDIT", Amount: 10000, MinimumBalance: 5}, - }, - }, - want: false, - }, - { - name: "multiple credits, GOOGLE_ONE_AI present with enough", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "OTHER_CREDIT", Amount: 0, MinimumBalance: 5}, - {CreditType: "GOOGLE_ONE_AI", Amount: 100, MinimumBalance: 10}, - }, - }, - want: true, - }, - { - name: "negative MinimumBalance defaults to 5", - info: &UsageInfo{ - AICredits: []AICredit{ - {CreditType: "GOOGLE_ONE_AI", Amount: 6, MinimumBalance: -1}, - }, - }, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, hasEnoughCredits(tt.info)) - }) - } -} diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 6dba547cfc..a76e59fbea 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -557,13 +557,7 @@ func (s *AntigravityGatewayService) antigravityRetryLoop(p antigravityRetryLoopP if p.requestedModel != "" && p.account.Platform == PlatformAntigravity && p.account.IsOveragesEnabled() && !p.account.isCreditsExhausted() && p.account.isModelRateLimitedWithContext(p.ctx, p.requestedModel) { - // Check actual credits balance before injection - if !s.checkAccountCredits(p.ctx, p.account) { - // No credits available - mark as exhausted and skip injection - s.setCreditsExhausted(p.ctx, p.account) - logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: no_credits_available account=%d (skipping credits injection)", - p.prefix, p.account.ID) - } else if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil { + if creditsBody := injectEnabledCreditTypes(p.body); creditsBody != nil { p.body = creditsBody overagesInjected = true logger.LegacyPrintf("service.antigravity_gateway", "%s pre_check: model_rate_limited_credits_inject model=%s account=%d (injecting enabledCreditTypes)", @@ -876,15 +870,14 @@ func logPrefix(sessionID, accountName string) string { // AntigravityGatewayService 处理 Antigravity 平台的 API 转发 type AntigravityGatewayService struct { - accountRepo AccountRepository - tokenProvider *AntigravityTokenProvider - rateLimitService *RateLimitService - httpUpstream HTTPUpstream - settingService *SettingService - cache GatewayCache // 用于模型级限流时清除粘性会话绑定 - schedulerSnapshot *SchedulerSnapshotService - internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器 - accountUsageService *AccountUsageService // 共享 usage 缓存,用于积分余额检查 + accountRepo AccountRepository + tokenProvider *AntigravityTokenProvider + rateLimitService *RateLimitService + httpUpstream HTTPUpstream + settingService *SettingService + cache GatewayCache // 用于模型级限流时清除粘性会话绑定 + schedulerSnapshot *SchedulerSnapshotService + internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器 } func NewAntigravityGatewayService( @@ -896,18 +889,16 @@ func NewAntigravityGatewayService( httpUpstream HTTPUpstream, settingService *SettingService, internal500Cache Internal500CounterCache, - accountUsageService *AccountUsageService, ) *AntigravityGatewayService { return &AntigravityGatewayService{ - accountRepo: accountRepo, - tokenProvider: tokenProvider, - rateLimitService: rateLimitService, - httpUpstream: httpUpstream, - settingService: settingService, - cache: cache, - schedulerSnapshot: schedulerSnapshot, - internal500Cache: internal500Cache, - accountUsageService: accountUsageService, + accountRepo: accountRepo, + tokenProvider: tokenProvider, + rateLimitService: rateLimitService, + httpUpstream: httpUpstream, + settingService: settingService, + cache: cache, + schedulerSnapshot: schedulerSnapshot, + internal500Cache: internal500Cache, } } @@ -1007,8 +998,14 @@ func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedMo } // applyThinkingModelSuffix 根据 thinking 配置调整模型名 -// Sonnet 4.5 已统一映射到 4.6,此函数保留为扩展点 +// 当映射结果是 claude-sonnet-4-5 且请求开启了 thinking 时,改为 claude-sonnet-4-5-thinking func applyThinkingModelSuffix(mappedModel string, thinkingEnabled bool) string { + if !thinkingEnabled { + return mappedModel + } + if mappedModel == "claude-sonnet-4-5" { + return "claude-sonnet-4-5-thinking" + } return mappedModel } @@ -1744,7 +1741,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, var clientDisconnect bool if claudeReq.Stream { // 客户端要求流式,直接透传转换 - streamRes, err := s.handleClaudeStreamingResponse(c, resp, startTime, originalModel, account.ID) + streamRes, err := s.handleClaudeStreamingResponse(c, resp, startTime, originalModel) if err != nil { logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_error error=%v", prefix, err) return nil, err @@ -1754,7 +1751,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, clientDisconnect = streamRes.clientDisconnect } else { // 客户端要求非流式,收集流式响应后转换返回 - streamRes, err := s.handleClaudeStreamToNonStreaming(c, resp, startTime, originalModel, account.ID) + streamRes, err := s.handleClaudeStreamToNonStreaming(c, resp, startTime, originalModel) if err != nil { logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_collect_error error=%v", prefix, err) return nil, err @@ -3702,7 +3699,7 @@ func (s *AntigravityGatewayService) writeGoogleError(c *gin.Context, status int, // handleClaudeStreamToNonStreaming 收集上游流式响应,转换为 Claude 非流式格式返回 // 用于处理客户端非流式请求但上游只支持流式的情况 -func (s *AntigravityGatewayService) handleClaudeStreamToNonStreaming(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string, accountID int64) (*antigravityStreamResult, error) { +func (s *AntigravityGatewayService) handleClaudeStreamToNonStreaming(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string) (*antigravityStreamResult, error) { scanner := bufio.NewScanner(resp.Body) maxLineSize := defaultMaxLineSize if s.settingService.cfg != nil && s.settingService.cfg.Gateway.MaxLineSize > 0 { @@ -3874,7 +3871,7 @@ returnResponse: } // handleClaudeStreamingResponse 处理 Claude 流式响应(Gemini SSE → Claude SSE 转换) -func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string, accountID int64) (*antigravityStreamResult, error) { +func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context, resp *http.Response, startTime time.Time, originalModel string) (*antigravityStreamResult, error) { c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") @@ -3887,7 +3884,6 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context } processor := antigravity.NewStreamingProcessor(originalModel) - var firstTokenMs *int // 使用 Scanner 并限制单行大小,避免 ReadString 无上限导致 OOM scanner := bufio.NewScanner(resp.Body) diff --git a/backend/internal/service/antigravity_gateway_service_test.go b/backend/internal/service/antigravity_gateway_service_test.go index 48a580ac4d..1eb1451e8a 100644 --- a/backend/internal/service/antigravity_gateway_service_test.go +++ b/backend/internal/service/antigravity_gateway_service_test.go @@ -926,7 +926,7 @@ func TestHandleClaudeStreamingResponse_NormalComplete(t *testing.T) { fmt.Fprintln(pw, "") }() - result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0) + result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5") _ = pr.Close() require.NoError(t, err) @@ -1003,7 +1003,7 @@ func TestHandleClaudeStreamingResponse_ThoughtsTokenCount(t *testing.T) { fmt.Fprintln(pw, "") }() - result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "gemini-2.5-pro", 0) + result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "gemini-2.5-pro") _ = pr.Close() require.NoError(t, err) @@ -1206,7 +1206,7 @@ func TestHandleClaudeStreamingResponse_ClientDisconnect(t *testing.T) { fmt.Fprintln(pw, "") }() - result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0) + result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5") _ = pr.Close() require.NoError(t, err) @@ -1238,7 +1238,7 @@ func TestHandleClaudeStreamingResponse_EmptyStream(t *testing.T) { fmt.Fprintln(pw, "") }() - _, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0) + _, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5") _ = pr.Close() // 应当返回 UpstreamFailoverError 而非 nil,以便上层触发 failover @@ -1270,7 +1270,7 @@ func TestHandleClaudeStreamingResponse_ContextCanceled(t *testing.T) { resp := &http.Response{StatusCode: http.StatusOK, Body: cancelReadCloser{}, Header: http.Header{}} - result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5", 0) + result, err := svc.handleClaudeStreamingResponse(c, resp, time.Now(), "claude-sonnet-4-5") require.NoError(t, err) require.NotNil(t, result) diff --git a/backend/internal/service/antigravity_model_mapping_test.go b/backend/internal/service/antigravity_model_mapping_test.go index a151e1a139..a29000e7ed 100644 --- a/backend/internal/service/antigravity_model_mapping_test.go +++ b/backend/internal/service/antigravity_model_mapping_test.go @@ -69,10 +69,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-sonnet-4-6", }, { - name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-6", + name: "默认映射 - claude-sonnet-4-5-20250929 → claude-sonnet-4-5", requestedModel: "claude-sonnet-4-5-20250929", accountMapping: nil, - expected: "claude-sonnet-4-6", + expected: "claude-sonnet-4-5", }, // 3. 默认映射中的透传(映射到自己) @@ -83,10 +83,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-sonnet-4-6", }, { - name: "默认映射 - claude-sonnet-4-5 → claude-sonnet-4-6", + name: "默认映射透传 - claude-sonnet-4-5", requestedModel: "claude-sonnet-4-5", accountMapping: nil, - expected: "claude-sonnet-4-6", + expected: "claude-sonnet-4-5", }, { name: "默认映射透传 - claude-opus-4-6-thinking", @@ -95,10 +95,10 @@ func TestAntigravityGatewayService_GetMappedModel(t *testing.T) { expected: "claude-opus-4-6-thinking", }, { - name: "默认映射 - claude-sonnet-4-5-thinking → claude-sonnet-4-6", + name: "默认映射透传 - claude-sonnet-4-5-thinking", requestedModel: "claude-sonnet-4-5-thinking", accountMapping: nil, - expected: "claude-sonnet-4-6", + expected: "claude-sonnet-4-5-thinking", }, { name: "默认映射透传 - gemini-2.5-flash", diff --git a/backend/internal/service/antigravity_rate_limit_test.go b/backend/internal/service/antigravity_rate_limit_test.go index d0559d7f8c..35e130dc47 100644 --- a/backend/internal/service/antigravity_rate_limit_test.go +++ b/backend/internal/service/antigravity_rate_limit_test.go @@ -206,7 +206,7 @@ func TestHandleUpstreamError_429_NonModelRateLimit(t *testing.T) { // 但 429 兜底逻辑会使用 requestedModel 设置模型级限流 require.Nil(t, result) require.Len(t, repo.modelRateLimitCalls, 1) - require.Equal(t, "claude-sonnet-4-6", repo.modelRateLimitCalls[0].modelKey) + require.Equal(t, "claude-sonnet-4-5", repo.modelRateLimitCalls[0].modelKey) } // TestHandleUpstreamError_429_NonModelRateLimit_UsesMappedModelKey 测试 429 非模型限流场景 @@ -832,7 +832,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRateLimited(t *testing.T) { Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": time.Now().Add(2 * time.Second).Format(time.RFC3339), }, }, @@ -875,7 +875,7 @@ func TestAntigravityRetryLoop_PreCheck_SwitchesWhenRemainingLong(t *testing.T) { Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": time.Now().Add(11 * time.Second).Format(time.RFC3339), }, }, diff --git a/backend/internal/service/antigravity_single_account_retry_test.go b/backend/internal/service/antigravity_single_account_retry_test.go index 6ac19b610a..675e9c0cbf 100644 --- a/backend/internal/service/antigravity_single_account_retry_test.go +++ b/backend/internal/service/antigravity_single_account_retry_test.go @@ -689,7 +689,7 @@ func TestAntigravityRetryLoop_PreCheck_SingleAccountRetry_SkipsRateLimit(t *test Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339), }, }, @@ -733,7 +733,7 @@ func TestAntigravityRetryLoop_PreCheck_NoSingleAccountRetry_SwitchesOnRateLimit( Concurrency: 1, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": time.Now().Add(30 * time.Second).Format(time.RFC3339), }, }, diff --git a/backend/internal/service/antigravity_thinking_test.go b/backend/internal/service/antigravity_thinking_test.go index 34b3bf9219..b3952ee4a8 100644 --- a/backend/internal/service/antigravity_thinking_test.go +++ b/backend/internal/service/antigravity_thinking_test.go @@ -27,12 +27,12 @@ func TestApplyThinkingModelSuffix(t *testing.T) { expected: "claude-opus-4-6-thinking", }, - // Thinking 开启 + claude-sonnet-4-5:Sonnet 4.5 已统一映射到 4.6,不再特殊处理 + // Thinking 开启 + claude-sonnet-4-5:自动添加后缀 { - name: "thinking enabled - claude-sonnet-4-5 unchanged (mapped to 4.6 elsewhere)", + name: "thinking enabled - claude-sonnet-4-5 becomes thinking version", mappedModel: "claude-sonnet-4-5", thinkingEnabled: true, - expected: "claude-sonnet-4-5", + expected: "claude-sonnet-4-5-thinking", }, // Thinking 开启 + 其他模型:保持原样 diff --git a/backend/internal/service/gateway_service_antigravity_whitelist_test.go b/backend/internal/service/gateway_service_antigravity_whitelist_test.go index 98b0680119..c078be3264 100644 --- a/backend/internal/service/gateway_service_antigravity_whitelist_test.go +++ b/backend/internal/service/gateway_service_antigravity_whitelist_test.go @@ -103,8 +103,7 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes expected: false, }, // 场景 3: 配置 claude-sonnet-4-5(非 thinking),请求 claude-sonnet-4-5 + thinking=true - // applyThinkingModelSuffix 已简化为直通,不再添加 -thinking 后缀 - // 最终模型名仍为 claude-sonnet-4-5,在 mapping 中 → 返回 true + // 最终模型名 = claude-sonnet-4-5-thinking,不在 mapping 中,应该不匹配 { name: "thinking_enabled_no_match_non_thinking_mapping", modelMapping: map[string]any{ @@ -112,7 +111,7 @@ func TestGatewayService_isModelSupportedByAccountWithContext_ThinkingMode(t *tes }, requestedModel: "claude-sonnet-4-5", thinkingEnabled: true, - expected: true, + expected: false, }, // 场景 4: 配置两种模型,请求 claude-sonnet-4-5 + thinking=true,应该匹配 thinking 版本 { diff --git a/backend/internal/service/model_rate_limit_test.go b/backend/internal/service/model_rate_limit_test.go index 7af3f6c788..b79b9688ac 100644 --- a/backend/internal/service/model_rate_limit_test.go +++ b/backend/internal/service/model_rate_limit_test.go @@ -181,12 +181,11 @@ func TestIsModelRateLimited_Antigravity_ThinkingAffectsModelKey(t *testing.T) { now := time.Now() future := now.Add(10 * time.Minute).Format(time.RFC3339) - // Sonnet 4.5 now maps to Sonnet 4.6, so rate limit key is claude-sonnet-4-6 account := &Account{ Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5-thinking": map[string]any{ "rate_limit_reset_at": future, }, }, @@ -344,7 +343,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) { Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": future15m, }, }, @@ -360,7 +359,7 @@ func TestGetRateLimitRemainingTime(t *testing.T) { Platform: PlatformAntigravity, Extra: map[string]any{ modelRateLimitsKey: map[string]any{ - "claude-sonnet-4-6": map[string]any{ + "claude-sonnet-4-5": map[string]any{ "rate_limit_reset_at": future5m, }, }, diff --git a/backend/migrations/056_add_sonnet46_to_model_mapping.sql b/backend/migrations/056_add_sonnet46_to_model_mapping.sql deleted file mode 100644 index aa7657d71a..0000000000 --- a/backend/migrations/056_add_sonnet46_to_model_mapping.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Add claude-sonnet-4-6 to model_mapping for all Antigravity accounts --- --- Background: --- Antigravity now supports claude-sonnet-4-6 --- --- Strategy: --- Directly overwrite the entire model_mapping with updated mappings --- This ensures consistency with DefaultAntigravityModelMapping in constants.go - -UPDATE accounts -SET credentials = jsonb_set( - credentials, - '{model_mapping}', - '{ - "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", - "claude-opus-4-5-20251101": "claude-opus-4-6-thinking", - "claude-sonnet-4-6": "claude-sonnet-4-6", - "claude-sonnet-4-5": "claude-sonnet-4-5", - "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", - "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", - "claude-haiku-4-5": "claude-sonnet-4-5", - "claude-haiku-4-5-20251001": "claude-sonnet-4-5", - "gemini-2.5-flash": "gemini-2.5-flash", - "gemini-2.5-flash-lite": "gemini-2.5-flash-lite", - "gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking", - "gemini-2.5-pro": "gemini-2.5-pro", - "gemini-3-flash": "gemini-3-flash", - "gemini-3-pro-high": "gemini-3-pro-high", - "gemini-3-pro-low": "gemini-3-pro-low", - "gemini-3-pro-image": "gemini-3-pro-image", - "gemini-3-flash-preview": "gemini-3-flash", - "gemini-3-pro-preview": "gemini-3-pro-high", - "gemini-3-pro-image-preview": "gemini-3-pro-image", - "gpt-oss-120b-medium": "gpt-oss-120b-medium", - "tab_flash_lite_preview": "tab_flash_lite_preview" - }'::jsonb -) -WHERE platform = 'antigravity' - AND deleted_at IS NULL - AND credentials->'model_mapping' IS NOT NULL; diff --git a/backend/migrations/057_add_gemini31_pro_to_model_mapping.sql b/backend/migrations/057_add_gemini31_pro_to_model_mapping.sql deleted file mode 100644 index 6305e717bc..0000000000 --- a/backend/migrations/057_add_gemini31_pro_to_model_mapping.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Add gemini-3.1-pro-high, gemini-3.1-pro-low, gemini-3.1-pro-preview to model_mapping --- --- Background: --- Antigravity now supports gemini-3.1-pro-high and gemini-3.1-pro-low --- --- Strategy: --- Directly overwrite the entire model_mapping with updated mappings --- This ensures consistency with DefaultAntigravityModelMapping in constants.go - -UPDATE accounts -SET credentials = jsonb_set( - credentials, - '{model_mapping}', - '{ - "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", - "claude-opus-4-5-20251101": "claude-opus-4-6-thinking", - "claude-sonnet-4-6": "claude-sonnet-4-6", - "claude-sonnet-4-5": "claude-sonnet-4-5", - "claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", - "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", - "claude-haiku-4-5": "claude-sonnet-4-5", - "claude-haiku-4-5-20251001": "claude-sonnet-4-5", - "gemini-2.5-flash": "gemini-2.5-flash", - "gemini-2.5-flash-lite": "gemini-2.5-flash-lite", - "gemini-2.5-flash-thinking": "gemini-2.5-flash-thinking", - "gemini-2.5-pro": "gemini-2.5-pro", - "gemini-3-flash": "gemini-3-flash", - "gemini-3-pro-high": "gemini-3-pro-high", - "gemini-3-pro-low": "gemini-3-pro-low", - "gemini-3-pro-image": "gemini-3-pro-image", - "gemini-3-flash-preview": "gemini-3-flash", - "gemini-3-pro-preview": "gemini-3-pro-high", - "gemini-3-pro-image-preview": "gemini-3-pro-image", - "gemini-3.1-pro-high": "gemini-3.1-pro-high", - "gemini-3.1-pro-low": "gemini-3.1-pro-low", - "gemini-3.1-pro-preview": "gemini-3.1-pro-high", - "gpt-oss-120b-medium": "gpt-oss-120b-medium", - "tab_flash_lite_preview": "tab_flash_lite_preview" - }'::jsonb -) -WHERE platform = 'antigravity' - AND deleted_at IS NULL - AND credentials->'model_mapping' IS NOT NULL; diff --git a/backend/migrations/060_add_group_simulate_claude_max.sql b/backend/migrations/060_add_group_simulate_claude_max.sql deleted file mode 100644 index 55662dfdac..0000000000 --- a/backend/migrations/060_add_group_simulate_claude_max.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE groups - ADD COLUMN IF NOT EXISTS simulate_claude_max_enabled BOOLEAN NOT NULL DEFAULT FALSE; - diff --git a/backend/migrations/070_add_sora_tasks.sql b/backend/migrations/070_add_sora_tasks.sql deleted file mode 100644 index 3a484976d9..0000000000 --- a/backend/migrations/070_add_sora_tasks.sql +++ /dev/null @@ -1,36 +0,0 @@ --- +migrate Up - --- Sora 异步任务表:记录视频/角色/图片生成任务状态 -CREATE TABLE IF NOT EXISTS sora_tasks ( - id VARCHAR(64) PRIMARY KEY, -- 对外 ID (video_xxx / char_xxx / img_xxx) - account_id BIGINT NOT NULL, -- 使用的账号 ID - api_key_id BIGINT, -- 调用方 API Key ID - upstream_task_id VARCHAR(128) NOT NULL DEFAULT '', -- 上游任务 ID - object_type VARCHAR(16) NOT NULL DEFAULT 'video', -- video / character / image - model VARCHAR(64) NOT NULL, -- 请求模型名 - prompt TEXT NOT NULL DEFAULT '', -- 生成提示词 - status VARCHAR(16) NOT NULL DEFAULT 'queued', -- queued / in_progress / completed / failed - progress INT NOT NULL DEFAULT 0, -- 进度 0-100 - video_url TEXT NOT NULL DEFAULT '', -- 原始上游下载 URL - stored_key TEXT NOT NULL DEFAULT '', -- 存储后的 key(本地相对路径或 S3 object key) - storage_type VARCHAR(16) NOT NULL DEFAULT '', -- 存储类型:local / s3 / gdrive / 空=未存储 - share_id VARCHAR(128) NOT NULL DEFAULT '', -- 可分享 ID - character_info JSONB, -- 角色信息 {username, display_name} - error_message TEXT NOT NULL DEFAULT '', -- 错误信息 - error_type VARCHAR(64) NOT NULL DEFAULT '', -- 错误类型 - request_body JSONB, -- 原始请求体(用于上游重放) - seconds VARCHAR(8) NOT NULL DEFAULT '', -- 视频时长 - size VARCHAR(16) NOT NULL DEFAULT '', -- 分辨率 (如 1920x1080) - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - completed_at TIMESTAMPTZ -); - --- 仅索引未完成任务,供后台 worker 轮询 -CREATE INDEX IF NOT EXISTS idx_sora_tasks_pending ON sora_tasks(status) WHERE status IN ('queued', 'in_progress'); - --- 按 API Key 查询 -CREATE INDEX IF NOT EXISTS idx_sora_tasks_api_key ON sora_tasks(api_key_id) WHERE api_key_id IS NOT NULL; - --- +migrate Down - -DROP TABLE IF EXISTS sora_tasks; diff --git a/backend/migrations/077_rollback_sonnet_to_original.sql b/backend/migrations/077_rollback_sonnet_to_original.sql deleted file mode 100644 index 13316e0b72..0000000000 --- a/backend/migrations/077_rollback_sonnet_to_original.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 077: Rollback Sonnet/Haiku mappings from Opus back to original values --- Reverts migration 077_migrate_sonnet_to_opus46_thinking.sql --- Restores: Sonnet→Sonnet, Haiku→Sonnet (original behavior) - -UPDATE accounts -SET credentials = jsonb_set( - credentials, '{model_mapping}', - (credentials->'model_mapping') - || '{"claude-sonnet-4-6": "claude-sonnet-4-6"}'::jsonb - || '{"claude-sonnet-4-5": "claude-sonnet-4-5"}'::jsonb - || '{"claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking"}'::jsonb - || '{"claude-sonnet-4-5-20250929": "claude-sonnet-4-5"}'::jsonb - || '{"claude-haiku-4-5": "claude-sonnet-4-6"}'::jsonb - || '{"claude-haiku-4-5-20251001": "claude-sonnet-4-6"}'::jsonb -) -WHERE platform = 'antigravity' - AND deleted_at IS NULL - AND credentials->'model_mapping' IS NOT NULL; diff --git a/backend/migrations/078_migrate_sonnet45_to_sonnet46.sql b/backend/migrations/078_migrate_sonnet45_to_sonnet46.sql deleted file mode 100644 index d617ca4a16..0000000000 --- a/backend/migrations/078_migrate_sonnet45_to_sonnet46.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 078: Migrate Sonnet 4.5 → Sonnet 4.6 for all antigravity accounts --- Sonnet 4.5 is no longer reliable on upstream, redirect to 4.6 - -UPDATE accounts -SET credentials = jsonb_set( - credentials, '{model_mapping}', - (credentials->'model_mapping') - || '{"claude-sonnet-4-5": "claude-sonnet-4-6"}'::jsonb - || '{"claude-sonnet-4-5-thinking": "claude-sonnet-4-6"}'::jsonb - || '{"claude-sonnet-4-5-20250929": "claude-sonnet-4-6"}'::jsonb - || '{"claude-haiku-4-5": "claude-sonnet-4-6"}'::jsonb - || '{"claude-haiku-4-5-20251001": "claude-sonnet-4-6"}'::jsonb -) -WHERE platform = 'antigravity' - AND deleted_at IS NULL - AND credentials->'model_mapping' IS NOT NULL; diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 9594313914..d16ed97707 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -283,10 +283,10 @@ const geminiPresetMappings = [ // Antigravity 预设映射(支持通配符) const antigravityPresetMappings = [ // Claude 通配符映射 - { label: 'Claude→Sonnet4.6', from: 'claude-*', to: 'claude-sonnet-4-6', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' }, - { label: 'Sonnet→4.6', from: 'claude-sonnet-*', 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: 'Claude→Sonnet', from: 'claude-*', to: 'claude-sonnet-4-5', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' }, + { label: 'Sonnet→Sonnet', from: 'claude-sonnet-*', to: 'claude-sonnet-4-5', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' }, { label: 'Opus→Opus', from: 'claude-opus-*', to: 'claude-opus-4-6-thinking', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, - { label: 'Haiku→Sonnet4.6', from: 'claude-haiku-*', to: 'claude-sonnet-4-6', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, + { label: 'Haiku→Sonnet', from: 'claude-haiku-*', to: 'claude-sonnet-4-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'Sonnet4→4.6', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-6', color: 'bg-sky-100 text-sky-700 hover:bg-sky-200 dark:bg-sky-900/30 dark:text-sky-400' }, { label: 'Sonnet4.5→4.6', from: 'claude-sonnet-4-5-20250929', 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: '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' }, @@ -307,7 +307,7 @@ const antigravityPresetMappings = [ { label: '2.5-Flash-Lite透传', from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, // 精确映射 { 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→4.6', from: 'claude-sonnet-4-5', 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' } ]