Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 41 additions & 2 deletions backend/src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]> {
): 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)
Expand All @@ -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 };
}

/**
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "模型供应商",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/pages/models/models-registry-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,15 @@ 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[]
},
})

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
Expand Down
70 changes: 64 additions & 6 deletions frontend/src/pages/settings/models-settings-page.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 (
<div className="space-y-8">
Expand All @@ -36,7 +47,7 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) {
<CardDescription className="text-sm">{t('pages.models.registry.Description')}</CardDescription>
</CardHeader>
<CardContent>
{systemNames.length > 0 ? (
{hasAny ? (
<Table>
<TableHeader>
<TableRow>
Expand All @@ -47,9 +58,12 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) {
</TableRow>
</TableHeader>
<TableBody>
{systemNames.map((systemName) => (
{activeSystemNames.map((systemName) => (
<ModelRow key={systemName} systemName={systemName} />
))}
{archivedSystemNames.map((item) => (
<ArchivedModelRow key={item.systemName} systemName={item.systemName} modelType={item.modelType} />
))}
</TableBody>
</Table>
) : (
Expand All @@ -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[]
},
Expand Down Expand Up @@ -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 (
<TableRow className="opacity-50">
<TableCell>
<div className="flex items-center gap-3">
<ArchiveIcon className="text-muted-foreground/50 size-5" />
<span className="text-muted-foreground font-mono text-sm font-medium line-through">{systemName}</span>
</div>
</TableCell>
<TableCell>
<span className="text-muted-foreground text-sm">-</span>
</TableCell>
<TableCell>
<span className="text-muted-foreground text-sm italic" title={t('pages.models.registry.ArchivedTooltip')}>
{t('pages.models.registry.Archived')}
</span>
</TableCell>
<TableCell className="text-center">
<Button
variant="ghost"
size="sm"
className="size-8 p-0"
onClick={handleHistoryClick}
title={t('pages.models.registry.ViewHistory')}
>
<HistoryIcon className="size-4" />
</Button>
</TableCell>
</TableRow>
)
}

interface LoadBalancingDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/models/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ export const Route = createFileRoute('/models/registry')({
function RouteComponent() {
const { data } = useSuspenseQuery(systemNamesQueryOptions())

return <ModelsSettingsPage systemNames={data} />
return <ModelsSettingsPage activeSystemNames={data.active} archivedSystemNames={data.archived} />
}