ui : show every provider's models in the manager

The table grows a section per enabled provider, each with its own mark,
error and loading state, and a provider filter with live repo counts.
Rows gain their capability gates back, the draft column with its
use-as-draft action, and the quant badge names the provider on its rows.

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 17:12:26 +02:00
parent 15860276e6
commit 3897f113a0
6 changed files with 402 additions and 41 deletions
@@ -1,9 +1,13 @@
<script lang="ts">
import ModelsDiscoverAvatar from './discover/ModelsDiscoverAvatar.svelte';
import { HF_BASE_MODEL_TAG_REGEX } from '$lib/constants';
import { BackendIcon } from '$lib/components/app/backends';
import { Logo } from '$lib/components/app/misc';
import { HF_BASE_MODEL_TAG_REGEX, LOCAL_BACKEND_ID, MODEL_ICON } from '$lib/constants';
import { HuggingFaceService, ModelsService } from '$lib/services';
import type { ModelOption } from '$lib/types/models';
import { orgOf } from '$lib/utils';
import { getBackend } from '$lib/utils/api-base';
import { getBackendCapabilities } from '$lib/utils/backend';
import type { Snippet } from 'svelte';
interface Props {
@@ -39,6 +43,12 @@
let parsedId = $derived(ModelsService.parseModelId(option.model));
let orgName = $derived(parsedId.orgName);
// a llama-compat model whose id carries no `org/name` is not a Hugging Face repo,
// so the provider's own mark identifies it better than an initial
let isLlamaCompat = $derived(getBackendCapabilities(getBackend(option.backendId)).props);
let useProviderIcon = $derived(isLlamaCompat && !orgName);
// the bundled server has no favicon to resolve, its mark is the llama.cpp logo
let isLocal = $derived(getBackend(option.backendId)?.id === LOCAL_BACKEND_ID);
let tagBaseModel = $derived(
(option.tags ?? [])
.find((t) => HF_BASE_MODEL_TAG_REGEX.test(t))
@@ -80,6 +90,12 @@
if (!isNearViewport || !showBaseModelAvatar || !orgName || tagBaseModel) return;
// external provider ids (`~openai/gpt-...`, `deepseek/deepseek-chat`) are
// not HF repos; their org is already the provider slug
const backend = getBackend(option.backendId);
if (backend && !getBackendCapabilities(backend).props) return;
let cancelled = false;
void HuggingFaceService.getBaseModel(option.model)
@@ -95,7 +111,19 @@
});
</script>
{#if orgName}
{#if useProviderIcon}
<span class={['inline-flex shrink-0', className]}>
<BackendIcon backend={getBackend(option.backendId)} class={size}>
{#snippet fallback()}
{#if isLocal}
<Logo class={size} style="--size: 100%" />
{:else}
<MODEL_ICON class={size} />
{/if}
{/snippet}
</BackendIcon>
</span>
{:else if orgName}
<span bind:this={avatarEl} class={['inline-flex shrink-0', className]}>
<ModelsDiscoverAvatar
class="mt-0"
@@ -1,11 +1,15 @@
<script lang="ts">
import { CircleAlert, Eject, Loader2, Power, RotateCw } from '@lucide/svelte';
import { ActionIcon } from '$lib/components/app';
import { BackendIcon } from '$lib/components/app/backends';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { modelsStore } from '$lib/stores';
import { backendsModelsStore, modelsStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { getBackend } from '$lib/utils/api-base';
interface Props {
/** Backend can load and unload models, llama-compat servers only. */
canLoad: boolean;
class?: string;
isFailed?: boolean;
isLoaded: boolean;
@@ -16,9 +20,14 @@
revealOnHover?: boolean;
/** Renders the state alone, for a caller that moves load and unload elsewhere. */
showAction?: boolean;
/** Non-loadable rows show the provider mark, which identifies them in a flat list. */
showBackendMark?: boolean;
/** Table rows mark a remote provider, which this UI cannot load or unload. */
showRemoteMark?: boolean;
}
let {
canLoad,
class: className = '',
isFailed = false,
isLoaded,
@@ -26,12 +35,35 @@
isSleeping = false,
option,
revealOnHover = true,
showAction = true
showAction = true,
showBackendMark = false,
showRemoteMark = false
}: Props = $props();
let backendName = $derived(getBackend(option.backendId)?.name ?? 'Remote provider');
/** The provider's listing failed, so nothing it serves is selectable right now. */
let isBackendFailed = $derived(
Boolean(option.backendId && backendsModelsStore.get(option.backendId).error)
);
</script>
<div class={['flex w-5 shrink-0 items-center justify-center', className]}>
{#if isLoading}
{#if !canLoad}
{#if showBackendMark}
<BackendIcon backend={getBackend(option.backendId)} class="h-3.5 w-3.5" />
{:else if showRemoteMark}
<span
class="flex items-center gap-1 {isBackendFailed ? 'opacity-50 grayscale' : ''}"
title={isBackendFailed ? `${backendName} is unavailable` : backendName}
>
<BackendIcon backend={getBackend(option.backendId)} class="h-3.5 w-3.5" />
{#if isBackendFailed}
<CircleAlert class="h-3 w-3 text-destructive" />
{/if}
</span>
{/if}
{:else if isLoading}
<Loader2 class="{ICON_CLASS_DEFAULT} animate-spin text-muted-foreground" />
{:else}
<!-- the state dot is what the row shows at rest; the action takes its place on hover -->
@@ -2,18 +2,25 @@
import ModelsManagerModelsTable from './ModelsManagerModelsTable.svelte';
import {
groupModelQuants,
loadOverrides,
type ModalityKey,
modelContextLength,
modelDraftsFor,
type ModelOverride,
type ModelQuantGroup,
type ModelsTableGroup,
modelSupports
modelSupports,
saveOverrides
} from './utils';
import { LOCAL_BACKEND_ID } from '$lib/constants';
import { ModelCapability } from '$lib/enums';
import { modelsStore, uiStore } from '$lib/stores';
import { backendsStore, modelsStore, uiStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { getBackend } from '$lib/utils/api-base';
import { getBackendCapabilities } from '$lib/utils/backend';
import { type Snippet } from 'svelte';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
interface Props {
class?: string;
@@ -24,15 +31,20 @@
let { class: className, toolbarEnd }: Props = $props();
let filter = $state('');
let providerFilter = $state<string[]>([]);
let contextLimit = $state(0);
let modalityFilter = $state<ModalityKey[]>([]);
let capabilityFilter = $state<ModelCapability[]>([]);
let draftFilter = $state(false);
let selectedId = $state<string | null>(null);
let overrides = $state<Record<string, ModelOverride>>(loadOverrides());
let allModels = $derived(modelsStore.models);
let isFavorite = $derived((option: ModelOption) =>
modelsStore.favoriteModelIds.has(option.model)
);
// 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();
@@ -48,6 +60,13 @@
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
if (modalityFilter.length > 0 && !modalityFilter.some((key) => option.modalities?.[key])) {
return false;
@@ -56,6 +75,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<string, number> = {};
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<string, number>();
@@ -68,12 +105,16 @@
rank.size === 0 ? list : [...list].sort((a, b) => rankOf(a) - rankOf(b));
// one entry per repo, so a model with several quants is a single table row;
// loaded models lead the table, then favorites, then the local block
let entries = $derived(byRecency(groupModelQuants(matching)));
// loaded models lead the table, then favorites, then one block per provider,
// and an entry lands in the first group that claims it
let entries = $derived(byRecency(groupModelQuants(visible)));
let groups = $derived.by(() => {
// A loaded quant is a model of its own: it moves to the loaded section, and
// the quants of its repo that are not loaded stay behind as that repo.
const isLoaded = (option: ModelOption) => modelsStore.isModelLoaded(option.model);
// the quants of its repo that are not loaded stay behind as that repo. Only
// llama-compat servers report a load state.
const isLoaded = (option: ModelOption) =>
getBackendCapabilities(getBackend(option.backendId)).loadUnload &&
modelsStore.isModelLoaded(option.model);
const loaded: ModelQuantGroup[] = [];
const rest: ModelQuantGroup[] = [];
@@ -100,7 +141,18 @@
(entry) => !claimed.has(entry.key) && entry.quants.some((q) => modelsStore.isHidden(q.id))
);
const hiddenKeys = new SvelteSet(hidden.map((entry) => entry.key));
const local = rest.filter((entry) => !claimed.has(entry.key) && !hiddenKeys.has(entry.key));
const byBackend = new SvelteMap<string, ModelQuantGroup[]>();
for (const entry of rest) {
if (claimed.has(entry.key) || hiddenKeys.has(entry.key)) continue;
const backendId = entry.base.backendId ?? LOCAL_BACKEND_ID;
if (!byBackend.has(backendId)) byBackend.set(backendId, []);
byBackend.get(backendId)!.push(entry);
}
const ordered: ModelsTableGroup[] = [];
if (loaded.length) {
@@ -125,17 +177,36 @@
});
}
if (local.length) {
const localItems = byBackend.get(LOCAL_BACKEND_ID);
if (localItems?.length) {
ordered.push({
backendId: LOCAL_BACKEND_ID,
isLocal: true,
items: local,
items: localItems,
key: LOCAL_BACKEND_ID,
kind: 'local',
label: 'Local models'
});
}
for (const backend of backendsStore.enabled) {
if (backend.id === LOCAL_BACKEND_ID) continue;
const items = byBackend.get(backend.id);
if (items?.length) {
ordered.push({
backendId: backend.id,
isLocal: false,
items,
key: backend.id,
kind: 'provider',
label: backend.name
});
}
}
if (hidden.length) {
ordered.push({
backendId: null,
@@ -162,6 +233,24 @@
uiStore.manageModelFocus = null;
});
/** Point the selected model's load settings at another model as its draft. */
function useAsDraft(draft: ModelOption, targetId: string): void {
const target = modelsStore.models.find((option) => option.id === targetId);
if (!target) return;
saveOverride(target, {
...overrides[target.id],
load: { ...overrides[target.id]?.load, speculativeDecoding: draft.id }
});
}
function saveOverride(option: ModelOption, override: ModelOverride): void {
overrides = { ...overrides, [option.id]: override };
saveOverrides(overrides);
toast.success(`Saved settings for ${option.name}`);
}
</script>
<div class={['flex min-h-0 flex-1', className]}>
@@ -169,11 +258,16 @@
<ModelsManagerModelsTable
bind:capabilities={capabilityFilter}
bind:contextLimit
bind:draft={draftFilter}
bind:filter
bind:modalities={modalityFilter}
bind:providers={providerFilter}
{groups}
{isFavorite}
onSelect={(option) => (selectedId = option.id)}
onUseAsDraft={useAsDraft}
{overrides}
{providerCounts}
{selectedId}
{toolbarEnd}
/>
@@ -1,25 +1,40 @@
<script lang="ts">
import type { ModalityKey } from './utils';
import { Image, Lightbulb, Mic, Video, Wrench } from '@lucide/svelte';
import { ScrollCarousel } from '$lib/components/app';
import { Check, ChevronDown, Image, Lightbulb, Mic, Server, Video, Wrench } from '@lucide/svelte';
import { Logo, ScrollCarousel } from '$lib/components/app';
import { BackendIcon } from '$lib/components/app/backends';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Select from '$lib/components/ui/select';
import { Toggle } from '$lib/components/ui/toggle';
import * as ToggleGroup from '$lib/components/ui/toggle-group';
import { FILTER_TRIGGER_CLASS } from '$lib/constants';
import { FILTER_TRIGGER_CLASS, LOCAL_BACKEND_ID } from '$lib/constants';
import { ModelCapability } from '$lib/enums';
import type { Backend } from '$lib/types/backend';
interface Props {
backends: Backend[];
/** Capabilities a model must have every one of. */
capabilities?: ModelCapability[];
/** Smallest context a model must support; 0 keeps every model. */
contextLimit?: number;
/** Keep only models that have a draft sidecar to speculate with. */
draft?: boolean;
/** Modalities a model must support at least one of. */
modalities?: ModalityKey[];
/** Backend ids to keep; empty keeps every provider. */
providers?: string[];
/** Repos each provider contributes to the current search, shown in the menu. */
providerCounts?: Record<string, number>;
}
let {
backends,
capabilities = $bindable<ModelCapability[]>([]),
contextLimit = $bindable(0),
modalities = $bindable<ModalityKey[]>([])
draft = $bindable(false),
modalities = $bindable<ModalityKey[]>([]),
providerCounts = {},
providers = $bindable<string[]>([])
}: Props = $props();
const CONTEXT_STEPS: { label: string; value: number }[] = [
@@ -58,12 +73,83 @@
const TOGGLE_ITEM_CLASS =
'bg-muted! border-border/30! shadow-none! dark:border-border/20! data-[state=on]:bg-muted-foreground/15! data-[state=on]:text-foreground! dark:data-[state=on]:bg-muted-foreground/25!';
// none selected means every provider, so the label names the selection
let providerLabel = $derived(
providers.length === 0
? 'All providers'
: providers.length === 1
? (backends.find((backend) => backend.id === providers[0])?.name ?? '1 provider')
: `${providers.length} providers`
);
let contextLabel = $derived(
CONTEXT_STEPS.find((step) => step.value === contextLimit)?.label ?? CONTEXT_STEPS[0].label
);
function toggleProvider(id: string, checked: boolean | 'indeterminate'): void {
providers = checked === true ? [...providers, id] : providers.filter((entry) => entry !== id);
}
</script>
{#snippet providerMark(backend: Backend)}
{#if backend.id === LOCAL_BACKEND_ID}
<BackendIcon {backend} class="h-3.5 w-3.5">
{#snippet fallback()}
<Logo class="shrink-0" style="--size: 0.875rem" />
{/snippet}
</BackendIcon>
{:else}
<BackendIcon {backend} class="h-3.5 w-3.5" />
{/if}
{/snippet}
<ScrollCarousel alwaysShowArrows class="min-w-0 flex-1" gapSize="2" innerClass="items-center">
{#if backends.length > 1}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<button
{...props}
class="inline-flex items-center whitespace-nowrap {FILTER_TRIGGER_CLASS}"
type="button"
>
<Server class="h-3.5 w-3.5" />
{providerLabel}
<ChevronDown class="h-3.5 w-3.5 opacity-60" />
</button>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content align="start" class="min-w-48">
<DropdownMenu.Group>
<DropdownMenu.GroupHeading>Providers</DropdownMenu.GroupHeading>
{#each backends as backend (backend.id)}
<DropdownMenu.CheckboxItem
checked={providers.includes(backend.id)}
onCheckedChange={(checked) => toggleProvider(backend.id, checked)}
>
{@render providerMark(backend)}
{backend.name}
<DropdownMenu.Shortcut>{providerCounts[backend.id] ?? 0}</DropdownMenu.Shortcut>
</DropdownMenu.CheckboxItem>
{/each}
</DropdownMenu.Group>
{#if providers.length > 0}
<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={() => (providers = [])}
>Show every provider</DropdownMenu.Item
>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
{/if}
<Select.Root
onValueChange={(value) => (contextLimit = Number(value))}
type="single"
@@ -82,6 +168,26 @@
</Select.Content>
</Select.Root>
<!-- a checkbox chip: the whole pill is the control, the box is its indicator -->
<Toggle
bind:pressed={draft}
class="inline-flex items-center whitespace-nowrap {FILTER_TRIGGER_CLASS} px-2!"
variant="outline"
>
<span
aria-hidden="true"
class="flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-shadow {draft
? 'border-border/30 bg-muted-foreground/15 text-foreground dark:border-border/20 dark:bg-muted-foreground/25'
: 'border-input bg-background dark:bg-input/30'}"
>
{#if draft}
<Check class="size-3" />
{/if}
</span>
Has draft sidecar
</Toggle>
<ToggleGroup.Root
class="border border-border/30 bg-muted/60 shadow-sm dark:border-border/20 dark:bg-muted/75"
onValueChange={setToggles}
@@ -1,8 +1,12 @@
<script lang="ts">
import ModelsManagerFilters from './ModelsManagerFilters.svelte';
import {
isLocalOption,
type ModalityKey,
modelContextLength,
type ModelDraft,
modelDraftsFor,
type ModelOverride,
type ModelQuantGroup,
type ModelsTableGroup
} from './utils';
@@ -19,7 +23,8 @@
MoreHorizontal,
Power,
Trash2,
X
X,
Zap
} from '@lucide/svelte';
import {
DropdownMenuActions,
@@ -40,8 +45,16 @@
import { Button } from '$lib/components/ui/button';
import { FAMILY_ROW_WINDOW, MODEL_ICON, MODEL_ROW_WINDOW } from '$lib/constants';
import { ModelCapability, ModelDownloadConfirmAction, ServerModelStatus } from '$lib/enums';
import { modelsStore, settingsStore, uiStore } from '$lib/stores';
import {
backendsModelsStore,
backendsStore,
modelsStore,
settingsStore,
uiStore
} from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
import { getBackend } from '$lib/utils/api-base';
import { getBackendCapabilities } from '$lib/utils/backend';
import { groupModelFamilies, type ModelFamilyGroup } from '$lib/utils/model-families';
import type { Snippet } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
@@ -51,12 +64,22 @@
capabilities?: ModelCapability[];
/** Smallest context a model must support; 0 keeps every model. */
contextLimit?: number;
/** Keep only models that have a draft sidecar to speculate with. */
draft?: boolean;
filter?: string;
groups: ModelsTableGroup[];
isFavorite: (option: ModelOption) => boolean;
onSelect: (option: ModelOption) => void;
/** Per-model load and inference overrides, keyed by backend-qualified id. */
/** Modalities a model must support at least one of. */
modalities?: ModalityKey[];
overrides: Record<string, ModelOverride>;
/** Backend ids to keep; empty keeps every provider. */
providers?: string[];
/** Repos each provider contributes to the current search, for the filter menu. */
providerCounts?: Record<string, number>;
/** Called when a row is set as the draft of the selected model. */
onUseAsDraft?: (draft: ModelOption, targetId: string) => void;
selectedId: string | null;
/** Rendered at the toolbar's right end, past the filters. */
toolbarEnd?: Snippet;
@@ -65,17 +88,28 @@
let {
capabilities = $bindable<ModelCapability[]>([]),
contextLimit = $bindable(0),
draft = $bindable(false),
filter = $bindable(''),
groups,
isFavorite,
modalities = $bindable<ModalityKey[]>([]),
onSelect,
onUseAsDraft,
overrides,
providerCounts = {},
providers = $bindable<string[]>([]),
selectedId,
toolbarEnd
}: Props = $props();
let isEmpty = $derived(groups.every((group) => group.items.length === 0));
let hasFilters = $derived(contextLimit > 0 || modalities.length > 0 || capabilities.length > 0);
let hasFilters = $derived(
providers.length > 0 ||
contextLimit > 0 ||
modalities.length > 0 ||
capabilities.length > 0 ||
draft
);
let filterInput = $state<HTMLInputElement | null>(null);
// the dialog hands focus to its first control, so the filter takes it instead
@@ -153,20 +187,38 @@
}
/** Row actions follow the app's dropdown pattern: icon, label, separators, variants. */
function rowActions(option: ModelOption, favorite: boolean, isHidden: boolean) {
function rowActions(option: ModelOption, canLoad: boolean, favorite: boolean, isHidden: boolean) {
// a draft only makes sense for the model the configuration pane has open, and
// only when both ends can load and unload at all
const canBeDraft = canLoad && selectedId !== null && selectedId !== option.id && selectedName;
return [
...(canBeDraft
? [
{
icon: Zap,
label: `Use as draft for ${selectedName}`,
onclick: () => onUseAsDraft?.(option, selectedId as string),
separator: true
}
]
: []),
{
icon: favorite ? HeartOff : Heart,
label: favorite ? 'Remove from favorites' : 'Add to favorites',
onclick: () => modelsStore.toggleFavorite(option.model)
},
{
icon: Trash2,
label: 'Delete from disk',
onclick: () => requestDelete(option),
separator: true,
variant: 'destructive' as const
},
...(canLoad
? [
{
icon: Trash2,
label: 'Delete from disk',
onclick: () => requestDelete(option),
separator: true,
variant: 'destructive' as const
}
]
: []),
{
icon: isHidden ? Eye : EyeOff,
label: isHidden ? 'Unhide model' : 'Hide model',
@@ -181,9 +233,25 @@
return modelsStore.getModelStatus(option.model);
}
/** Context the model runs with: what a loaded model reports. */
/** Drafts to show on a row: the configured one first, then this repo's own sidecars. */
function draftsFor(option: ModelOption): ModelDraft[] {
return modelDraftsFor(option, overrides[option.id]?.load?.speculativeDecoding);
}
/** Name of the model the configuration pane has open, for the draft action label. */
let selectedName = $derived(
modelsStore.models.find((option) => option.id === selectedId)?.name ?? null
);
/** Context the model runs with: the stored override, else what a loaded model reports. */
function configuredContext(option: ModelOption): number | null {
return isLoadedOption(option) ? modelsStore.props.getModelContextSize(option.model) : null;
const override = overrides[option.id]?.load?.contextLength;
if (override) return override;
return isLocalOption(option) && isLoadedOption(option)
? modelsStore.props.getModelContextSize(option.model)
: null;
}
type SortKey = 'context' | 'name' | 'status';
@@ -260,12 +328,14 @@
{@const isSleeping = status === ServerModelStatus.SLEEPING}
<ModelLoadControl
canLoad={getBackendCapabilities(getBackend(option.backendId)).loadUnload}
class="justify-self-center"
{isFailed}
isLoaded={isLoadedOption(option)}
{isLoading}
{isSleeping}
{option}
showRemoteMark
/>
{/snippet}
@@ -290,6 +360,7 @@
{#snippet row(option: ModelOption, indent = 0)}
{@const favorite = isFavorite(option)}
{@const canLoad = getBackendCapabilities(getBackend(option.backendId)).loadUnload}
{@const isHidden = modelsStore.isHidden(option.id)}
<!-- <div class="px-2"> -->
@@ -317,6 +388,7 @@
<ModelId
aliases={option.aliases}
class="min-w-0 flex-1"
drafts={draftsFor(option)}
hideCapabilities
hideModalities
modalities={option.modalities}
@@ -335,7 +407,7 @@
<div class="flex items-center justify-center justify-self-center">
<DropdownMenuActions
actions={rowActions(option, favorite, isHidden)}
actions={rowActions(option, canLoad, favorite, isHidden)}
align="end"
triggerIcon={MoreHorizontal}
triggerTooltip="Model actions"
@@ -347,10 +419,13 @@
{#snippet repoRow(entry: ModelQuantGroup, indent = 0)}
{@const isExpanded = !collapsedQuants.has(entry.key)}
{@const providerCount = new Set(entry.quants.map((option) => option.backendId ?? '')).size}
{@const groupLabel =
entry.kind === 'variants'
? `${entry.quants.length} variants`
: `${entry.quants.length} quants available`}
entry.kind === 'providers'
? `${providerCount} provider${providerCount === 1 ? '' : 's'}`
: entry.kind === 'variants'
? `${entry.quants.length} variants`
: `${entry.quants.length} quants available`}
{@const anyLoaded = entry.quants.some(isLoadedOption)}
<!-- a repo row stands for its quants, so it reports what they agree on -->
@@ -377,6 +452,7 @@
<ModelId
aliases={entry.base.aliases}
class="min-w-0"
drafts={draftsFor(entry.base)}
hideCapabilities
hideModalities
hideQuantization
@@ -417,8 +493,9 @@
</div>
{/snippet}
{#snippet quantRow(option: ModelOption, indent = 0)}
{#snippet quantRow(option: ModelOption, indent = 0, showProvider = false)}
{@const favorite = isFavorite(option)}
{@const canLoad = getBackendCapabilities(getBackend(option.backendId)).loadUnload}
{@const isHidden = modelsStore.isHidden(option.id)}
{@const quant = option.parsedId?.quantization ?? option.model}
@@ -435,7 +512,9 @@
tabindex="0"
>
<span class="flex min-w-0 items-center gap-3" style="padding-left: {indent}px">
<Badge class="h-5 shrink-0 px-1.5 text-[10px]" variant="secondary">{quant}</Badge>
<Badge class="h-5 shrink-0 px-1.5 text-[10px]" variant="secondary">
{showProvider ? (getBackend(option.backendId)?.name ?? quant) : quant}
</Badge>
<span class="truncate text-sm text-muted-foreground">{option.model}</span>
</span>
@@ -446,7 +525,7 @@
<div class="flex items-center justify-center justify-self-center">
<DropdownMenuActions
actions={rowActions(option, favorite, isHidden)}
actions={rowActions(option, canLoad, favorite, isHidden)}
align="end"
triggerIcon={MoreHorizontal}
triggerTooltip="Model actions"
@@ -461,7 +540,7 @@
{#if !collapsedQuants.has(entry.key)}
{#each entry.quants as quant (quant.id)}
{@render quantRow(quant, indent + 24)}
{@render quantRow(quant, indent + 24, entry.kind === 'providers')}
{/each}
{/if}
{:else}
@@ -568,15 +647,25 @@
size="sm"
/>
<ModelsManagerFilters bind:capabilities bind:contextLimit bind:modalities />
<ModelsManagerFilters
bind:capabilities
bind:contextLimit
bind:draft
bind:modalities
bind:providers
backends={backendsStore.enabled}
{providerCounts}
/>
{#if hasFilters}
<Button
class="gap-1.5 text-muted-foreground"
onclick={() => {
providers = [];
contextLimit = 0;
modalities = [];
capabilities = [];
draft = false;
}}
size="sm"
variant="ghost"
@@ -633,12 +722,17 @@
{/if}
{/snippet}
{@const backendState = group.backendId ? backendsModelsStore.get(group.backendId) : null}
<ModelsSection
backendId={group.kind === 'provider' ? (group.backendId ?? undefined) : undefined}
chevronClass="mr-7"
count={group.items.length}
defaultOpen={group.kind !== 'hidden'}
icon={groupIcon}
error={Boolean(backendState?.error)}
icon={group.kind === 'provider' ? undefined : groupIcon}
label={group.label}
loading={Boolean(backendState?.loading)}
persistKey={group.key}
revealChevronOnHover
sticky
@@ -1,12 +1,16 @@
<script lang="ts">
import { ChevronLeft, CircleAlert, Loader2 } from '@lucide/svelte';
import { CollapsibleSection } from '$lib/components/app';
import { BackendIcon } from '$lib/components/app/backends';
import { modelsStore } from '$lib/stores';
import { getBackend } from '$lib/utils/api-base';
import type { Snippet } from 'svelte';
interface Props {
/** Section rows. */
children: Snippet;
/** Renders the backend's logo in the header. */
backendId?: string;
/** Extra classes for the chevron, to line it up with a row's own control. */
chevronClass?: string;
/** Number shown next to the label, omitted when undefined. */
@@ -31,6 +35,7 @@
}
let {
backendId,
chevronClass = '',
children,
count,
@@ -81,6 +86,8 @@
{#if icon}
{@render icon()}
{:else if backendId}
<BackendIcon backend={getBackend(backendId)} class="h-3.5 w-3.5" />
{/if}
<span class="truncate">{label}</span>