Merge pull request #2554 from Arron196/feature/sync-upstream-models-pr

feat: 支持从上游同步账号可用模型列表
This commit is contained in:
Wesley Liddick
2026-05-19 14:42:47 +08:00
committed by GitHub
11 changed files with 1015 additions and 5 deletions
+15
View File
@@ -446,6 +446,20 @@ export async function getAvailableModels(id: number): Promise<ClaudeModel[]> {
return data
}
export interface SyncUpstreamModelsResult {
models: string[]
}
/**
* Sync live supported models from the account's upstream model-list endpoint
* @param id - Account ID
* @returns List of model IDs returned by the upstream
*/
export async function syncUpstreamModels(id: number): Promise<SyncUpstreamModelsResult> {
const { data } = await apiClient.post<SyncUpstreamModelsResult>(`/admin/accounts/${id}/models/sync-upstream`)
return data
}
export interface CRSPreviewAccount {
crs_account_id: string
kind: string
@@ -660,6 +674,7 @@ export const accountsAPI = {
resetTempUnschedulable,
setSchedulable,
getAvailableModels,
syncUpstreamModels,
generateAuthUrl,
exchangeCode,
refreshOpenAIToken,
@@ -139,7 +139,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" />
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" :account-id="account?.id" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@@ -454,7 +454,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" />
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" :account-id="account?.id" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@@ -666,7 +666,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" />
<ModelWhitelistSelector v-model="allowedModels" :platform="account?.platform || 'anthropic'" :account-id="account?.id" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@@ -987,6 +987,17 @@
<p class="text-xs text-purple-700 dark:text-purple-400">{{ t('admin.accounts.mapRequestModels') }}</p>
</div>
<div class="mb-3 flex flex-wrap gap-2">
<button
type="button"
@click="syncAntigravityUpstreamModels"
:disabled="isSyncingAntigravityUpstream || !account?.id"
class="rounded-lg border border-emerald-200 px-3 py-1.5 text-sm text-emerald-600 hover:bg-emerald-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-emerald-800 dark:text-emerald-400 dark:hover:bg-emerald-900/30"
>
{{ isSyncingAntigravityUpstream ? t('admin.accounts.syncUpstreamModelsLoading') : t('admin.accounts.syncUpstreamModels') }}
</button>
</div>
<div v-if="antigravityModelMappings.length > 0" class="mb-3 space-y-2">
<div
v-for="(mapping, index) in antigravityModelMappings"
@@ -2288,6 +2299,7 @@ const allowOverages = ref(false) // For antigravity accounts: enable AI Credits
const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
const antigravityWhitelistModels = ref<string[]>([])
const antigravityModelMappings = ref<ModelMapping[]>([])
const isSyncingAntigravityUpstream = ref(false)
const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-model-mapping')
@@ -2935,6 +2947,40 @@ const addAntigravityPresetMapping = (from: string, to: string) => {
antigravityModelMappings.value.push({ from, to })
}
const syncAntigravityUpstreamModels = async () => {
if (!props.account?.id || isSyncingAntigravityUpstream.value) return
isSyncingAntigravityUpstream.value = true
try {
const result = await adminAPI.accounts.syncUpstreamModels(props.account.id)
const upstreamModels = result.models.map((model) => model.trim()).filter(Boolean)
if (upstreamModels.length === 0) {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsEmpty'))
return
}
let addedCount = 0
for (const model of upstreamModels) {
const exists = antigravityModelMappings.value.some((mapping) => mapping.from === model)
if (!exists) {
antigravityModelMappings.value.push({ from: model, to: model })
addedCount += 1
}
}
if (addedCount > 0) {
appStore.showSuccess(t('admin.accounts.syncUpstreamModelsSuccess', { count: addedCount, total: upstreamModels.length }))
} else {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsNoChanges', { count: upstreamModels.length }))
}
} catch (error) {
const message = error instanceof Error ? error.message : t('admin.accounts.syncUpstreamModelsFailed')
appStore.showError(t('admin.accounts.syncUpstreamModelsError', { message }))
} finally {
isSyncingAntigravityUpstream.value = false
}
}
// Error code toggle helper
const toggleErrorCode = (code: number) => {
const index = selectedErrorCodes.value.indexOf(code)
@@ -85,6 +85,15 @@
>
{{ t('admin.accounts.fillRelatedModels') }}
</button>
<button
v-if="canSyncUpstream"
type="button"
@click="syncUpstreamModels"
:disabled="isSyncingUpstream"
class="rounded-lg border border-emerald-200 px-3 py-1.5 text-sm text-emerald-600 hover:bg-emerald-50 disabled:cursor-not-allowed disabled:opacity-60 dark:border-emerald-800 dark:text-emerald-400 dark:hover:bg-emerald-900/30"
>
{{ isSyncingUpstream ? t('admin.accounts.syncUpstreamModelsLoading') : t('admin.accounts.syncUpstreamModels') }}
</button>
<button
type="button"
@click="clearAll"
@@ -123,6 +132,7 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { accountsAPI } from '@/api/admin/accounts'
import ModelIcon from '@/components/common/ModelIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
@@ -133,6 +143,7 @@ const props = defineProps<{
modelValue: string[]
platform?: string
platforms?: string[]
accountId?: number
}>()
const emit = defineEmits<{
@@ -145,6 +156,7 @@ const showDropdown = ref(false)
const searchQuery = ref('')
const customModel = ref('')
const isComposing = ref(false)
const isSyncingUpstream = ref(false)
const normalizedPlatforms = computed(() => {
const rawPlatforms =
props.platforms && props.platforms.length > 0
@@ -162,6 +174,13 @@ const normalizedPlatforms = computed(() => {
)
})
const upstreamSyncPlatforms = new Set(['anthropic', 'openai', 'gemini', 'antigravity'])
const canSyncUpstream = computed(() => {
if (!props.accountId) return false
if (normalizedPlatforms.value.length === 0) return true
return normalizedPlatforms.value.some(platform => upstreamSyncPlatforms.has(platform.toLowerCase()))
})
const availableOptions = computed(() => {
if (normalizedPlatforms.value.length === 0) {
return allModels
@@ -229,6 +248,41 @@ const fillRelated = () => {
emit('update:modelValue', newModels)
}
const syncUpstreamModels = async () => {
if (!props.accountId || isSyncingUpstream.value) return
isSyncingUpstream.value = true
try {
const result = await accountsAPI.syncUpstreamModels(props.accountId)
const upstreamModels = result.models.map(model => model.trim()).filter(Boolean)
if (upstreamModels.length === 0) {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsEmpty'))
return
}
const newModels = [...props.modelValue]
let addedCount = 0
for (const model of upstreamModels) {
if (!newModels.includes(model)) {
newModels.push(model)
addedCount += 1
}
}
emit('update:modelValue', newModels)
if (addedCount > 0) {
appStore.showSuccess(t('admin.accounts.syncUpstreamModelsSuccess', { count: addedCount, total: upstreamModels.length }))
} else {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsNoChanges', { count: upstreamModels.length }))
}
} catch (error) {
const message = error instanceof Error ? error.message : t('admin.accounts.syncUpstreamModelsFailed')
appStore.showError(t('admin.accounts.syncUpstreamModelsError', { message }))
} finally {
isSyncingUpstream.value = false
}
}
const clearAll = () => {
emit('update:modelValue', [])
}
+7
View File
@@ -3229,6 +3229,13 @@ export default {
searchModels: 'Search models...',
noMatchingModels: 'No matching models',
fillRelatedModels: 'Sync latest supported models',
syncUpstreamModels: 'Sync upstream supported models',
syncUpstreamModelsLoading: 'Syncing upstream...',
syncUpstreamModelsSuccess: 'Synced {count} new model(s) from upstream ({total} upstream total)',
syncUpstreamModelsNoChanges: 'All {count} upstream model(s) are already in the whitelist',
syncUpstreamModelsEmpty: 'Upstream returned no models to sync',
syncUpstreamModelsFailed: 'Failed to sync upstream models',
syncUpstreamModelsError: 'Failed to sync upstream models: {message}',
clearAllModels: 'Clear all models',
customModelName: 'Custom model name',
enterCustomModelName: 'Enter custom model name',
+7
View File
@@ -3373,6 +3373,13 @@ export default {
searchModels: '搜索模型...',
noMatchingModels: '没有匹配的模型',
fillRelatedModels: '同步最新支持模型',
syncUpstreamModels: '同步上游支持的模型',
syncUpstreamModelsLoading: '同步上游中...',
syncUpstreamModelsSuccess: '已从上游同步 {count} 个新模型(上游共 {total} 个)',
syncUpstreamModelsNoChanges: '上游 {count} 个模型均已在白名单中',
syncUpstreamModelsEmpty: '上游没有返回可同步的模型',
syncUpstreamModelsFailed: '同步上游模型失败',
syncUpstreamModelsError: '同步上游模型失败:{message}',
clearAllModels: '清除所有模型',
customModelName: '自定义模型名称',
enterCustomModelName: '输入自定义模型名称',