mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #2554 from Arron196/feature/sync-upstream-models-pr
feat: 支持从上游同步账号可用模型列表
This commit is contained in:
@@ -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', [])
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3373,6 +3373,13 @@ export default {
|
||||
searchModels: '搜索模型...',
|
||||
noMatchingModels: '没有匹配的模型',
|
||||
fillRelatedModels: '同步最新支持模型',
|
||||
syncUpstreamModels: '同步上游支持的模型',
|
||||
syncUpstreamModelsLoading: '同步上游中...',
|
||||
syncUpstreamModelsSuccess: '已从上游同步 {count} 个新模型(上游共 {total} 个)',
|
||||
syncUpstreamModelsNoChanges: '上游 {count} 个模型均已在白名单中',
|
||||
syncUpstreamModelsEmpty: '上游没有返回可同步的模型',
|
||||
syncUpstreamModelsFailed: '同步上游模型失败',
|
||||
syncUpstreamModelsError: '同步上游模型失败:{message}',
|
||||
clearAllModels: '清除所有模型',
|
||||
customModelName: '自定义模型名称',
|
||||
enterCustomModelName: '输入自定义模型名称',
|
||||
|
||||
Reference in New Issue
Block a user