diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts index c433623..076b4ab 100644 --- a/backend/src/db/index.ts +++ b/backend/src/db/index.ts @@ -947,11 +947,17 @@ export async function getModelsWithProviderBySystemName( /** * list unique system names (for global model registry) + * Returns active model names and archived names (deleted but referenced by historical requests) */ export async function listUniqueSystemNames( modelType?: ModelTypeEnumType, -): Promise { +): Promise<{ + active: string[]; + archived: { systemName: string; modelType: "chat" | "embedding" }[]; +}> { logger.debug("listUniqueSystemNames", modelType); + + // Get active model names (non-deleted models with non-deleted providers) const r = await db .selectDistinct({ systemName: schema.ModelsTable.systemName }) .from(schema.ModelsTable) @@ -967,7 +973,40 @@ export async function listUniqueSystemNames( ), ) .orderBy(asc(schema.ModelsTable.systemName)); - return r.map((x) => x.systemName); + const active = r.map((x) => x.systemName); + const activeSet = new Set(active); + + // Get archived model names: exist in completions/embeddings but not in active models + // Each model tagged with its type based on which table it comes from + const historicalResult = await db.execute(sql` + SELECT model AS name, 'chat' AS model_type FROM completions WHERE deleted = false + UNION + SELECT model AS name, 'embedding' AS model_type FROM embeddings WHERE deleted = false + `); + const historicalRows = historicalResult as unknown as { + name: string; + model_type: "chat" | "embedding"; + }[]; + + // Filter out active models and respect modelType filter + const archivedMap = new Map< + string, + "chat" | "embedding" + >(); + for (const row of historicalRows) { + if (activeSet.has(row.name)) continue; + if (modelType && row.model_type !== modelType) continue; + // If a name appears in both tables, prefer 'chat' (it's the more common case) + if (!archivedMap.has(row.name)) { + archivedMap.set(row.name, row.model_type); + } + } + + const archived = Array.from(archivedMap.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([systemName, mt]) => ({ systemName, modelType: mt })); + + return { active, archived }; } /** diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index a1a08fa..4299bf8 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -331,6 +331,8 @@ "pages.models.registry.WeightConfiguration": "Weight Configuration", "pages.models.registry.History": "History", "pages.models.registry.ViewHistory": "View request history", + "pages.models.registry.Archived": "Archived", + "pages.models.registry.ArchivedTooltip": "This model has been deleted but has historical requests", "routes.models.Title": "Models", "routes.models.Description": "Configure model providers and global models.", "routes.models.nav.Providers": "Model Providers", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index ea02d0e..baa9aff 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -332,6 +332,8 @@ "pages.models.registry.WeightConfiguration": "权重配置", "pages.models.registry.History": "历史", "pages.models.registry.ViewHistory": "查看请求历史", + "pages.models.registry.Archived": "已归档", + "pages.models.registry.ArchivedTooltip": "该模型已删除但存在历史请求记录", "routes.models.Title": "模型", "routes.models.Description": "配置模型供应商和全局模型。", "routes.models.nav.Providers": "模型供应商", diff --git a/frontend/src/pages/models/models-registry-table.tsx b/frontend/src/pages/models/models-registry-table.tsx index 4d0487e..6c8f67f 100644 --- a/frontend/src/pages/models/models-registry-table.tsx +++ b/frontend/src/pages/models/models-registry-table.tsx @@ -120,7 +120,7 @@ function ModelsBySystemNameTable({ systemName }: { systemName: string }) { const { data: models = [], isLoading } = useQuery({ queryKey: ['models', 'by-system-name', systemName], queryFn: async () => { - const { data, error } = await api.admin.models['by-system-name'][systemName].get() + const { data, error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].get() if (error) throw error return data as ModelWithProvider[] }, @@ -128,7 +128,7 @@ function ModelsBySystemNameTable({ systemName }: { systemName: string }) { const updateWeightsMutation = useMutation({ mutationFn: async (weights: { modelId: number; weight: number }[]) => { - const { error } = await api.admin.models['by-system-name'][systemName].weights.put({ + const { error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].weights.put({ weights, }) if (error) throw error diff --git a/frontend/src/pages/settings/models-settings-page.tsx b/frontend/src/pages/settings/models-settings-page.tsx index cf8a007..eb1be7f 100644 --- a/frontend/src/pages/settings/models-settings-page.tsx +++ b/frontend/src/pages/settings/models-settings-page.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' -import { CpuIcon, HistoryIcon } from 'lucide-react' +import { ArchiveIcon, CpuIcon, HistoryIcon } from 'lucide-react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' @@ -25,8 +25,19 @@ interface ModelWithProvider { } } -export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) { +interface ArchivedModel { + systemName: string + modelType: 'chat' | 'embedding' +} + +interface ModelsSettingsPageProps { + activeSystemNames: string[] + archivedSystemNames: ArchivedModel[] +} + +export function ModelsSettingsPage({ activeSystemNames, archivedSystemNames }: ModelsSettingsPageProps) { const { t } = useTranslation() + const hasAny = activeSystemNames.length > 0 || archivedSystemNames.length > 0 return (
@@ -36,7 +47,7 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) { {t('pages.models.registry.Description')} - {systemNames.length > 0 ? ( + {hasAny ? ( @@ -47,9 +58,12 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) { - {systemNames.map((systemName) => ( + {activeSystemNames.map((systemName) => ( ))} + {archivedSystemNames.map((item) => ( + + ))}
) : ( @@ -69,7 +83,7 @@ function ModelRow({ systemName }: { systemName: string }) { const { data: models = [], isLoading } = useQuery({ queryKey: ['models', 'by-system-name', systemName], queryFn: async () => { - const { data, error } = await api.admin.models['by-system-name'][systemName].get() + const { data, error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].get() if (error) throw error return data as ModelWithProvider[] }, @@ -157,6 +171,50 @@ function ModelRow({ systemName }: { systemName: string }) { ) } +function ArchivedModelRow({ systemName, modelType }: { systemName: string; modelType: 'chat' | 'embedding' }) { + const { t } = useTranslation() + const navigate = useNavigate() + + const handleHistoryClick = (e: React.MouseEvent) => { + e.stopPropagation() + if (modelType === 'embedding') { + navigate({ to: '/embeddings', search: { model: systemName } }) + } else { + navigate({ to: '/requests', search: { model: systemName } }) + } + } + + return ( + + +
+ + {systemName} +
+
+ + - + + + + {t('pages.models.registry.Archived')} + + + + + +
+ ) +} + interface LoadBalancingDialogProps { open: boolean onOpenChange: (open: boolean) => void @@ -182,7 +240,7 @@ function LoadBalancingDialog({ open, onOpenChange, systemName, models }: LoadBal const updateWeightsMutation = useMutation({ mutationFn: async (weights: { modelId: number; weight: number }[]) => { - const { error } = await api.admin.models['by-system-name'][systemName].weights.put({ + const { error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].weights.put({ weights, }) if (error) throw error diff --git a/frontend/src/routes/models/registry.tsx b/frontend/src/routes/models/registry.tsx index 7baabab..d281591 100644 --- a/frontend/src/routes/models/registry.tsx +++ b/frontend/src/routes/models/registry.tsx @@ -27,5 +27,5 @@ export const Route = createFileRoute('/models/registry')({ function RouteComponent() { const { data } = useSuspenseQuery(systemNamesQueryOptions()) - return + return }