From 051c0a9bff92f58dd358e5dd4e3524afc7da5f66 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 15 Sep 2026 19:44:19 +0200 Subject: [PATCH] ui : keep models of every enabled backend selectable Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../ModelsSelectorDropdown.svelte | 2 +- .../ModelsSelector/ModelsSelectorList.svelte | 67 +++++++++---------- .../ModelsSelector/ModelsSelectorSheet.svelte | 1 - .../lib/components/app/navigation/utils.ts | 15 ++++- .../lib/hooks/use-models-selector.svelte.ts | 20 +++--- .../stories/ModelsSelector.stories.svelte | 3 + 6 files changed, 57 insertions(+), 51 deletions(-) diff --git a/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDropdown.svelte b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDropdown.svelte index e18ef0dc6b..30420524f4 100644 --- a/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDropdown.svelte +++ b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDropdown.svelte @@ -91,6 +91,7 @@ for (const item of ms.groupedFilteredOptions.favorites) order.push(item.option.id); for (const item of ms.groupedFilteredOptions.loaded) order.push(item.option.id); + for (const item of ms.groupedFilteredOptions.external) order.push(item.option.id); for (const group of ms.groupedFilteredOptions.available) { for (const item of group.items) order.push(item.option.id); } @@ -342,7 +343,6 @@ void; onInfoClick: (modelName: string) => void; @@ -21,7 +19,6 @@ let { activeId, currentModel, - flat = false, groups, onInfoClick, onSelect, @@ -64,44 +61,44 @@ /> {/snippet} -{#if flat} - {#each [...groups.favorites, ...groups.available.flatMap((group) => group.items)] as item (item.option.id)} +{#if groups.favorites.length > 0} +

Favorite models

+ + {#each groups.favorites as item (`fav-${item.option.id}`)} {@render render(item, true)} {/each} -{:else} - {#if groups.favorites.length > 0} -

Favorite models

+{/if} - {#each groups.favorites as item (`fav-${item.option.id}`)} +{#if getDownloadEntries.length > 0} +

Download in progress

+ + {#each getDownloadEntries as entry (entry.repoWithTag)} + + {/each} +{/if} + +{#if groups.loaded.length > 0} +

Loaded models

+ + {#each groups.loaded as item (`loaded-${item.option.id}`)} + {@render render(item, false)} + {/each} +{/if} + +{#if groups.available.length > 0} +

Downloaded models

+ + {#each groups.available as group (group.orgName)} + {#each group.items as item (item.option.id)} {@render render(item, true)} {/each} - {/if} + {/each} +{/if} - {#if getDownloadEntries.length > 0} -

Download in progress

- - {#each getDownloadEntries as entry (entry.repoWithTag)} - - {/each} - {/if} - - {#if groups.loaded.length > 0} -

Loaded models

- - {#each groups.loaded as item (`loaded-${item.option.id}`)} - {@render render(item, false)} - {/each} - {/if} - - {#if groups.available.length > 0} -

Downloaded models

- - {#each groups.available as group (group.orgName)} - {#each group.items as item (item.option.id)} - {@render render(item, true)} - {/each} - {/each} - {/if} +{#if groups.external.length > 0} + {#each groups.external as item (`ext-${item.option.id}`)} + {@render render(item, true)} + {/each} {/if} , - isModelLoaded: (model: string) => boolean + isModelLoaded: (model: string) => boolean, + isLocalModel: (option: ModelOption) => boolean = () => true ): GroupedModelOptions { // Loaded models const loaded: ModelItem[] = []; @@ -79,6 +82,8 @@ export function groupModelOptions( // Available models grouped by org (excluding loaded and favorites) const available: OrgGroup[] = []; + // models from other backends have no load state, so they stay flat + const external: ModelItem[] = []; const orgGroups = new SvelteMap(); for (let i = 0; i < filteredOptions.length; i++) { @@ -86,6 +91,12 @@ export function groupModelOptions( if (loadedModelIds.has(option.model) || favoriteIds.has(option.model)) continue; + if (!isLocalModel(option)) { + external.push({ flatIndex: i, option }); + + continue; + } + const key = option.parsedId?.orgName ?? ''; if (!orgGroups.has(key)) orgGroups.set(key, []); @@ -97,5 +108,5 @@ export function groupModelOptions( available.push({ items, orgName: orgName || null }); } - return { available, favorites, loaded }; + return { available, external, favorites, loaded }; } diff --git a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts index d6c427fa53..c2cf96b2a8 100644 --- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts +++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts @@ -1,5 +1,5 @@ import { filterModelOptions, groupModelOptions } from '$lib/components/app/navigation/utils'; -import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants'; +import { CHAT_INPUT_FOCUS_SELECTOR, LOCAL_BACKEND_ID } from '$lib/constants'; import { backendsModelsStore, backendsStore, modelsStore, serverStore } from '$lib/stores'; import type { Backend } from '$lib/types'; import type { ModelOption } from '$lib/types/models'; @@ -21,7 +21,6 @@ export interface UseModelsSelectorReturn { readonly updating: boolean; readonly activeId: string | null; readonly activeBackendId: string; - readonly isLocalBackend: boolean; readonly backends: Backend[]; readonly isMultiModel: boolean; readonly isRouter: boolean; @@ -53,12 +52,10 @@ export interface UseModelsSelectorReturn { */ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn { const activeBackendId = $derived(backendsStore.active.id); - const isLocalBackend = $derived(backendsStore.active.protocol === 'llama.cpp'); - // the backend switcher tabs scope the list to one backend's models + // every enabled backend's models are selectable; the switcher only picks + // the backend new requests target const options = $derived( modelsStore.models.filter((option) => { - if (option.backendId !== activeBackendId) return false; - const modelProps = modelsStore.props.getModelProps(option.model); return modelProps?.ui !== false; @@ -95,8 +92,11 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele const filteredOptions = $derived(filterModelOptions(options, searchTerm)); const groupedFilteredOptions = $derived( - groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) => - modelsStore.isModelLoaded(m) + groupModelOptions( + filteredOptions, + modelsStore.favoriteModelIds, + (m) => modelsStore.isModelLoaded(m), + (option) => option.backendId === LOCAL_BACKEND_ID ) ); @@ -277,10 +277,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele return isLoadingModel; }, - get isLocalBackend() { - return isLocalBackend; - }, - get isMultiModel() { return isMultiModel; }, diff --git a/tools/ui/tests/stories/ModelsSelector.stories.svelte b/tools/ui/tests/stories/ModelsSelector.stories.svelte index f0c5733cf8..bf7c947f98 100644 --- a/tools/ui/tests/stories/ModelsSelector.stories.svelte +++ b/tools/ui/tests/stories/ModelsSelector.stories.svelte @@ -103,6 +103,7 @@ orgName: 'intel' } ], + external: [], favorites: favoriteModels, loaded: loadedModels }; @@ -138,6 +139,7 @@ currentModel={null} groups={{ available: [], + external: [], favorites: [], loaded: [loadedModels[0]] }} @@ -154,6 +156,7 @@ currentModel={null} groups={{ available: [], + external: [], favorites: favoriteModels, loaded: [] }}