Merge pull request #2892 from Arron196/feat/sync-upstream-models-on-create
feat: 添加账号时支持同步上游支持的模型
This commit is contained in:
commit
910ff3cd58
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user