fix(antigravity): fetch default mapping from API and sync Redis on rate limit
1. Frontend: replace hardcoded antigravityDefaultMappings with async fetch from GET /admin/accounts/antigravity/default-model-mapping, eliminating the duplicate data source that caused frontend/backend mapping inconsistency. 2. Backend: convert handleSmartRetry and antigravityRetryLoop from standalone functions to AntigravityGatewayService methods, enabling Redis cache sync (updateAccountModelRateLimitInCache) after both rate-limit write paths — long-delay branch and retry-exhausted branch.
This commit is contained in:
@@ -136,7 +136,7 @@ type smartRetryResult struct {
|
|||||||
|
|
||||||
// handleSmartRetry 处理 OAuth 账号的智能重试逻辑
|
// handleSmartRetry 处理 OAuth 账号的智能重试逻辑
|
||||||
// 将 429/503 限流处理逻辑抽取为独立函数,减少 antigravityRetryLoop 的复杂度
|
// 将 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)
|
// "Resource has been exhausted" 是 URL 级别限流,切换 URL(仅 429)
|
||||||
if resp.StatusCode == http.StatusTooManyRequests && isURLLevelRateLimit(respBody) && urlIdx < len(availableURLs)-1 {
|
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])
|
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) {
|
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)
|
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)
|
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 {
|
} else {
|
||||||
log.Printf("%s status=%d model_rate_limited_after_smart_retry model=%s account=%d reset_in=%v",
|
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)
|
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 的重试循环
|
// antigravityRetryLoop 执行带 URL fallback 的重试循环
|
||||||
func antigravityRetryLoop(p antigravityRetryLoopParams) (*antigravityRetryLoopResult, error) {
|
func (s *AntigravityGatewayService) antigravityRetryLoop(p antigravityRetryLoopParams) (*antigravityRetryLoopResult, error) {
|
||||||
// 预检查:如果账号已限流,根据剩余时间决定等待或切换
|
// 预检查:如果账号已限流,根据剩余时间决定等待或切换
|
||||||
if p.requestedModel != "" {
|
if p.requestedModel != "" {
|
||||||
if remaining := p.account.GetRateLimitRemainingTimeWithContext(p.ctx, p.requestedModel); remaining > 0 {
|
if remaining := p.account.GetRateLimitRemainingTimeWithContext(p.ctx, p.requestedModel); remaining > 0 {
|
||||||
@@ -363,7 +366,7 @@ urlFallbackLoop:
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
|
||||||
// 尝试智能重试处理(OAuth 账号专用)
|
// 尝试智能重试处理(OAuth 账号专用)
|
||||||
smartResult := handleSmartRetry(p, resp, respBody, baseURL, urlIdx, availableURLs)
|
smartResult := s.handleSmartRetry(p, resp, respBody, baseURL, urlIdx, availableURLs)
|
||||||
switch smartResult.action {
|
switch smartResult.action {
|
||||||
case smartRetryActionContinueURL:
|
case smartRetryActionContinueURL:
|
||||||
continue urlFallbackLoop
|
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,
|
ctx: ctx,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
account: account,
|
account: account,
|
||||||
@@ -1106,7 +1109,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
|
|||||||
if txErr != nil {
|
if txErr != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
retryResult, retryErr := antigravityRetryLoop(antigravityRetryLoopParams{
|
retryResult, retryErr := s.antigravityRetryLoop(antigravityRetryLoopParams{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
account: account,
|
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,
|
ctx: ctx,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
account: account,
|
account: account,
|
||||||
|
|||||||
@@ -387,6 +387,17 @@ export async function importData(payload: {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Antigravity default model mapping from backend
|
||||||
|
* @returns Default model mapping (from -> to)
|
||||||
|
*/
|
||||||
|
export async function getAntigravityDefaultModelMapping(): Promise<Record<string, string>> {
|
||||||
|
const { data } = await apiClient.get<Record<string, string>>(
|
||||||
|
'/admin/accounts/antigravity/default-model-mapping'
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
export const accountsAPI = {
|
export const accountsAPI = {
|
||||||
list,
|
list,
|
||||||
getById,
|
getById,
|
||||||
@@ -412,7 +423,8 @@ export const accountsAPI = {
|
|||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
syncFromCrs,
|
syncFromCrs,
|
||||||
exportData,
|
exportData,
|
||||||
importData
|
importData,
|
||||||
|
getAntigravityDefaultModelMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
export default accountsAPI
|
export default accountsAPI
|
||||||
|
|||||||
@@ -1980,7 +1980,7 @@ import {
|
|||||||
getModelsByPlatform,
|
getModelsByPlatform,
|
||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
buildModelMappingObject,
|
buildModelMappingObject,
|
||||||
antigravityDefaultMappings,
|
fetchAntigravityDefaultMappings,
|
||||||
isValidWildcardPattern
|
isValidWildcardPattern
|
||||||
} from '@/composables/useModelWhitelist'
|
} from '@/composables/useModelWhitelist'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
@@ -2270,7 +2270,9 @@ watch(
|
|||||||
// Antigravity: 默认使用映射模式并填充默认映射
|
// Antigravity: 默认使用映射模式并填充默认映射
|
||||||
if (form.platform === 'antigravity') {
|
if (form.platform === 'antigravity') {
|
||||||
antigravityModelRestrictionMode.value = 'mapping'
|
antigravityModelRestrictionMode.value = 'mapping'
|
||||||
antigravityModelMappings.value = [...antigravityDefaultMappings]
|
fetchAntigravityDefaultMappings().then(mappings => {
|
||||||
|
antigravityModelMappings.value = [...mappings]
|
||||||
|
})
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
} else {
|
} else {
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
@@ -2318,7 +2320,9 @@ watch(
|
|||||||
// Antigravity: 默认使用映射模式并填充默认映射
|
// Antigravity: 默认使用映射模式并填充默认映射
|
||||||
if (newPlatform === 'antigravity') {
|
if (newPlatform === 'antigravity') {
|
||||||
antigravityModelRestrictionMode.value = 'mapping'
|
antigravityModelRestrictionMode.value = 'mapping'
|
||||||
antigravityModelMappings.value = [...antigravityDefaultMappings]
|
fetchAntigravityDefaultMappings().then(mappings => {
|
||||||
|
antigravityModelMappings.value = [...mappings]
|
||||||
|
})
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
accountCategory.value = 'oauth-based'
|
accountCategory.value = 'oauth-based'
|
||||||
antigravityAccountType.value = 'oauth'
|
antigravityAccountType.value = 'oauth'
|
||||||
@@ -2576,7 +2580,9 @@ const resetForm = () => {
|
|||||||
|
|
||||||
antigravityModelRestrictionMode.value = 'mapping'
|
antigravityModelRestrictionMode.value = 'mapping'
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
antigravityModelMappings.value = [...antigravityDefaultMappings]
|
fetchAntigravityDefaultMappings().then(mappings => {
|
||||||
|
antigravityModelMappings.value = [...mappings]
|
||||||
|
})
|
||||||
customErrorCodesEnabled.value = false
|
customErrorCodesEnabled.value = false
|
||||||
selectedErrorCodes.value = []
|
selectedErrorCodes.value = []
|
||||||
customErrorCodeInput.value = null
|
customErrorCodeInput.value = null
|
||||||
|
|||||||
@@ -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' }
|
{ 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 保持一致)
|
// Antigravity 默认映射(从后端 API 获取,与 constants.go 保持一致)
|
||||||
// 基于官方 API 返回的模型列表,只支持 Claude 4.5+ 和 Gemini 2.5+
|
// 使用 fetchAntigravityDefaultMappings() 异步获取
|
||||||
// 精确匹配,无通配符
|
import { getAntigravityDefaultModelMapping } from '@/api/admin/accounts'
|
||||||
export const antigravityDefaultMappings: { from: string; to: string }[] = [
|
|
||||||
// Claude 白名单
|
let _antigravityDefaultMappingsCache: { from: string; to: string }[] | null = null
|
||||||
{ from: 'claude-opus-4-6', to: 'claude-opus-4-6' },
|
|
||||||
{ from: 'claude-opus-4-5-thinking', to: 'claude-opus-4-5-thinking' },
|
export async function fetchAntigravityDefaultMappings(): Promise<{ from: string; to: string }[]> {
|
||||||
{ from: 'claude-sonnet-4-5', to: 'claude-sonnet-4-5' },
|
if (_antigravityDefaultMappingsCache !== null) {
|
||||||
{ from: 'claude-sonnet-4-5-thinking', to: 'claude-sonnet-4-5-thinking' },
|
return _antigravityDefaultMappingsCache
|
||||||
// Claude 详细版本 ID 映射
|
}
|
||||||
{ from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-thinking' },
|
try {
|
||||||
{ from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5' },
|
const mapping = await getAntigravityDefaultModelMapping()
|
||||||
// Claude Haiku → Sonnet(无 Haiku 支持)
|
_antigravityDefaultMappingsCache = Object.entries(mapping).map(([from, to]) => ({ from, to }))
|
||||||
{ from: 'claude-haiku-4-5', to: 'claude-sonnet-4-5' },
|
} catch (e) {
|
||||||
{ from: 'claude-haiku-4-5-20251001', to: 'claude-sonnet-4-5' },
|
console.warn('[fetchAntigravityDefaultMappings] API failed, using empty fallback', e)
|
||||||
// Gemini 2.5 白名单
|
_antigravityDefaultMappingsCache = []
|
||||||
{ from: 'gemini-2.5-flash', to: 'gemini-2.5-flash' },
|
}
|
||||||
{ from: 'gemini-2.5-flash-lite', to: 'gemini-2.5-flash-lite' },
|
return _antigravityDefaultMappingsCache
|
||||||
{ 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' }
|
|
||||||
]
|
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// 常用错误码
|
// 常用错误码
|
||||||
|
|||||||
Reference in New Issue
Block a user