From 3044a73ce5844f3cb17cafebc3159ab43552a284 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 10:23:51 +0200 Subject: [PATCH] ui : filter models by capability and by draft sidecar The manager could narrow by provider, context and modality only. It now also asks for tool use or for reasoning, which the Hub chat template answers for a local model, and for models that have a draft sidecar to speculate with. The provider menu says how many repos each provider contributes and steps aside when there is only one to choose between. The Hub details cache becomes a SvelteMap so a filter re-runs when a template arrives after the row mounts, and the discover entry's effect consumes its flag untracked instead of invalidating itself. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../app/dialogs/DialogManageModels.svelte | 8 +- .../models/ModelsManager/ModelsManager.svelte | 48 +++++- .../ModelsManager/ModelsManagerFilters.svelte | 141 +++++++++++++----- .../ModelsManagerModelsTable.svelte | 42 +++--- .../app/models/ModelsManager/utils.ts | 20 +++ .../src/lib/services/huggingface.service.ts | 3 +- 6 files changed, 195 insertions(+), 67 deletions(-) diff --git a/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte b/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte index c9603f1231..13af001d9d 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogManageModels.svelte @@ -58,9 +58,11 @@ $effect(() => { if (!uiStore.discoverModelsOpen) return; - uiStore.discoverModelsOpen = false; - view = 'discover'; - handleOpenChange(true); + untrack(() => { + uiStore.discoverModelsOpen = false; + view = 'discover'; + handleOpenChange(true); + }); }); function handleOpenChange(value: boolean) { diff --git a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManager.svelte b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManager.svelte index 7e8f5714cb..cb4e4f09ba 100644 --- a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManager.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManager.svelte @@ -8,12 +8,15 @@ loadOverrides, type ModalityKey, modelContextLength, + modelDraftsFor, type ModelOverride, type ModelQuantGroup, type ModelsTableGroup, + modelSupports, saveOverrides } from './utils'; import { LOCAL_BACKEND_ID } from '$lib/constants'; + import { ModelCapability } from '$lib/enums'; import { backendsStore, conversationsStore, modelsStore, uiStore } from '$lib/stores'; import type { ModelOption } from '$lib/types/models'; import { getBackend } from '$lib/utils/api-base'; @@ -35,6 +38,8 @@ let providerFilter = $state([]); let contextLimit = $state(0); let modalityFilter = $state([]); + let capabilityFilter = $state([]); + let draftFilter = $state(false); let selectedId = $state(null); let overrides = $state>(loadOverrides()); @@ -42,18 +47,28 @@ let isFavorite = $derived((option: ModelOption) => modelsStore.favoriteModelIds.has(option.model) ); - // the rail counts follow the active view and filter, so it always says how many - // models each provider contributes to what the table is showing - let visible = $derived.by(() => { + // every filter but the provider one, so a provider count does not fall to zero + // the moment that provider is the one being looked at + let matching = $derived.by(() => { const term = filter.trim().toLowerCase(); return allModels.filter((option) => { if (term && !`${option.name} ${option.model}`.toLowerCase().includes(term)) return false; - if (providerFilter.length > 0) { - const backendId = option.backendId ?? LOCAL_BACKEND_ID; + // every capability asked for has to be there: tools and reasoning are + // features a model either has or does not, unlike modalities + if ( + capabilityFilter.length > 0 && + !capabilityFilter.every((capability) => modelSupports(option, capability)) + ) { + return false; + } - if (!providerFilter.includes(backendId)) return false; + if ( + draftFilter && + modelDraftsFor(option, overrides[option.id]?.load?.speculativeDecoding).length === 0 + ) { + return false; } // a model whose modalities are unknown cannot be shown to match @@ -64,6 +79,24 @@ return contextLimit === 0 || modelContextLength(option) >= contextLimit; }); }); + let visible = $derived.by(() => + providerFilter.length === 0 + ? matching + : matching.filter((option) => providerFilter.includes(option.backendId ?? LOCAL_BACKEND_ID)) + ); + // the rail counts follow the active view and filter, so it always says how many + // repos each provider contributes to what the table is showing + let providerCounts = $derived.by(() => { + const counts: Record = {}; + + for (const entry of groupModelQuants(matching)) { + const backendId = entry.base.backendId ?? LOCAL_BACKEND_ID; + + counts[backendId] = (counts[backendId] ?? 0) + 1; + } + + return counts; + }); // recently used models lead their section, the rest keep the server's order const rank = new SvelteMap(); @@ -356,7 +389,9 @@
(selectedId = option.id)} onUseAsDraft={useAsDraft} {overrides} + {providerCounts} {selectedId} toolbarEnd={toolbarEndRegion} /> diff --git a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerFilters.svelte b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerFilters.svelte index 956acf20af..055de72200 100644 --- a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerFilters.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerFilters.svelte @@ -1,29 +1,40 @@