Skip to content

Commit 80cc1ef

Browse files
pescnclaude
andauthored
feat(models): archived model display and URL encoding fix (#77)
* feat(models): show archived models with historical requests in registry Models that have been deleted but still have historical completion/embedding records now appear as grayed-out "Archived" entries in the global model registry. Users can click the history button to trace past requests. - Backend: `listUniqueSystemNames()` returns `{ active, archived }` using SQL UNION query on completions/embeddings tables - Frontend: new `ArchivedModelRow` component with archive icon, opacity, and strikethrough styling - i18n: added Archived/ArchivedTooltip keys for en-US and zh-CN Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(frontend): encode systemName in Eden Treaty path params Eden Treaty does not URL-encode dynamic path segments, causing 404 errors for model names containing slashes (e.g. `Qwen/Qwen3-8B`). Wrap all `by-system-name` path params with `encodeURIComponent()`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(models): address PR review — archived modelType filtering and routing - Backend: archived query now respects `modelType` param and returns `{ systemName, modelType }` per archived entry (instead of plain string) - Backend: simplified SQL by fetching all historical names and filtering in TypeScript, avoiding duplicated active-model logic in NOT IN subquery - Frontend: `ArchivedModelRow` routes to `/embeddings` for embedding models instead of hardcoding `/requests` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3121a7b commit 80cc1ef

6 files changed

Lines changed: 112 additions & 11 deletions

File tree

backend/src/db/index.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -947,11 +947,17 @@ export async function getModelsWithProviderBySystemName(
947947

948948
/**
949949
* list unique system names (for global model registry)
950+
* Returns active model names and archived names (deleted but referenced by historical requests)
950951
*/
951952
export async function listUniqueSystemNames(
952953
modelType?: ModelTypeEnumType,
953-
): Promise<string[]> {
954+
): Promise<{
955+
active: string[];
956+
archived: { systemName: string; modelType: "chat" | "embedding" }[];
957+
}> {
954958
logger.debug("listUniqueSystemNames", modelType);
959+
960+
// Get active model names (non-deleted models with non-deleted providers)
955961
const r = await db
956962
.selectDistinct({ systemName: schema.ModelsTable.systemName })
957963
.from(schema.ModelsTable)
@@ -967,7 +973,40 @@ export async function listUniqueSystemNames(
967973
),
968974
)
969975
.orderBy(asc(schema.ModelsTable.systemName));
970-
return r.map((x) => x.systemName);
976+
const active = r.map((x) => x.systemName);
977+
const activeSet = new Set(active);
978+
979+
// Get archived model names: exist in completions/embeddings but not in active models
980+
// Each model tagged with its type based on which table it comes from
981+
const historicalResult = await db.execute(sql`
982+
SELECT model AS name, 'chat' AS model_type FROM completions WHERE deleted = false
983+
UNION
984+
SELECT model AS name, 'embedding' AS model_type FROM embeddings WHERE deleted = false
985+
`);
986+
const historicalRows = historicalResult as unknown as {
987+
name: string;
988+
model_type: "chat" | "embedding";
989+
}[];
990+
991+
// Filter out active models and respect modelType filter
992+
const archivedMap = new Map<
993+
string,
994+
"chat" | "embedding"
995+
>();
996+
for (const row of historicalRows) {
997+
if (activeSet.has(row.name)) continue;
998+
if (modelType && row.model_type !== modelType) continue;
999+
// If a name appears in both tables, prefer 'chat' (it's the more common case)
1000+
if (!archivedMap.has(row.name)) {
1001+
archivedMap.set(row.name, row.model_type);
1002+
}
1003+
}
1004+
1005+
const archived = Array.from(archivedMap.entries())
1006+
.sort(([a], [b]) => a.localeCompare(b))
1007+
.map(([systemName, mt]) => ({ systemName, modelType: mt }));
1008+
1009+
return { active, archived };
9711010
}
9721011

9731012
/**

frontend/src/i18n/locales/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@
331331
"pages.models.registry.WeightConfiguration": "Weight Configuration",
332332
"pages.models.registry.History": "History",
333333
"pages.models.registry.ViewHistory": "View request history",
334+
"pages.models.registry.Archived": "Archived",
335+
"pages.models.registry.ArchivedTooltip": "This model has been deleted but has historical requests",
334336
"routes.models.Title": "Models",
335337
"routes.models.Description": "Configure model providers and global models.",
336338
"routes.models.nav.Providers": "Model Providers",

frontend/src/i18n/locales/zh-CN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@
332332
"pages.models.registry.WeightConfiguration": "权重配置",
333333
"pages.models.registry.History": "历史",
334334
"pages.models.registry.ViewHistory": "查看请求历史",
335+
"pages.models.registry.Archived": "已归档",
336+
"pages.models.registry.ArchivedTooltip": "该模型已删除但存在历史请求记录",
335337
"routes.models.Title": "模型",
336338
"routes.models.Description": "配置模型供应商和全局模型。",
337339
"routes.models.nav.Providers": "模型供应商",

frontend/src/pages/models/models-registry-table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,15 @@ function ModelsBySystemNameTable({ systemName }: { systemName: string }) {
120120
const { data: models = [], isLoading } = useQuery({
121121
queryKey: ['models', 'by-system-name', systemName],
122122
queryFn: async () => {
123-
const { data, error } = await api.admin.models['by-system-name'][systemName].get()
123+
const { data, error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].get()
124124
if (error) throw error
125125
return data as ModelWithProvider[]
126126
},
127127
})
128128

129129
const updateWeightsMutation = useMutation({
130130
mutationFn: async (weights: { modelId: number; weight: number }[]) => {
131-
const { error } = await api.admin.models['by-system-name'][systemName].weights.put({
131+
const { error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].weights.put({
132132
weights,
133133
})
134134
if (error) throw error

frontend/src/pages/settings/models-settings-page.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useState } from 'react'
22
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
33
import { useNavigate } from '@tanstack/react-router'
4-
import { CpuIcon, HistoryIcon } from 'lucide-react'
4+
import { ArchiveIcon, CpuIcon, HistoryIcon } from 'lucide-react'
55
import { useTranslation } from 'react-i18next'
66
import { toast } from 'sonner'
77

@@ -25,8 +25,19 @@ interface ModelWithProvider {
2525
}
2626
}
2727

28-
export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) {
28+
interface ArchivedModel {
29+
systemName: string
30+
modelType: 'chat' | 'embedding'
31+
}
32+
33+
interface ModelsSettingsPageProps {
34+
activeSystemNames: string[]
35+
archivedSystemNames: ArchivedModel[]
36+
}
37+
38+
export function ModelsSettingsPage({ activeSystemNames, archivedSystemNames }: ModelsSettingsPageProps) {
2939
const { t } = useTranslation()
40+
const hasAny = activeSystemNames.length > 0 || archivedSystemNames.length > 0
3041

3142
return (
3243
<div className="space-y-8">
@@ -36,7 +47,7 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) {
3647
<CardDescription className="text-sm">{t('pages.models.registry.Description')}</CardDescription>
3748
</CardHeader>
3849
<CardContent>
39-
{systemNames.length > 0 ? (
50+
{hasAny ? (
4051
<Table>
4152
<TableHeader>
4253
<TableRow>
@@ -47,9 +58,12 @@ export function ModelsSettingsPage({ systemNames }: { systemNames: string[] }) {
4758
</TableRow>
4859
</TableHeader>
4960
<TableBody>
50-
{systemNames.map((systemName) => (
61+
{activeSystemNames.map((systemName) => (
5162
<ModelRow key={systemName} systemName={systemName} />
5263
))}
64+
{archivedSystemNames.map((item) => (
65+
<ArchivedModelRow key={item.systemName} systemName={item.systemName} modelType={item.modelType} />
66+
))}
5367
</TableBody>
5468
</Table>
5569
) : (
@@ -69,7 +83,7 @@ function ModelRow({ systemName }: { systemName: string }) {
6983
const { data: models = [], isLoading } = useQuery({
7084
queryKey: ['models', 'by-system-name', systemName],
7185
queryFn: async () => {
72-
const { data, error } = await api.admin.models['by-system-name'][systemName].get()
86+
const { data, error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].get()
7387
if (error) throw error
7488
return data as ModelWithProvider[]
7589
},
@@ -157,6 +171,50 @@ function ModelRow({ systemName }: { systemName: string }) {
157171
)
158172
}
159173

174+
function ArchivedModelRow({ systemName, modelType }: { systemName: string; modelType: 'chat' | 'embedding' }) {
175+
const { t } = useTranslation()
176+
const navigate = useNavigate()
177+
178+
const handleHistoryClick = (e: React.MouseEvent) => {
179+
e.stopPropagation()
180+
if (modelType === 'embedding') {
181+
navigate({ to: '/embeddings', search: { model: systemName } })
182+
} else {
183+
navigate({ to: '/requests', search: { model: systemName } })
184+
}
185+
}
186+
187+
return (
188+
<TableRow className="opacity-50">
189+
<TableCell>
190+
<div className="flex items-center gap-3">
191+
<ArchiveIcon className="text-muted-foreground/50 size-5" />
192+
<span className="text-muted-foreground font-mono text-sm font-medium line-through">{systemName}</span>
193+
</div>
194+
</TableCell>
195+
<TableCell>
196+
<span className="text-muted-foreground text-sm">-</span>
197+
</TableCell>
198+
<TableCell>
199+
<span className="text-muted-foreground text-sm italic" title={t('pages.models.registry.ArchivedTooltip')}>
200+
{t('pages.models.registry.Archived')}
201+
</span>
202+
</TableCell>
203+
<TableCell className="text-center">
204+
<Button
205+
variant="ghost"
206+
size="sm"
207+
className="size-8 p-0"
208+
onClick={handleHistoryClick}
209+
title={t('pages.models.registry.ViewHistory')}
210+
>
211+
<HistoryIcon className="size-4" />
212+
</Button>
213+
</TableCell>
214+
</TableRow>
215+
)
216+
}
217+
160218
interface LoadBalancingDialogProps {
161219
open: boolean
162220
onOpenChange: (open: boolean) => void
@@ -182,7 +240,7 @@ function LoadBalancingDialog({ open, onOpenChange, systemName, models }: LoadBal
182240

183241
const updateWeightsMutation = useMutation({
184242
mutationFn: async (weights: { modelId: number; weight: number }[]) => {
185-
const { error } = await api.admin.models['by-system-name'][systemName].weights.put({
243+
const { error } = await api.admin.models['by-system-name'][encodeURIComponent(systemName)].weights.put({
186244
weights,
187245
})
188246
if (error) throw error

frontend/src/routes/models/registry.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ export const Route = createFileRoute('/models/registry')({
2727
function RouteComponent() {
2828
const { data } = useSuspenseQuery(systemNamesQueryOptions())
2929

30-
return <ModelsSettingsPage systemNames={data} />
30+
return <ModelsSettingsPage activeSystemNames={data.active} archivedSystemNames={data.archived} />
3131
}

0 commit comments

Comments
 (0)