diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index c36f771c..7fdb4d19 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -136,7 +136,7 @@ type smartRetryResult struct { // handleSmartRetry 处理 OAuth 账号的智能重试逻辑 // 将 429/503 限流处理逻辑抽取为独立函数,减少 antigravityRetryLoop 的复杂度 -func handleSmartRetry(p antigravityRetryLoopParams, resp *http.Response, respBody []byte, baseURL string, urlIdx int, availableURLs []string) *smartRetryResult { +func (s *AntigravityGatewayService) handleSmartRetry(p antigravityRetryLoopParams, resp *http.Response, respBody []byte, baseURL string, urlIdx int, availableURLs []string) *smartRetryResult { // "Resource has been exhausted" 是 URL 级别限流,切换 URL(仅 429) if resp.StatusCode == http.StatusTooManyRequests && isURLLevelRateLimit(respBody) && urlIdx < len(availableURLs)-1 { log.Printf("%s URL fallback (429): %s -> %s", p.prefix, baseURL, availableURLs[urlIdx+1]) @@ -155,6 +155,8 @@ func handleSmartRetry(p antigravityRetryLoopParams, resp *http.Response, respBod if !setModelRateLimitByModelName(p.ctx, p.accountRepo, p.account.ID, modelName, p.prefix, resp.StatusCode, resetAt, false) { p.handleError(p.ctx, p.prefix, p.account, resp.StatusCode, resp.Header, respBody, p.quotaScope, p.groupID, p.sessionHash, p.isStickySession) log.Printf("%s status=%d rate_limited account=%d (no scope mapping)", p.prefix, resp.StatusCode, p.account.ID) + } else { + s.updateAccountModelRateLimitInCache(p.ctx, p.account, modelName, resetAt) } // 返回账号切换信号,让上层切换账号重试 @@ -241,6 +243,7 @@ func handleSmartRetry(p antigravityRetryLoopParams, resp *http.Response, respBod } else { log.Printf("%s status=%d model_rate_limited_after_smart_retry model=%s account=%d reset_in=%v", p.prefix, resp.StatusCode, modelName, p.account.ID, antigravityDefaultRateLimitDuration) + s.updateAccountModelRateLimitInCache(p.ctx, p.account, modelName, resetAt) } } @@ -260,7 +263,7 @@ func handleSmartRetry(p antigravityRetryLoopParams, resp *http.Response, respBod } // antigravityRetryLoop 执行带 URL fallback 的重试循环 -func antigravityRetryLoop(p antigravityRetryLoopParams) (*antigravityRetryLoopResult, error) { +func (s *AntigravityGatewayService) antigravityRetryLoop(p antigravityRetryLoopParams) (*antigravityRetryLoopResult, error) { // 预检查:如果账号已限流,根据剩余时间决定等待或切换 if p.requestedModel != "" { if remaining := p.account.GetRateLimitRemainingTimeWithContext(p.ctx, p.requestedModel); remaining > 0 { @@ -363,7 +366,7 @@ urlFallbackLoop: _ = resp.Body.Close() // 尝试智能重试处理(OAuth 账号专用) - smartResult := handleSmartRetry(p, resp, respBody, baseURL, urlIdx, availableURLs) + smartResult := s.handleSmartRetry(p, resp, respBody, baseURL, urlIdx, availableURLs) switch smartResult.action { case smartRetryActionContinueURL: continue urlFallbackLoop @@ -1025,7 +1028,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, } // 执行带重试的请求 - result, err := antigravityRetryLoop(antigravityRetryLoopParams{ + result, err := s.antigravityRetryLoop(antigravityRetryLoopParams{ ctx: ctx, prefix: prefix, account: account, @@ -1106,7 +1109,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, if txErr != nil { continue } - retryResult, retryErr := antigravityRetryLoop(antigravityRetryLoopParams{ + retryResult, retryErr := s.antigravityRetryLoop(antigravityRetryLoopParams{ ctx: ctx, prefix: prefix, account: account, @@ -1670,7 +1673,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co } // 执行带重试的请求 - result, err := antigravityRetryLoop(antigravityRetryLoopParams{ + result, err := s.antigravityRetryLoop(antigravityRetryLoopParams{ ctx: ctx, prefix: prefix, account: account, diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 97776a06..6df93498 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -387,6 +387,17 @@ export async function importData(payload: { return data } +/** + * Get Antigravity default model mapping from backend + * @returns Default model mapping (from -> to) + */ +export async function getAntigravityDefaultModelMapping(): Promise> { + const { data } = await apiClient.get>( + '/admin/accounts/antigravity/default-model-mapping' + ) + return data +} + export const accountsAPI = { list, getById, @@ -412,7 +423,8 @@ export const accountsAPI = { bulkUpdate, syncFromCrs, exportData, - importData + importData, + getAntigravityDefaultModelMapping } export default accountsAPI diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index e7f6f79e..e7c5d030 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1980,7 +1980,7 @@ import { getModelsByPlatform, commonErrorCodes, buildModelMappingObject, - antigravityDefaultMappings, + fetchAntigravityDefaultMappings, isValidWildcardPattern } from '@/composables/useModelWhitelist' import { useAuthStore } from '@/stores/auth' @@ -2270,7 +2270,9 @@ watch( // Antigravity: 默认使用映射模式并填充默认映射 if (form.platform === 'antigravity') { antigravityModelRestrictionMode.value = 'mapping' - antigravityModelMappings.value = [...antigravityDefaultMappings] + fetchAntigravityDefaultMappings().then(mappings => { + antigravityModelMappings.value = [...mappings] + }) antigravityWhitelistModels.value = [] } else { antigravityWhitelistModels.value = [] @@ -2318,7 +2320,9 @@ watch( // Antigravity: 默认使用映射模式并填充默认映射 if (newPlatform === 'antigravity') { antigravityModelRestrictionMode.value = 'mapping' - antigravityModelMappings.value = [...antigravityDefaultMappings] + fetchAntigravityDefaultMappings().then(mappings => { + antigravityModelMappings.value = [...mappings] + }) antigravityWhitelistModels.value = [] accountCategory.value = 'oauth-based' antigravityAccountType.value = 'oauth' @@ -2576,7 +2580,9 @@ const resetForm = () => { antigravityModelRestrictionMode.value = 'mapping' antigravityWhitelistModels.value = [] - antigravityModelMappings.value = [...antigravityDefaultMappings] + fetchAntigravityDefaultMappings().then(mappings => { + antigravityModelMappings.value = [...mappings] + }) customErrorCodesEnabled.value = false selectedErrorCodes.value = [] customErrorCodeInput.value = null diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 9d2e254d..d4446e2e 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -273,39 +273,25 @@ const antigravityPresetMappings = [ { label: 'Opus 4.5', from: 'claude-opus-4-5-thinking', to: 'claude-opus-4-5-thinking', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' } ] -// Antigravity 默认映射(与迁移脚本 049 保持一致) -// 基于官方 API 返回的模型列表,只支持 Claude 4.5+ 和 Gemini 2.5+ -// 精确匹配,无通配符 -export const antigravityDefaultMappings: { from: string; to: string }[] = [ - // Claude 白名单 - { from: 'claude-opus-4-6', to: 'claude-opus-4-6' }, - { from: 'claude-opus-4-5-thinking', to: 'claude-opus-4-5-thinking' }, - { from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5' }, - { from: 'claude-sonnet-4-5-thinking', to: 'claude-sonnet-4-5-thinking' }, - // Claude 详细版本 ID 映射 - { from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-thinking' }, - { from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5' }, - // Claude Haiku → Sonnet(无 Haiku 支持) - { from: 'claude-haiku-4-5', to: 'claude-sonnet-4-5' }, - { from: 'claude-haiku-4-5-20251001', to: 'claude-sonnet-4-5' }, - // Gemini 2.5 白名单 - { from: 'gemini-2.5-flash', to: 'gemini-2.5-flash' }, - { from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite' }, - { from: 'gemini-2.5-flash-thinking', to: 'gemini-2.5-flash-thinking' }, - { from: 'gemini-2.5-pro', to: 'gemini-2.5-pro' }, - // Gemini 3 白名单 - { from: 'gemini-3-flash', to: 'gemini-3-flash' }, - { from: 'gemini-3-pro-high', to: 'gemini-3-pro-high' }, - { from: 'gemini-3-pro-low', to: 'gemini-3-pro-low' }, - { from: 'gemini-3-pro-image', to: 'gemini-3-pro-image' }, - // Gemini 3 preview 映射 - { from: 'gemini-3-flash-preview', to: 'gemini-3-flash' }, - { from: 'gemini-3-pro-preview', to: 'gemini-3-pro-high' }, - { from: 'gemini-3-pro-image-preview', to: 'gemini-3-pro-image' }, - // 其他官方模型 - { from: 'gpt-oss-120b-medium', to: 'gpt-oss-120b-medium' }, - { from: 'tab_flash_lite_preview', to: 'tab_flash_lite_preview' } -] +// Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致) +// 使用 fetchAntigravityDefaultMappings() 异步获取 +import { getAntigravityDefaultModelMapping } from '@/api/admin/accounts' + +let _antigravityDefaultMappingsCache: { from: string; to: string }[] | null = null + +export async function fetchAntigravityDefaultMappings(): Promise<{ from: string; to: string }[]> { + if (_antigravityDefaultMappingsCache !== null) { + return _antigravityDefaultMappingsCache + } + try { + const mapping = await getAntigravityDefaultModelMapping() + _antigravityDefaultMappingsCache = Object.entries(mapping).map(([from, to]) => ({ from, to })) + } catch (e) { + console.warn('[fetchAntigravityDefaultMappings] API failed, using empty fallback', e) + _antigravityDefaultMappingsCache = [] + } + return _antigravityDefaultMappingsCache +} // ===================== // 常用错误码