feat: add upstream model sync controls

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
benjamin 2026-05-18 19:01:55 +08:00
parent 3b4eccdd5d
commit 5713820813
2 changed files with 103 additions and 3 deletions

View File

@ -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)

View File

@ -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', [])
}