mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : prefetch backend model lists on first selector open
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
+10
-2
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Plus } from '@lucide/svelte';
|
import { Loader2, Plus } from '@lucide/svelte';
|
||||||
|
import { backendsModelsStore } from '$lib/stores';
|
||||||
import type { Backend } from '$lib/types';
|
import type { Backend } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,9 +15,10 @@
|
|||||||
|
|
||||||
<div class="flex items-center gap-1 overflow-x-auto border-b border-border/50 px-2 py-2">
|
<div class="flex items-center gap-1 overflow-x-auto border-b border-border/50 px-2 py-2">
|
||||||
{#each backends as backend (backend.id)}
|
{#each backends as backend (backend.id)}
|
||||||
|
{@const state = backendsModelsStore.get(backend.id)}
|
||||||
<button
|
<button
|
||||||
class={[
|
class={[
|
||||||
'shrink-0 rounded-full px-2.5 py-1 text-xs font-medium whitespace-nowrap transition',
|
'inline-flex shrink-0 items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium whitespace-nowrap transition',
|
||||||
activeId === backend.id
|
activeId === backend.id
|
||||||
? 'bg-muted text-foreground'
|
? 'bg-muted text-foreground'
|
||||||
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||||
@@ -25,6 +27,12 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{backend.name}
|
{backend.name}
|
||||||
|
|
||||||
|
{#if state.loading}
|
||||||
|
<Loader2 class="h-3 w-3 animate-spin" />
|
||||||
|
{:else if state.error}
|
||||||
|
<span class="h-1.5 w-1.5 rounded-full bg-destructive" title={state.error}></span>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { filterModelOptions, groupModelOptions } from '$lib/components/app/navigation/utils';
|
import { filterModelOptions, groupModelOptions } from '$lib/components/app/navigation/utils';
|
||||||
import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
|
import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants';
|
||||||
import { backendsStore, modelsStore, serverStore } from '$lib/stores';
|
import { backendsModelsStore, backendsStore, modelsStore, serverStore } from '$lib/stores';
|
||||||
import type { Backend } from '$lib/types';
|
import type { Backend } from '$lib/types';
|
||||||
import type { ModelOption } from '$lib/types/models';
|
import type { ModelOption } from '$lib/types/models';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@@ -86,6 +86,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
|||||||
let searchTerm = $state('');
|
let searchTerm = $state('');
|
||||||
let showModelDialog = $state(false);
|
let showModelDialog = $state(false);
|
||||||
let infoModelId = $state<string | null>(null);
|
let infoModelId = $state<string | null>(null);
|
||||||
|
let backendsPrefetched = false;
|
||||||
|
|
||||||
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
|
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
|
||||||
const groupedFilteredOptions = $derived(
|
const groupedFilteredOptions = $derived(
|
||||||
@@ -106,6 +107,12 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleOpenChange(open: boolean) {
|
function handleOpenChange(open: boolean) {
|
||||||
|
// first open: prefetch every backend's model list so switching is instant
|
||||||
|
if (open && !backendsPrefetched) {
|
||||||
|
backendsPrefetched = true;
|
||||||
|
void backendsModelsStore.loadAll();
|
||||||
|
}
|
||||||
|
|
||||||
if (loading || updating) return;
|
if (loading || updating) return;
|
||||||
|
|
||||||
if (isRouter) {
|
if (isRouter) {
|
||||||
@@ -130,6 +137,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
|||||||
searchTerm = '';
|
searchTerm = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await backendsModelsStore.ensureLoaded(backendId);
|
||||||
await modelsStore.switchBackend();
|
await modelsStore.switchBackend();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to switch backend:', error);
|
console.error('Failed to switch backend:', error);
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* backendsModelsStore - Per-backend model catalog cache.
|
||||||
|
*
|
||||||
|
* The models selector prefetches every enabled backend's model list the first
|
||||||
|
* time it opens, so switching backends is instant and the switcher can show
|
||||||
|
* load state. The active backend's list still lives in modelsStore, which owns
|
||||||
|
* selection and chat wiring; this cache is the prefetch layer for the others.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BackendsService } from '$lib/services/backends.service';
|
||||||
|
import { backendsStore } from '$lib/stores/backends.svelte';
|
||||||
|
import type { ModelOption } from '$lib/types/models';
|
||||||
|
|
||||||
|
export interface BackendModelsState {
|
||||||
|
error: string | null;
|
||||||
|
loaded: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
models: ModelOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_STATE: BackendModelsState = { error: null, loaded: false, loading: false, models: [] };
|
||||||
|
|
||||||
|
class BackendsModelsStore {
|
||||||
|
private states = $state<Record<string, BackendModelsState>>({});
|
||||||
|
|
||||||
|
clear(backendId: string): void {
|
||||||
|
delete this.states[backendId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a backend's models once. Local backends are skipped: their list is
|
||||||
|
* owned by modelsStore.
|
||||||
|
*/
|
||||||
|
async ensureLoaded(backendId: string): Promise<void> {
|
||||||
|
const backend = backendsStore.enabled.find((candidate) => candidate.id === backendId);
|
||||||
|
|
||||||
|
if (!backend || !backend.baseUrl.trim()) return;
|
||||||
|
|
||||||
|
const state = this.states[backendId];
|
||||||
|
|
||||||
|
if (state?.loaded || state?.loading) return;
|
||||||
|
|
||||||
|
this.states[backendId] = { error: null, loaded: false, loading: true, models: [] };
|
||||||
|
|
||||||
|
const result = await BackendsService.listModels(backend);
|
||||||
|
|
||||||
|
this.states[backendId] = {
|
||||||
|
error: result.error ?? null,
|
||||||
|
loaded: result.ok,
|
||||||
|
loading: false,
|
||||||
|
models: result.models
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get(backendId: string): BackendModelsState {
|
||||||
|
return this.states[backendId] ?? EMPTY_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Prefetch every enabled external backend, skipping the active one. */
|
||||||
|
async loadAll(): Promise<void> {
|
||||||
|
const enabled = backendsStore.enabled;
|
||||||
|
const ids = new Set(enabled.map((backend) => backend.id));
|
||||||
|
|
||||||
|
for (const id of Object.keys(this.states)) {
|
||||||
|
if (!ids.has(id)) {
|
||||||
|
delete this.states[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
enabled
|
||||||
|
.filter((backend) => backend.id !== backendsStore.active.id)
|
||||||
|
.map((backend) => this.ensureLoaded(backend.id))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backendsModelsStore = new BackendsModelsStore();
|
||||||
@@ -40,6 +40,8 @@ export { mcpStore } from './mcp/index.svelte';
|
|||||||
// BACKENDS
|
// BACKENDS
|
||||||
export { backendsStore } from './backends.svelte';
|
export { backendsStore } from './backends.svelte';
|
||||||
|
|
||||||
|
export { backendsModelsStore } from './backendsModels.svelte';
|
||||||
|
|
||||||
// MODELS
|
// MODELS
|
||||||
export { modelsStore } from './models/index.svelte';
|
export { modelsStore } from './models/index.svelte';
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { FAVORITE_MODELS_LOCALSTORAGE_KEY } from '$lib/constants';
|
|||||||
import { ServerModelStatus } from '$lib/enums';
|
import { ServerModelStatus } from '$lib/enums';
|
||||||
import { ModelsService } from '$lib/services/models.service';
|
import { ModelsService } from '$lib/services/models.service';
|
||||||
// direct imports between stores, not via the barrel, to avoid circular deps
|
// direct imports between stores, not via the barrel, to avoid circular deps
|
||||||
|
import { backendsStore } from '$lib/stores/backends.svelte';
|
||||||
|
import { backendsModelsStore } from '$lib/stores/backendsModels.svelte';
|
||||||
import { conversationsStore } from '$lib/stores/conversations/index.svelte';
|
import { conversationsStore } from '$lib/stores/conversations/index.svelte';
|
||||||
import { type ModelPropsHost, ModelPropsManager } from '$lib/stores/models/props.svelte';
|
import { type ModelPropsHost, ModelPropsManager } from '$lib/stores/models/props.svelte';
|
||||||
import { type ModelStatusHost, ModelStatusManager } from '$lib/stores/models/status.svelte';
|
import { type ModelStatusHost, ModelStatusManager } from '$lib/stores/models/status.svelte';
|
||||||
@@ -350,11 +352,27 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
|||||||
async switchBackend(): Promise<void> {
|
async switchBackend(): Promise<void> {
|
||||||
this.status.unsubscribe();
|
this.status.unsubscribe();
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
this.models = [];
|
|
||||||
this.routerModels = [];
|
this.routerModels = [];
|
||||||
this.error = null;
|
this.error = null;
|
||||||
serverStore.clear();
|
serverStore.clear();
|
||||||
|
|
||||||
|
// prefer the prefetched list so switching does not refetch
|
||||||
|
const backend = backendsStore.active;
|
||||||
|
const cached = backend.baseUrl.trim() ? backendsModelsStore.get(backend.id) : null;
|
||||||
|
|
||||||
|
if (cached?.loaded) {
|
||||||
|
this.models = cached.models;
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
await serverStore.fetch();
|
||||||
|
|
||||||
|
if (this.models.length > 0) {
|
||||||
|
await this.ensureFirstModelSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.fetch(true);
|
await this.fetch(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user