feat(antigravity): comprehensive enhancements - model mapping, rate limiting, scheduling & ops

Key changes:
- Upgrade model mapping: Opus 4.5 → Opus 4.6-thinking with precise matching
- Unified rate limiting: scope-level → model-level with Redis snapshot sync
- Load-balanced scheduling by call count with smart retry mechanism
- Force cache billing support
- Model identity injection in prompts with leak prevention
- Thinking mode auto-handling (max_tokens/budget_tokens fix)
- Frontend: whitelist mode toggle, model mapping validation, status indicators
- Gemini session fallback with Redis Trie O(L) matching
- Ops: enhanced concurrency monitoring, account availability, retry logic
- Migration scripts: 049-051 for model mapping unification
This commit is contained in:
erio
2026-02-07 12:31:10 +08:00
parent e617b45ba3
commit 5e98445b22
73 changed files with 8553 additions and 1926 deletions

View File

@@ -698,6 +698,97 @@
</div>
</div>
<!-- Antigravity model restriction (applies to OAuth + Upstream) -->
<!-- Antigravity 只支持模型映射模式不支持白名单模式 -->
<div v-if="form.platform === 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
<!-- Mapping Mode Only (no toggle for Antigravity) -->
<div>
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
<p class="text-xs text-purple-700 dark:text-purple-400">
{{ t('admin.accounts.mapRequestModels') }}
</p>
</div>
<div v-if="antigravityModelMappings.length > 0" class="mb-3 space-y-2">
<div
v-for="(mapping, index) in antigravityModelMappings"
:key="index"
class="space-y-1"
>
<div class="flex items-center gap-2">
<input
v-model="mapping.from"
type="text"
:class="[
'input flex-1',
!isValidWildcardPattern(mapping.from) ? 'border-red-500 dark:border-red-500' : ''
]"
:placeholder="t('admin.accounts.requestModel')"
/>
<svg class="h-4 w-4 flex-shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
<input
v-model="mapping.to"
type="text"
:class="[
'input flex-1',
mapping.to.includes('*') ? 'border-red-500 dark:border-red-500' : ''
]"
:placeholder="t('admin.accounts.actualModel')"
/>
<button
type="button"
@click="removeAntigravityModelMapping(index)"
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
</div>
<!-- 校验错误提示 -->
<p v-if="!isValidWildcardPattern(mapping.from)" class="text-xs text-red-500">
{{ t('admin.accounts.wildcardOnlyAtEnd') }}
</p>
<p v-if="mapping.to.includes('*')" class="text-xs text-red-500">
{{ t('admin.accounts.targetNoWildcard') }}
</p>
</div>
</div>
<button
type="button"
@click="addAntigravityModelMapping"
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
>
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{{ t('admin.accounts.addMapping') }}
</button>
<div class="flex flex-wrap gap-2">
<button
v-for="preset in antigravityPresetMappings"
:key="preset.label"
type="button"
@click="addAntigravityPresetMapping(preset.from, preset.to)"
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
>
+ {{ preset.label }}
</button>
</div>
</div>
</div>
<!-- Add Method (only for Anthropic OAuth-based type) -->
<div v-if="form.platform === 'anthropic' && isOAuthFlow">
<label class="input-label">{{ t('admin.accounts.addMethod') }}</label>
@@ -1883,7 +1974,15 @@
import { ref, reactive, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { claudeModels, getPresetMappingsByPlatform, getModelsByPlatform, commonErrorCodes, buildModelMappingObject } from '@/composables/useModelWhitelist'
import {
claudeModels,
getPresetMappingsByPlatform,
getModelsByPlatform,
commonErrorCodes,
buildModelMappingObject,
antigravityDefaultMappings,
isValidWildcardPattern
} from '@/composables/useModelWhitelist'
import { useAuthStore } from '@/stores/auth'
import { adminAPI } from '@/api/admin'
import {
@@ -2022,6 +2121,10 @@ const mixedScheduling = ref(false) // For antigravity accounts: enable mixed sch
const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream
const upstreamBaseUrl = ref('') // For upstream type: base URL
const upstreamApiKey = ref('') // For upstream type: API key
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const antigravityWhitelistModels = ref<string[]>([])
const antigravityModelMappings = ref<ModelMapping[]>([])
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
@@ -2164,6 +2267,16 @@ watch(
if (newVal) {
// Modal opened - fill related models
allowedModels.value = [...getModelsByPlatform(form.platform)]
// Antigravity: 默认使用映射模式并填充默认映射
if (form.platform === 'antigravity') {
antigravityModelRestrictionMode.value = 'mapping'
antigravityModelMappings.value = [...antigravityDefaultMappings]
antigravityWhitelistModels.value = []
} else {
antigravityWhitelistModels.value = []
antigravityModelMappings.value = []
antigravityModelRestrictionMode.value = 'mapping'
}
} else {
resetForm()
}
@@ -2202,15 +2315,22 @@ watch(
// Clear model-related settings
allowedModels.value = []
modelMappings.value = []
// Antigravity: 默认使用映射模式并填充默认映射
if (newPlatform === 'antigravity') {
antigravityModelRestrictionMode.value = 'mapping'
antigravityModelMappings.value = [...antigravityDefaultMappings]
antigravityWhitelistModels.value = []
accountCategory.value = 'oauth-based'
antigravityAccountType.value = 'oauth'
} else {
antigravityWhitelistModels.value = []
antigravityModelMappings.value = []
antigravityModelRestrictionMode.value = 'mapping'
}
// Reset Anthropic-specific settings when switching to other platforms
if (newPlatform !== 'anthropic') {
interceptWarmupRequests.value = false
}
// Antigravity: reset to OAuth by default, but allow upstream selection
if (newPlatform === 'antigravity') {
accountCategory.value = 'oauth-based'
antigravityAccountType.value = 'oauth'
}
// Reset OAuth states
oauth.resetState()
openaiOAuth.resetState()
@@ -2254,6 +2374,15 @@ watch(
}
)
watch(
[antigravityModelRestrictionMode, () => form.platform],
([, platform]) => {
if (platform !== 'antigravity') return
// Antigravity 默认不做限制:白名单留空表示允许所有(包含未来新增模型)。
// 如果需要快速填充常用模型,可在组件内点“填充相关模型”。
}
)
// Model mapping helpers
const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' })
@@ -2271,6 +2400,22 @@ const addPresetMapping = (from: string, to: string) => {
modelMappings.value.push({ from, to })
}
const addAntigravityModelMapping = () => {
antigravityModelMappings.value.push({ from: '', to: '' })
}
const removeAntigravityModelMapping = (index: number) => {
antigravityModelMappings.value.splice(index, 1)
}
const addAntigravityPresetMapping = (from: string, to: string) => {
if (antigravityModelMappings.value.some((m) => m.from === from)) {
appStore.showInfo(t('admin.accounts.mappingExists', { model: from }))
return
}
antigravityModelMappings.value.push({ from, to })
}
// Error code toggle helper
const toggleErrorCode = (code: number) => {
const index = selectedErrorCodes.value.indexOf(code)
@@ -2428,6 +2573,10 @@ const resetForm = () => {
modelMappings.value = []
modelRestrictionMode.value = 'whitelist'
allowedModels.value = [...claudeModels] // Default fill related models
antigravityModelRestrictionMode.value = 'mapping'
antigravityWhitelistModels.value = []
antigravityModelMappings.value = [...antigravityDefaultMappings]
customErrorCodesEnabled.value = false
selectedErrorCodes.value = []
customErrorCodeInput.value = null
@@ -2541,12 +2690,24 @@ const handleSubmit = async () => {
return
}
// Build upstream credentials (and optional model restriction)
const credentials: Record<string, unknown> = {
base_url: upstreamBaseUrl.value.trim(),
api_key: upstreamApiKey.value.trim()
}
// Antigravity 只使用映射模式
const antigravityModelMapping = buildModelMappingObject(
'mapping',
[],
antigravityModelMappings.value
)
if (antigravityModelMapping) {
credentials.model_mapping = antigravityModelMapping
}
submitting.value = true
try {
const credentials: Record<string, unknown> = {
base_url: upstreamBaseUrl.value.trim(),
api_key: upstreamApiKey.value.trim()
}
await createAccountAndFinish(form.platform, 'upstream', credentials)
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
@@ -2752,11 +2913,20 @@ const handleAntigravityExchange = async (authCode: string) => {
state: stateToUse,
proxyId: form.proxy_id
})
if (!tokenInfo) return
if (!tokenInfo) return
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
await createAccountAndFinish('antigravity', 'oauth', credentials, extra)
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
// Antigravity 只使用映射模式
const antigravityModelMapping = buildModelMappingObject(
'mapping',
[],
antigravityModelMappings.value
)
if (antigravityModelMapping) {
credentials.model_mapping = antigravityModelMapping
}
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
await createAccountAndFinish('antigravity', 'oauth', credentials, extra)
} catch (error: any) {
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
appStore.showError(antigravityOAuth.error.value)