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">
|
||||
import { Plus } from '@lucide/svelte';
|
||||
import { Loader2, Plus } from '@lucide/svelte';
|
||||
import { backendsModelsStore } from '$lib/stores';
|
||||
import type { Backend } from '$lib/types';
|
||||
|
||||
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">
|
||||
{#each backends as backend (backend.id)}
|
||||
{@const state = backendsModelsStore.get(backend.id)}
|
||||
<button
|
||||
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
|
||||
? 'bg-muted text-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||
@@ -25,6 +27,12 @@
|
||||
type="button"
|
||||
>
|
||||
{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>
|
||||
{/each}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { filterModelOptions, groupModelOptions } from '$lib/components/app/navigation/utils';
|
||||
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 { ModelOption } from '$lib/types/models';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -86,6 +86,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
let searchTerm = $state('');
|
||||
let showModelDialog = $state(false);
|
||||
let infoModelId = $state<string | null>(null);
|
||||
let backendsPrefetched = false;
|
||||
|
||||
const filteredOptions = $derived(filterModelOptions(options, searchTerm));
|
||||
const groupedFilteredOptions = $derived(
|
||||
@@ -106,6 +107,12 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
});
|
||||
|
||||
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 (isRouter) {
|
||||
@@ -130,6 +137,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
searchTerm = '';
|
||||
|
||||
try {
|
||||
await backendsModelsStore.ensureLoaded(backendId);
|
||||
await modelsStore.switchBackend();
|
||||
} catch (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
|
||||
export { backendsStore } from './backends.svelte';
|
||||
|
||||
export { backendsModelsStore } from './backendsModels.svelte';
|
||||
|
||||
// MODELS
|
||||
export { modelsStore } from './models/index.svelte';
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { FAVORITE_MODELS_LOCALSTORAGE_KEY } from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { ModelsService } from '$lib/services/models.service';
|
||||
// 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 { type ModelPropsHost, ModelPropsManager } from '$lib/stores/models/props.svelte';
|
||||
import { type ModelStatusHost, ModelStatusManager } from '$lib/stores/models/status.svelte';
|
||||
@@ -350,11 +352,27 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
async switchBackend(): Promise<void> {
|
||||
this.status.unsubscribe();
|
||||
this.clearSelection();
|
||||
this.models = [];
|
||||
this.routerModels = [];
|
||||
this.error = null;
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user