diff --git a/alembic/versions/20251210_baseline.py b/alembic/versions/20251210_baseline.py index e46d1e4..78f2aa9 100644 --- a/alembic/versions/20251210_baseline.py +++ b/alembic/versions/20251210_baseline.py @@ -20,10 +20,10 @@ depends_on = None def upgrade() -> None: - # Create ENUM types - op.execute("CREATE TYPE userrole AS ENUM ('admin', 'user')") + # Create ENUM types (with IF NOT EXISTS for idempotency) + op.execute("DO $$ BEGIN CREATE TYPE userrole AS ENUM ('admin', 'user'); EXCEPTION WHEN duplicate_object THEN NULL; END $$") op.execute( - "CREATE TYPE providerbillingtype AS ENUM ('monthly_quota', 'pay_as_you_go', 'free_tier')" + "DO $$ BEGIN CREATE TYPE providerbillingtype AS ENUM ('monthly_quota', 'pay_as_you_go', 'free_tier'); EXCEPTION WHEN duplicate_object THEN NULL; END $$" ) # ==================== users ==================== @@ -35,7 +35,7 @@ def upgrade() -> None: sa.Column("password_hash", sa.String(255), nullable=False), sa.Column( "role", - sa.Enum("admin", "user", name="userrole", create_type=False), + postgresql.ENUM("admin", "user", name="userrole", create_type=False), nullable=False, server_default="user", ), @@ -67,7 +67,7 @@ def upgrade() -> None: sa.Column("website", sa.String(500), nullable=True), sa.Column( "billing_type", - sa.Enum( + postgresql.ENUM( "monthly_quota", "pay_as_you_go", "free_tier", name="providerbillingtype", create_type=False ), nullable=False, diff --git a/frontend/src/api/admin.ts b/frontend/src/api/admin.ts index b581e16..c6ef16b 100644 --- a/frontend/src/api/admin.ts +++ b/frontend/src/api/admin.ts @@ -124,6 +124,27 @@ export interface ModelExport { config?: any } +// Provider 模型查询响应 +export interface ProviderModelsQueryResponse { + success: boolean + data: { + models: Array<{ + id: string + object?: string + created?: number + owned_by?: string + display_name?: string + api_format?: string + }> + error?: string + } + provider: { + id: string + name: string + display_name: string + } +} + export interface ConfigImportRequest extends ConfigExportData { merge_mode: 'skip' | 'overwrite' | 'error' } @@ -356,5 +377,14 @@ export const adminApi = { data ) return response.data + }, + + // 查询 Provider 可用模型(从上游 API 获取) + async queryProviderModels(providerId: string, apiKeyId?: string): Promise { + const response = await apiClient.post( + '/api/admin/provider-query/models', + { provider_id: providerId, api_key_id: apiKeyId } + ) + return response.data } } diff --git a/frontend/src/api/endpoints/types.ts b/frontend/src/api/endpoints/types.ts index 4f666ad..ea8ee22 100644 --- a/frontend/src/api/endpoints/types.ts +++ b/frontend/src/api/endpoints/types.ts @@ -1,3 +1,25 @@ +// API 格式常量 +export const API_FORMATS = { + CLAUDE: 'CLAUDE', + CLAUDE_CLI: 'CLAUDE_CLI', + OPENAI: 'OPENAI', + OPENAI_CLI: 'OPENAI_CLI', + GEMINI: 'GEMINI', + GEMINI_CLI: 'GEMINI_CLI', +} as const + +export type APIFormat = typeof API_FORMATS[keyof typeof API_FORMATS] + +// API 格式显示名称映射(按品牌分组:API 在前,CLI 在后) +export const API_FORMAT_LABELS: Record = { + [API_FORMATS.CLAUDE]: 'Claude', + [API_FORMATS.CLAUDE_CLI]: 'Claude CLI', + [API_FORMATS.OPENAI]: 'OpenAI', + [API_FORMATS.OPENAI_CLI]: 'OpenAI CLI', + [API_FORMATS.GEMINI]: 'Gemini', + [API_FORMATS.GEMINI_CLI]: 'Gemini CLI', +} + export interface ProviderEndpoint { id: string provider_id: string @@ -214,6 +236,7 @@ export interface ConcurrencyStatus { export interface ProviderModelAlias { name: string priority: number // 优先级(数字越小优先级越高) + api_formats?: string[] // 作用域(适用的 API 格式),为空表示对所有格式生效 } export interface Model { diff --git a/frontend/src/features/models/components/GlobalModelFormDialog.vue b/frontend/src/features/models/components/GlobalModelFormDialog.vue index 9a478d7..3e09409 100644 --- a/frontend/src/features/models/components/GlobalModelFormDialog.vue +++ b/frontend/src/features/models/components/GlobalModelFormDialog.vue @@ -396,15 +396,13 @@ interface ProviderGroup { const groupedModels = computed(() => { let models = allModels.value.filter(m => !m.deprecated) + // 搜索(支持空格分隔的多关键词 AND 搜索) if (searchQuery.value) { - const query = searchQuery.value.toLowerCase() - models = models.filter(model => - model.providerId.toLowerCase().includes(query) || - model.providerName.toLowerCase().includes(query) || - model.modelId.toLowerCase().includes(query) || - model.modelName.toLowerCase().includes(query) || - model.family?.toLowerCase().includes(query) - ) + const keywords = searchQuery.value.toLowerCase().split(/\s+/).filter(k => k.length > 0) + models = models.filter(model => { + const searchableText = `${model.providerId} ${model.providerName} ${model.modelId} ${model.modelName} ${model.family || ''}`.toLowerCase() + return keywords.every(keyword => searchableText.includes(keyword)) + }) } // 按提供商分组 @@ -425,10 +423,12 @@ const groupedModels = computed(() => { // 如果有搜索词,把提供商名称/ID匹配的排在前面 if (searchQuery.value) { - const query = searchQuery.value.toLowerCase() + const keywords = searchQuery.value.toLowerCase().split(/\s+/).filter(k => k.length > 0) result.sort((a, b) => { - const aProviderMatch = a.providerId.toLowerCase().includes(query) || a.providerName.toLowerCase().includes(query) - const bProviderMatch = b.providerId.toLowerCase().includes(query) || b.providerName.toLowerCase().includes(query) + const aText = `${a.providerId} ${a.providerName}`.toLowerCase() + const bText = `${b.providerId} ${b.providerName}`.toLowerCase() + const aProviderMatch = keywords.some(k => aText.includes(k)) + const bProviderMatch = keywords.some(k => bText.includes(k)) if (aProviderMatch && !bProviderMatch) return -1 if (!aProviderMatch && bProviderMatch) return 1 return a.providerName.localeCompare(b.providerName) diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index 167d737..8838420 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -526,7 +526,14 @@ @edit-model="handleEditModel" @delete-model="handleDeleteModel" @batch-assign="handleBatchAssign" - @manage-alias="handleManageAlias" + /> + + + @@ -629,16 +636,6 @@ @update:open="batchAssignDialogOpen = $event" @changed="handleBatchAssignChanged" /> - - - diff --git a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue index bc20a0a..1465c4e 100644 --- a/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue +++ b/frontend/src/features/providers/components/provider-tabs/ModelsTab.vue @@ -165,15 +165,6 @@ > -