From a4d24cbb71be005757b6f3e86146ee178fee5ff5 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 22 Sep 2026 00:58:56 +0200 Subject: [PATCH] ui : fix duplicate model rows and the reloaded context size Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../ui/src/lib/stores/models/index.svelte.ts | 29 +++++++++++-------- tools/ui/src/lib/utils/backend.ts | 5 +++- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts index 352bb00899..92059ddf2b 100644 --- a/tools/ui/src/lib/stores/models/index.svelte.ts +++ b/tools/ui/src/lib/stores/models/index.svelte.ts @@ -17,6 +17,7 @@ import { conversationsStore } from '$lib/stores/conversations/index.svelte'; import { type ModelPropsHost, ModelPropsManager } from '$lib/stores/models/props.svelte'; import { type ModelStatusHost, ModelStatusManager } from '$lib/stores/models/status.svelte'; import { serverStore } from '$lib/stores/server.svelte'; +import { readModelContextLength } from '$lib/utils/backend'; import { getConversationModel } from '$lib/utils/conversation-utils'; import { backendIdFromModelId, qualifyModelId, rawModelId } from '$lib/utils/model-option-id'; import { SvelteSet } from 'svelte/reactivity'; @@ -96,28 +97,29 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { get models(): ModelOption[] { const activeBackendId = backendsStore.active.id; const merged: ModelOption[] = []; + const seen = new SvelteSet(); + const push = (option: ModelOption, backendId: string) => { + const id = qualifyModelId(backendId, rawModelId(option.id)); + + // a backend can be listed twice while a switch is in flight: the rows + // of the previous backend are still in activeModels + if (seen.has(id)) return; + + seen.add(id); + merged.push({ ...option, backendId, id }); + }; for (const option of this.activeModels) { // keep the backend an option was built for: rows from the previous // backend must not be relabelled while a switch is in flight - const backendId = option.backendId ?? activeBackendId; - - merged.push({ - ...option, - backendId, - id: qualifyModelId(backendId, rawModelId(option.id)) - }); + push(option, option.backendId ?? activeBackendId); } for (const backend of backendsStore.enabled) { if (backend.id === activeBackendId) continue; for (const option of backendsModelsStore.get(backend.id).models) { - merged.push({ - ...option, - backendId: option.backendId ?? backend.id, - id: qualifyModelId(option.backendId ?? backend.id, rawModelId(option.id)) - }); + push(option, option.backendId ?? backend.id); } } @@ -512,6 +514,9 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { capabilities: rawCapabilities.filter((value: unknown): value is string => Boolean(value) ), + // external backends report the context in their listing, so the + // gauge keeps working when the list is rebuilt on reload + contextLength: readModelContextLength(item), description: details?.description, details: details?.details, id: item.id, diff --git a/tools/ui/src/lib/utils/backend.ts b/tools/ui/src/lib/utils/backend.ts index 21555a7e8a..29c68558f7 100644 --- a/tools/ui/src/lib/utils/backend.ts +++ b/tools/ui/src/lib/utils/backend.ts @@ -227,7 +227,10 @@ function parseOptionalPath(raw: unknown): string | undefined { * different field names, and OpenRouter nests the authoritative value under * `top_provider`, so try the flat fields first and the nested one after. */ -export function readModelContextLength(entry: Record): number | undefined { +export function readModelContextLength(value: unknown): number | undefined { + if (!value || typeof value !== 'object') return undefined; + + const entry = value as Record; const flat = readContextField(entry); if (flat !== undefined) return flat;