diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 4f566a8be9f..5719534230b 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -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) { diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 2301adda6bf..9a3253b55dd 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -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) diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 6fd23c475d4..bb75e30251d 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -487,6 +487,23 @@ export async function syncUpstreamModels(id: number): Promise { + const { data } = await apiClient.post('/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, diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index a8abfd9f419..d1a7729a58d 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -1124,7 +1124,7 @@
- +

{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ @@ -1562,7 +1562,7 @@

- +

{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ t('admin.accounts.supportsAllModels') }} @@ -1813,7 +1813,7 @@

- +

{{ t('admin.accounts.selectedModels', { count: allowedModels.length }) }} {{ @@ -3361,6 +3361,17 @@ const accountCategory = ref<'oauth-based' | 'apikey' | 'bedrock' | 'service_acco const addMethod = ref('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(null) const editQuotaDailyLimit = ref(null) const editQuotaWeeklyLimit = ref(null) diff --git a/frontend/src/components/account/ModelWhitelistSelector.vue b/frontend/src/components/account/ModelWhitelistSelector.vue index 9a0d6af80f3..d4d726d06ab 100644 --- a/frontend/src/components/account/ModelWhitelistSelector.vue +++ b/frontend/src/components/account/ModelWhitelistSelector.vue @@ -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'))