mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-10-03 03:17:32 -05:00
ui : make the manager rail pick providers, not models
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
loadOverrides,
|
||||
type ModelOverride,
|
||||
type ModelsListFilter,
|
||||
type ModelsListGroup,
|
||||
type ModelsProviderGroup,
|
||||
saveOverrides
|
||||
} from './utils';
|
||||
import { LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
@@ -28,15 +28,17 @@
|
||||
|
||||
let filter = $state('');
|
||||
let view = $state<ModelsListFilter>('all');
|
||||
let selectedBackendId = $state<string | null>(null);
|
||||
let selectedId = $state<string | null>(null);
|
||||
let overrides = $state<Record<string, ModelOverride>>(loadOverrides());
|
||||
|
||||
let allModels = $derived(modelsStore.models);
|
||||
let selected = $derived(allModels.find((option) => option.id === selectedId) ?? null);
|
||||
let favorites = $derived(
|
||||
allModels.filter((option) => modelsStore.favoriteModelIds.has(option.model))
|
||||
let isFavorite = $derived((option: ModelOption) =>
|
||||
modelsStore.favoriteModelIds.has(option.model)
|
||||
);
|
||||
let matches = $derived.by(() => {
|
||||
// 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(() => {
|
||||
const term = filter.trim().toLowerCase();
|
||||
|
||||
return allModels.filter((option) => {
|
||||
@@ -49,25 +51,23 @@
|
||||
return true;
|
||||
});
|
||||
});
|
||||
let groups = $derived.by(() => {
|
||||
const byBackend = new SvelteMap<string, ModelOption[]>();
|
||||
let providers = $derived.by(() => {
|
||||
const counts = new SvelteMap<string, number>();
|
||||
|
||||
for (const option of matches) {
|
||||
for (const option of visible) {
|
||||
const backendId = option.backendId ?? LOCAL_BACKEND_ID;
|
||||
|
||||
if (!byBackend.has(backendId)) byBackend.set(backendId, []);
|
||||
|
||||
byBackend.get(backendId)!.push(option);
|
||||
counts.set(backendId, (counts.get(backendId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const ordered: ModelsListGroup[] = [];
|
||||
const localItems = byBackend.get(LOCAL_BACKEND_ID);
|
||||
const ordered: ModelsProviderGroup[] = [];
|
||||
const localCount = counts.get(LOCAL_BACKEND_ID);
|
||||
|
||||
if (localItems?.length) {
|
||||
if (localCount) {
|
||||
ordered.push({
|
||||
backendId: LOCAL_BACKEND_ID,
|
||||
count: localCount,
|
||||
isLocal: true,
|
||||
items: localItems,
|
||||
label: 'This server'
|
||||
});
|
||||
}
|
||||
@@ -75,15 +75,26 @@
|
||||
for (const backend of backendsStore.enabled) {
|
||||
if (backend.id === LOCAL_BACKEND_ID) continue;
|
||||
|
||||
const items = byBackend.get(backend.id);
|
||||
const count = counts.get(backend.id);
|
||||
|
||||
if (items?.length) {
|
||||
ordered.push({ backendId: backend.id, isLocal: false, items, label: backend.name });
|
||||
if (count) {
|
||||
ordered.push({
|
||||
backendId: backend.id,
|
||||
count,
|
||||
isLocal: false,
|
||||
label: backend.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ordered;
|
||||
});
|
||||
let matches = $derived(
|
||||
selectedBackendId === null
|
||||
? visible
|
||||
: visible.filter((option) => (option.backendId ?? LOCAL_BACKEND_ID) === selectedBackendId)
|
||||
);
|
||||
let selected = $derived(allModels.find((option) => option.id === selectedId) ?? null);
|
||||
let summary = $derived.by(() => {
|
||||
const local = matches.filter(isLocalOption);
|
||||
const bytes = local.reduce((total, option) => {
|
||||
@@ -102,7 +113,12 @@
|
||||
.join(' · ');
|
||||
});
|
||||
let title = $derived(
|
||||
view === 'favorites' ? 'Favorites' : view === 'loaded' ? 'Loaded models' : 'All models'
|
||||
view === 'favorites'
|
||||
? 'Favorites'
|
||||
: view === 'loaded'
|
||||
? 'Loaded models'
|
||||
: (providers.find((provider) => provider.backendId === selectedBackendId)?.label ??
|
||||
'All models')
|
||||
);
|
||||
|
||||
function toggleFavorite(option: ModelOption): void {
|
||||
@@ -138,24 +154,22 @@
|
||||
|
||||
<div
|
||||
class="grid min-h-0 flex-1"
|
||||
style="grid-template-columns: 20rem minmax(0, 1fr){selected ? ' 30rem' : ''};"
|
||||
style="grid-template-columns: 16rem minmax(0, 1fr){selected ? ' 30rem' : ''};"
|
||||
>
|
||||
<div class="min-h-0 border-r border-border/40 px-3 py-3">
|
||||
<div class="min-h-0 border-r border-border/40 px-2 py-3">
|
||||
<ModelsManagerModelsList
|
||||
bind:filter
|
||||
bind:view
|
||||
{favorites}
|
||||
{groups}
|
||||
isFavorite={(option) => modelsStore.favoriteModelIds.has(option.model)}
|
||||
onSelect={(option) => (selectedId = option.id)}
|
||||
onToggleFavorite={toggleFavorite}
|
||||
{selectedId}
|
||||
onSelect={(backendId) => (selectedBackendId = backendId)}
|
||||
{providers}
|
||||
{selectedBackendId}
|
||||
totalCount={visible.length}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0">
|
||||
<ModelsManagerModelsTable
|
||||
isFavorite={(option) => modelsStore.favoriteModelIds.has(option.model)}
|
||||
bind:filter
|
||||
bind:view
|
||||
{isFavorite}
|
||||
models={matches}
|
||||
onCopyId={copyId}
|
||||
onGetModels={() => onOpenDiscover?.()}
|
||||
|
||||
+26
-70
@@ -1,83 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { ModelsListFilter, ModelsListGroup } from '../utils';
|
||||
import ModelsManagerModelsListControls from './ModelsManagerModelsListControls.svelte';
|
||||
import type { ModelsProviderGroup } from '../utils';
|
||||
import ModelsManagerModelsListItem from './ModelsManagerModelsListItem.svelte';
|
||||
import { Heart } from '@lucide/svelte';
|
||||
import { Logo, ModelsSection } from '$lib/components/app';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
favorites: ModelOption[];
|
||||
filter?: string;
|
||||
groups: ModelsListGroup[];
|
||||
isFavorite: (option: ModelOption) => boolean;
|
||||
onSelect: (option: ModelOption) => void;
|
||||
onToggleFavorite: (option: ModelOption) => void;
|
||||
selectedId: string | null;
|
||||
view?: ModelsListFilter;
|
||||
onSelect: (backendId: string | null) => void;
|
||||
providers: ModelsProviderGroup[];
|
||||
selectedBackendId: string | null;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
let {
|
||||
favorites,
|
||||
filter = $bindable(''),
|
||||
groups,
|
||||
isFavorite,
|
||||
onSelect,
|
||||
onToggleFavorite,
|
||||
selectedId,
|
||||
view = $bindable<ModelsListFilter>('all')
|
||||
}: Props = $props();
|
||||
let { onSelect, providers, selectedBackendId, totalCount }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col gap-3">
|
||||
<ModelsManagerModelsListControls bind:filter bind:view />
|
||||
<div class="flex h-full min-h-0 flex-col gap-1">
|
||||
<p class="px-2 py-1 text-[13px] font-semibold text-muted-foreground/70 select-none">Providers</p>
|
||||
|
||||
<div class="min-h-0 flex-1 space-y-1 overflow-y-auto">
|
||||
{#if favorites.length > 0}
|
||||
<ModelsSection count={favorites.length} label="Favorites" sticky>
|
||||
{#snippet icon()}
|
||||
<Heart class="h-3.5 w-3.5 shrink-0" />
|
||||
{/snippet}
|
||||
<div class="min-h-0 flex-1 space-y-0.5 overflow-y-auto">
|
||||
<ModelsManagerModelsListItem
|
||||
backendId={null}
|
||||
count={totalCount}
|
||||
isActive={selectedBackendId === null}
|
||||
isLocal={false}
|
||||
label="All providers"
|
||||
onSelect={() => onSelect(null)}
|
||||
/>
|
||||
|
||||
{#each favorites as option (option.id)}
|
||||
<ModelsManagerModelsListItem
|
||||
isFavorite={isFavorite(option)}
|
||||
isSelected={selectedId === option.id}
|
||||
onSelect={() => onSelect(option)}
|
||||
onToggleFavorite={() => onToggleFavorite(option)}
|
||||
{option}
|
||||
/>
|
||||
{/each}
|
||||
</ModelsSection>
|
||||
{/if}
|
||||
|
||||
{#each groups as group (group.backendId)}
|
||||
<ModelsSection
|
||||
backendId={group.isLocal ? undefined : group.backendId}
|
||||
count={group.items.length}
|
||||
label={group.label}
|
||||
sticky
|
||||
>
|
||||
{#snippet icon()}
|
||||
{#if group.isLocal}
|
||||
<Logo class="shrink-0" style="--size: 0.875rem" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#each group.items as option (option.id)}
|
||||
<ModelsManagerModelsListItem
|
||||
isFavorite={isFavorite(option)}
|
||||
isSelected={selectedId === option.id}
|
||||
onSelect={() => onSelect(option)}
|
||||
onToggleFavorite={() => onToggleFavorite(option)}
|
||||
{option}
|
||||
/>
|
||||
{/each}
|
||||
</ModelsSection>
|
||||
{#each providers as provider (provider.backendId)}
|
||||
<ModelsManagerModelsListItem
|
||||
backendId={provider.backendId}
|
||||
count={provider.count}
|
||||
isActive={selectedBackendId === provider.backendId}
|
||||
isLocal={provider.isLocal}
|
||||
label={provider.label}
|
||||
onSelect={() => onSelect(provider.backendId)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if groups.length === 0 && favorites.length === 0}
|
||||
<p class="px-2 py-6 text-center text-sm text-muted-foreground">No models found.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+28
-97
@@ -1,109 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { modelQuantLabel, modelSizeLabel } from '../utils';
|
||||
import { ChevronDown, ChevronUp, Heart } from '@lucide/svelte';
|
||||
import { ModelCapabilityIcons } from '$lib/components/app';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { ModelCapability, ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
import { Boxes } from '@lucide/svelte';
|
||||
import { Logo } from '$lib/components/app';
|
||||
import { BackendIcon } from '$lib/components/app/backends';
|
||||
import { getBackend } from '$lib/utils/api-base';
|
||||
|
||||
interface Props {
|
||||
isFavorite: boolean;
|
||||
isSelected: boolean;
|
||||
/** Null renders the aggregate entry, which lists every provider's models. */
|
||||
backendId: string | null;
|
||||
count: number;
|
||||
isActive: boolean;
|
||||
isLocal: boolean;
|
||||
label: string;
|
||||
onSelect: () => void;
|
||||
onToggleFavorite: () => void;
|
||||
option: ModelOption;
|
||||
}
|
||||
|
||||
let { isFavorite, isSelected, onSelect, onToggleFavorite, option }: Props = $props();
|
||||
|
||||
let expanded = $state(true);
|
||||
|
||||
let serverStatus = $derived.by(() => {
|
||||
const model = modelsStore.routerModels.find((m) => m.id === option.model);
|
||||
|
||||
return (model?.status?.value as ServerModelStatus) ?? null;
|
||||
});
|
||||
let isOperationInProgress = $derived(modelsStore.status.isOperationInProgress(option.model));
|
||||
let isFailed = $derived(serverStatus === ServerModelStatus.FAILED);
|
||||
let isSleeping = $derived(serverStatus === ServerModelStatus.SLEEPING);
|
||||
let isLoaded = $derived(
|
||||
(serverStatus === ServerModelStatus.LOADED || isSleeping) && !isOperationInProgress
|
||||
);
|
||||
let isLoading = $derived(serverStatus === ServerModelStatus.LOADING || isOperationInProgress);
|
||||
|
||||
let quant = $derived(modelQuantLabel(option));
|
||||
let size = $derived(modelSizeLabel(option));
|
||||
let supportsToolUse = $derived(option.capabilities.includes(ModelCapability.TOOL_USE));
|
||||
let supportsThinking = $derived(option.capabilities.includes(ModelCapability.REASONING));
|
||||
let { backendId, count, isActive, isLocal, label, onSelect }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
<button
|
||||
aria-pressed={isActive}
|
||||
class={[
|
||||
'group rounded-md transition',
|
||||
isSelected ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/50'
|
||||
'flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition',
|
||||
isActive ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/50'
|
||||
]}
|
||||
onclick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 px-2 py-1.5">
|
||||
<button
|
||||
aria-label="Select model"
|
||||
class="shrink-0 cursor-pointer"
|
||||
onclick={onSelect}
|
||||
type="button"
|
||||
>
|
||||
{#if isLoading}
|
||||
<span class="block h-2 w-2 animate-pulse rounded-full bg-amber-500"></span>
|
||||
{:else if isFailed}
|
||||
<span class="block h-2 w-2 rounded-full bg-destructive"></span>
|
||||
{:else}
|
||||
<span
|
||||
class="block h-2 w-2 rounded-full {isLoaded
|
||||
? 'bg-emerald-500'
|
||||
: 'border border-muted-foreground/50'}"
|
||||
></span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isFavorite}
|
||||
<button
|
||||
aria-label="Remove from favorites"
|
||||
class="shrink-0 cursor-pointer text-muted-foreground transition hover:text-foreground"
|
||||
onclick={onToggleFavorite}
|
||||
type="button"
|
||||
>
|
||||
<Heart class="h-3.5 w-3.5 fill-current" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button class="min-w-0 flex-1 cursor-pointer text-left" onclick={onSelect} type="button">
|
||||
<span class="block truncate text-sm">{option.name}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
aria-label={expanded ? 'Hide model details' : 'Show model details'}
|
||||
class="shrink-0 cursor-pointer text-muted-foreground transition hover:text-foreground"
|
||||
onclick={() => (expanded = !expanded)}
|
||||
type="button"
|
||||
>
|
||||
{#if expanded}
|
||||
<ChevronUp class="h-3 w-3" />
|
||||
{:else}
|
||||
<ChevronDown class="h-3 w-3" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if expanded}
|
||||
<div class="flex flex-wrap items-center gap-1.5 px-2 pb-2 pl-5.5">
|
||||
{#if quant}
|
||||
<Badge class="h-5 px-1.5 text-[10px]" variant="secondary">{quant}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if size}
|
||||
<span class="text-xs text-muted-foreground">{size}</span>
|
||||
{/if}
|
||||
|
||||
<ModelCapabilityIcons modalities={option.modalities} {supportsThinking} {supportsToolUse} />
|
||||
</div>
|
||||
{#if isLocal}
|
||||
<Logo class="shrink-0" style="--size: 1rem" />
|
||||
{:else if backendId}
|
||||
<BackendIcon backend={getBackend(backendId)} class="h-4 w-4" />
|
||||
{:else}
|
||||
<Boxes class="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="truncate">{label}</span>
|
||||
|
||||
<span class="ml-auto shrink-0 text-xs text-muted-foreground/70">{count}</span>
|
||||
</button>
|
||||
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ModelsManagerModelsListControls from './ModelsManagerModelsList/ModelsManagerModelsListControls.svelte';
|
||||
import type { ModelsListFilter } from './utils';
|
||||
import { modelParamsLabel, modelSizeLabel, servedByLabel } from './utils';
|
||||
import { ArrowUpDown, ChevronDown, Heart, Loader2, MoreHorizontal, Plus } from '@lucide/svelte';
|
||||
import { ModelCapabilityIcons } from '$lib/components/app';
|
||||
@@ -10,6 +12,7 @@
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
interface Props {
|
||||
filter?: string;
|
||||
isFavorite: (option: ModelOption) => boolean;
|
||||
models: ModelOption[];
|
||||
onCopyId: (option: ModelOption) => void;
|
||||
@@ -21,9 +24,11 @@
|
||||
selectedId: string | null;
|
||||
summary: string;
|
||||
title: string;
|
||||
view?: ModelsListFilter;
|
||||
}
|
||||
|
||||
let {
|
||||
filter = $bindable(''),
|
||||
isFavorite,
|
||||
models,
|
||||
onCopyId,
|
||||
@@ -34,7 +39,8 @@
|
||||
onUseInNewChat,
|
||||
selectedId,
|
||||
summary,
|
||||
title
|
||||
title,
|
||||
view = $bindable<ModelsListFilter>('all')
|
||||
}: Props = $props();
|
||||
|
||||
let descending = $state(false);
|
||||
@@ -93,6 +99,10 @@
|
||||
<span class="text-right">Actions</span>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 border-b border-border/40 px-4 py-3">
|
||||
<ModelsManagerModelsListControls bind:filter bind:view />
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto">
|
||||
{#each sorted as option (option.id)}
|
||||
{@const status = stateOf(option)}
|
||||
|
||||
@@ -38,11 +38,11 @@ export type ModelOverrideMap = Record<string, ModelOverride>;
|
||||
|
||||
export type ModelsListFilter = 'all' | 'favorites' | 'loaded';
|
||||
|
||||
/** One backend's worth of models, as listed in the manager's rail. */
|
||||
export interface ModelsListGroup {
|
||||
/** One backend, as listed in the manager's provider rail. */
|
||||
export interface ModelsProviderGroup {
|
||||
backendId: string;
|
||||
count: number;
|
||||
isLocal: boolean;
|
||||
items: ModelOption[];
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user