ui : keep models of every enabled backend selectable

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 19:22:47 +02:00
parent d83c9f54c3
commit 051c0a9bff
6 changed files with 57 additions and 51 deletions
@@ -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 @@
<ModelsSelectorList
activeId={ms.activeId}
{currentModel}
flat={!ms.isLocalBackend}
groups={ms.groupedFilteredOptions}
onInfoClick={ms.handleInfoClick}
onSelect={ms.handleSelect}
@@ -10,8 +10,6 @@
groups: GroupedModelOptions;
currentModel: string | null;
activeId: string | null;
/** Render one flat list without section headers (external backends). */
flat?: boolean;
sectionHeaderClass?: string;
onSelect: (modelId: string) => 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}
<p class={sectionHeaderClass}>Favorite models</p>
{#each groups.favorites as item (`fav-${item.option.id}`)}
{@render render(item, true)}
{/each}
{:else}
{#if groups.favorites.length > 0}
<p class={sectionHeaderClass}>Favorite models</p>
{/if}
{#each groups.favorites as item (`fav-${item.option.id}`)}
{#if getDownloadEntries.length > 0}
<p class={sectionHeaderClass}>Download in progress</p>
{#each getDownloadEntries as entry (entry.repoWithTag)}
<ModelsSelectorDownloadItem {entry} onRequestCancel={requestCancel} />
{/each}
{/if}
{#if groups.loaded.length > 0}
<p class={sectionHeaderClass}>Loaded models</p>
{#each groups.loaded as item (`loaded-${item.option.id}`)}
{@render render(item, false)}
{/each}
{/if}
{#if groups.available.length > 0}
<h2 class={sectionHeaderClass}>Downloaded models</h2>
{#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}
<p class={sectionHeaderClass}>Download in progress</p>
{#each getDownloadEntries as entry (entry.repoWithTag)}
<ModelsSelectorDownloadItem {entry} onRequestCancel={requestCancel} />
{/each}
{/if}
{#if groups.loaded.length > 0}
<p class={sectionHeaderClass}>Loaded models</p>
{#each groups.loaded as item (`loaded-${item.option.id}`)}
{@render render(item, false)}
{/each}
{/if}
{#if groups.available.length > 0}
<h2 class={sectionHeaderClass}>Downloaded models</h2>
{#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}
<DialogConfirmDownload
@@ -188,7 +188,6 @@
<ModelsSelectorList
activeId={ms.activeId}
{currentModel}
flat={!ms.isLocalBackend}
groups={ms.groupedFilteredOptions}
onInfoClick={ms.handleInfoClick}
onSelect={ms.handleSelect}
@@ -16,6 +16,8 @@ export interface GroupedModelOptions {
loaded: ModelItem[];
favorites: ModelItem[];
available: OrgGroup[];
/** Models served by other backends; rendered flat, without categories. */
external: ModelItem[];
}
function matchesModality(option: ModelOption, term: string): boolean {
@@ -53,7 +55,8 @@ export function filterModelOptions(options: ModelOption[], searchTerm: string):
export function groupModelOptions(
filteredOptions: ModelOption[],
favoriteIds: Set<string>,
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<string, ModelItem[]>();
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 };
}
@@ -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;
},
@@ -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: []
}}