Merge pull request #2892 from Arron196/feat/sync-upstream-models-on-create

feat: 添加账号时支持同步上游支持的模型
This commit is contained in:
Wesley Liddick 2026-06-01 09:58:33 +08:00 committed by GitHub
commit 910ff3cd58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 109 additions and 8 deletions

View File

@ -2131,6 +2131,56 @@ func (h *AccountHandler) SyncUpstreamModels(c *gin.Context) {
response.Success(c, gin.H{"models": models})
}
// SyncUpstreamModelsPreview handles syncing live supported models using provided credentials (no account ID needed).
// POST /api/v1/admin/accounts/models/sync-upstream-preview
func (h *AccountHandler) SyncUpstreamModelsPreview(c *gin.Context) {
var req struct {
Platform string `json:"platform" binding:"required"`
Type string `json:"type" binding:"required"`
BaseURL string `json:"base_url"`
APIKey string `json:"api_key" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
tempAccount := &service.Account{
Platform: req.Platform,
Type: req.Type,
Credentials: map[string]any{
"api_key": req.APIKey,
"base_url": req.BaseURL,
},
}
if h.accountTestService == nil {
response.InternalError(c, "Account test service is not configured")
return
}
models, err := h.accountTestService.FetchUpstreamSupportedModels(c.Request.Context(), tempAccount)
if err != nil {
var syncErr *service.UpstreamModelSyncError
if errors.As(err, &syncErr) {
switch syncErr.Kind {
case service.UpstreamModelSyncErrorConfiguration, service.UpstreamModelSyncErrorUnsupported:
response.BadRequest(c, syncErr.SafeMessage())
default:
slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform, "kind", syncErr.Kind)
response.Error(c, http.StatusBadGateway, syncErr.SafeMessage())
}
return
}
slog.Warn("sync_upstream_models_preview_failed", "platform", req.Platform)
response.Error(c, http.StatusBadGateway, "Failed to sync upstream models from upstream")
return
}
response.Success(c, gin.H{"models": models})
}
// SetPrivacy handles setting privacy for a single OpenAI/Antigravity OAuth account
// POST /api/v1/admin/accounts/:id/set-privacy
func (h *AccountHandler) SetPrivacy(c *gin.Context) {

View File

@ -302,6 +302,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
accounts.GET("/:id/temp-unschedulable", h.Admin.Account.GetTempUnschedulable)
accounts.DELETE("/:id/temp-unschedulable", h.Admin.Account.ClearTempUnschedulable)
accounts.POST("/:id/schedulable", h.Admin.Account.SetSchedulable)
accounts.POST("/models/sync-upstream-preview", h.Admin.Account.SyncUpstreamModelsPreview)
accounts.GET("/:id/models", h.Admin.Account.GetAvailableModels)
accounts.POST("/:id/models/sync-upstream", h.Admin.Account.SyncUpstreamModels)
accounts.POST("/batch", h.Admin.Account.BatchCreate)

View File

@ -487,6 +487,23 @@ export async function syncUpstreamModels(id: number): Promise<SyncUpstreamModels
return data
}
export interface SyncUpstreamPreviewParams {
platform: string
type: string
base_url?: string
api_key: string
}
/**
* Preview upstream models without a saved account (create-flow)
* @param params - Connection credentials
* @returns List of model IDs returned by the upstream
*/
export async function syncUpstreamModelsPreview(params: SyncUpstreamPreviewParams): Promise<SyncUpstreamModelsResult> {
const { data } = await apiClient.post<SyncUpstreamModelsResult>('/admin/accounts/models/sync-upstream-preview', params)
return data
}
export interface CRSPreviewAccount {
crs_account_id: string
kind: string
@ -703,6 +720,7 @@ export const accountsAPI = {
setSchedulable,
getAvailableModels,
syncUpstreamModels,
syncUpstreamModelsPreview,
generateAuthUrl,
exchangeCode,
refreshOpenAIToken,

View File

@ -1124,7 +1124,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@ -1562,7 +1562,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" />
<ModelWhitelistSelector v-model="allowedModels" platform="anthropic" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{ t('admin.accounts.supportsAllModels') }}</span>
@ -1813,7 +1813,7 @@
<!-- Whitelist Mode -->
<div v-if="modelRestrictionMode === 'whitelist'">
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" />
<ModelWhitelistSelector v-model="allowedModels" :platform="form.platform" :sync-credentials="syncPreviewCredentials" />
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }}
<span v-if="allowedModels.length === 0">{{
@ -3361,6 +3361,17 @@ const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock' | 'service_acco
const addMethod = ref<AddMethod>('oauth') // For oauth-based: 'oauth' or 'setup-token'
const apiKeyBaseUrl = ref('https://api.anthropic.com')
const apiKeyValue = ref('')
const syncPreviewCredentials = computed(() => {
if (!apiKeyValue.value) return undefined
return {
platform: form.platform,
type: form.type,
base_url: apiKeyBaseUrl.value || undefined,
api_key: apiKeyValue.value
}
})
const editQuotaLimit = ref<number | null>(null)
const editQuotaDailyLimit = ref<number | null>(null)
const editQuotaWeeklyLimit = ref<number | null>(null)

View File

@ -133,6 +133,7 @@ import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { accountsAPI } from '@/api/admin/accounts'
import type { SyncUpstreamPreviewParams } from '@/api/admin/accounts'
import ModelIcon from '@/components/common/ModelIcon.vue'
import Icon from '@/components/icons/Icon.vue'
import { allModels, getModelsByPlatform } from '@/composables/useModelWhitelist'
@ -144,6 +145,12 @@ const props = defineProps<{
platform?: string
platforms?: string[]
accountId?: number
syncCredentials?: {
platform: string
type: string
base_url?: string
api_key: string
}
}>()
const emit = defineEmits<{
@ -176,9 +183,14 @@ 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()))
if (props.accountId) {
if (normalizedPlatforms.value.length === 0) return true
return normalizedPlatforms.value.some(platform => upstreamSyncPlatforms.has(platform.toLowerCase()))
}
if (props.syncCredentials) {
return upstreamSyncPlatforms.has(props.syncCredentials.platform.toLowerCase())
}
return false
})
const availableOptions = computed(() => {
@ -249,11 +261,20 @@ const fillRelated = () => {
}
const syncUpstreamModels = async () => {
if (!props.accountId || isSyncingUpstream.value) return
if (isSyncingUpstream.value) return
if (!props.accountId && !props.syncCredentials) return
isSyncingUpstream.value = true
try {
const result = await accountsAPI.syncUpstreamModels(props.accountId)
let result
if (props.accountId) {
result = await accountsAPI.syncUpstreamModels(props.accountId)
} else if (props.syncCredentials) {
result = await accountsAPI.syncUpstreamModelsPreview(props.syncCredentials as SyncUpstreamPreviewParams)
} else {
return
}
const upstreamModels = result.models.map(model => model.trim()).filter(Boolean)
if (upstreamModels.length === 0) {
appStore.showInfo(t('admin.accounts.syncUpstreamModelsEmpty'))