mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : prepare every backend at startup so tabs only swap memory
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -68,9 +68,12 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
const updating = $derived(modelsStore.updating);
|
||||
const activeId = $derived(modelsStore.selectedModelId);
|
||||
const backends = $derived(backendsStore.enabled);
|
||||
// Router mode and external backends both expose a selectable model list;
|
||||
// a single-model llama.cpp server does not.
|
||||
const isMultiModel = $derived(serverStore.isRouterMode || !serverStore.capabilities.props);
|
||||
// Router mode and external backends both expose a selectable model list, and
|
||||
// configured backends always need the tabs; only a lone llama.cpp server
|
||||
// without a router has nothing to list.
|
||||
const isMultiModel = $derived(
|
||||
serverStore.isRouterMode || !serverStore.capabilities.props || backendsStore.enabled.length > 1
|
||||
);
|
||||
const isRouter = $derived(serverStore.isRouterMode);
|
||||
const serverModel = $derived(modelsStore.singleModelName);
|
||||
const currentModel = $derived(opts.currentModel());
|
||||
@@ -118,9 +121,9 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (loading || updating) return;
|
||||
|
||||
// a single-model llama.cpp server has no list to show, so the trigger
|
||||
// opens the model info dialog instead; external backends have a menu
|
||||
if (!isRouter && serverStore.capabilities.props) {
|
||||
// a single-model llama.cpp server with no other backend has no list to
|
||||
// show, so the trigger opens the model info dialog instead
|
||||
if (!isMultiModel) {
|
||||
showModelDialog = open;
|
||||
|
||||
return;
|
||||
@@ -129,9 +132,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
|
||||
searchTerm = '';
|
||||
|
||||
if (open && isRouter) {
|
||||
modelsStore.fetchRouterModels().then(() => {
|
||||
modelsStore.props.fetchModalitiesForLoadedModels();
|
||||
});
|
||||
}
|
||||
|
||||
opts.onOpenChange?.(open);
|
||||
|
||||
@@ -15,17 +15,21 @@ export class PropsService {
|
||||
* In ROUTER mode, returns server-wide settings without model-specific modalities.
|
||||
*
|
||||
* @param autoload - If false, prevents automatic model loading (default: false)
|
||||
* @param backendId - Backend to ask; defaults to the active one.
|
||||
* @returns Server properties including default generation settings and capabilities
|
||||
* @throws {Error} If the request fails or returns invalid data
|
||||
*/
|
||||
static async fetch(autoload = false): Promise<ApiLlamaCppServerProps> {
|
||||
static async fetch(autoload = false, backendId?: string): Promise<ApiLlamaCppServerProps> {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
if (!autoload) {
|
||||
params.autoload = 'false';
|
||||
}
|
||||
|
||||
return apiFetchWithParams<ApiLlamaCppServerProps>('./props', params, { authOnly: true });
|
||||
return apiFetchWithParams<ApiLlamaCppServerProps>('./props', params, {
|
||||
authOnly: true,
|
||||
backendId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,13 @@ import { backendsStore } from './backends.svelte';
|
||||
import { backendsModelsStore } from './backendsModels.svelte';
|
||||
import { conversationsStore } from './conversations/index.svelte';
|
||||
import { permissionsStore } from './permissions.svelte';
|
||||
import { serverStore } from './server.svelte';
|
||||
import { settingsStore } from './settings/index.svelte';
|
||||
import { tabsStore } from './tabs.svelte';
|
||||
import { toolsStore } from './tools.svelte';
|
||||
import { versionStore } from './version.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
import { MigrationService } from '$lib/services/migration.service';
|
||||
|
||||
let startup: Promise<void> | null = null;
|
||||
@@ -25,6 +27,12 @@ export function initStores(): Promise<void> {
|
||||
// per-backend and never block startup
|
||||
void backendsModelsStore.loadAll();
|
||||
|
||||
// the local server state is needed once its tab is opened; loading it here
|
||||
// keeps the tab switch free of /props requests
|
||||
if (backendsStore.local.enabled && backendsStore.active.id !== LOCAL_BACKEND_ID) {
|
||||
void serverStore.prefetchLocalState();
|
||||
}
|
||||
|
||||
permissionsStore.initialize();
|
||||
toolsStore.initialize();
|
||||
void versionStore.initialize();
|
||||
|
||||
@@ -98,10 +98,14 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
const merged: ModelOption[] = [];
|
||||
|
||||
for (const option of this.activeModels) {
|
||||
// keep the backend an option was built for: rows from the previous
|
||||
// backend must not be relabelled while a switch is in flight
|
||||
const backendId = option.backendId ?? activeBackendId;
|
||||
|
||||
merged.push({
|
||||
...option,
|
||||
backendId: activeBackendId,
|
||||
id: qualifyModelId(activeBackendId, option.id)
|
||||
backendId,
|
||||
id: qualifyModelId(backendId, rawModelId(option.id))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,8 +115,8 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
for (const option of backendsModelsStore.get(backend.id).models) {
|
||||
merged.push({
|
||||
...option,
|
||||
backendId: backend.id,
|
||||
id: qualifyModelId(backend.id, option.id)
|
||||
backendId: option.backendId ?? backend.id,
|
||||
id: qualifyModelId(option.backendId ?? backend.id, rawModelId(option.id))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -404,12 +408,12 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the active backend's state in memory. Every backend is prefetched at
|
||||
* startup, so switching tabs restores the cached list and the local server
|
||||
* state without a request.
|
||||
* Activate a backend for the selector tabs. Everything comes from memory:
|
||||
* the model list and router rows are prefetched at startup and the local
|
||||
* server state is kept while an external backend is active. The selection
|
||||
* is left alone, switching tabs must not pick a model.
|
||||
*/
|
||||
async switchBackend(): Promise<void> {
|
||||
this.clearSelection();
|
||||
this.error = null;
|
||||
|
||||
const backend = backendsStore.active;
|
||||
@@ -446,10 +450,6 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
this.routerModels = cached.raw.data;
|
||||
this.activeModels = this.buildModelOptions(cached.raw);
|
||||
}
|
||||
|
||||
if (this.activeModels.length > 0) {
|
||||
await this.ensureFirstModelSelected();
|
||||
}
|
||||
}
|
||||
|
||||
toDisplayName(id: string): string {
|
||||
@@ -506,6 +506,9 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
|
||||
return {
|
||||
aliases: item.aliases ?? [],
|
||||
// stamp the backend here so the option keeps its origin even
|
||||
// after another backend becomes active
|
||||
backendId: backendsStore.active.id,
|
||||
capabilities: rawCapabilities.filter((value: unknown): value is string =>
|
||||
Boolean(value)
|
||||
),
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* PropsService for the /props fetch.
|
||||
*/
|
||||
|
||||
import { BACKEND_CAPABILITIES } from '$lib/constants';
|
||||
import { BACKEND_CAPABILITIES, LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
import { ServerRole } from '$lib/enums';
|
||||
import { PropsService } from '$lib/services/props.service';
|
||||
import type { BackendCapabilities } from '$lib/types';
|
||||
@@ -63,6 +63,10 @@ class ServerStore {
|
||||
* switching back restores it instead of asking the server again.
|
||||
*/
|
||||
cacheLocalState(): void {
|
||||
// props only exist while a llama.cpp server is active; an external to
|
||||
// external switch must not overwrite the kept local state with blanks
|
||||
if (!this.props) return;
|
||||
|
||||
this.localState = { props: this.props, role: this.role };
|
||||
}
|
||||
|
||||
@@ -156,6 +160,25 @@ class ServerStore {
|
||||
await promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the local server state in the background at startup. The local tab
|
||||
* then opens from memory instead of asking for props on the first click.
|
||||
*/
|
||||
async prefetchLocalState(): Promise<void> {
|
||||
if (this.localState) return;
|
||||
|
||||
try {
|
||||
const props = await PropsService.fetch(false, LOCAL_BACKEND_ID);
|
||||
|
||||
this.localState = {
|
||||
props,
|
||||
role: props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL
|
||||
};
|
||||
} catch {
|
||||
// the local tab falls back to fetching when it is opened
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore the state kept by {@link cacheLocalState}; no request is made. */
|
||||
restoreLocalState(): void {
|
||||
if (!this.localState) return;
|
||||
|
||||
@@ -32,6 +32,8 @@ export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
|
||||
* Default: false (uses JSON headers with Content-Type: application/json)
|
||||
*/
|
||||
authOnly?: boolean;
|
||||
/** Backend to target; defaults to the active one. */
|
||||
backendId?: string;
|
||||
/**
|
||||
* Additional headers to merge with default headers.
|
||||
*/
|
||||
@@ -59,10 +61,10 @@ export interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
|
||||
* ```
|
||||
*/
|
||||
export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}): Promise<T> {
|
||||
const { authOnly = false, headers: customHeaders, ...fetchOptions } = options;
|
||||
const baseHeaders = authOnly ? getAuthHeaders() : getJsonHeaders();
|
||||
const { authOnly = false, backendId, headers: customHeaders, ...fetchOptions } = options;
|
||||
const baseHeaders = authOnly ? getAuthHeaders(backendId) : getJsonHeaders(backendId);
|
||||
const headers = { ...baseHeaders, ...customHeaders };
|
||||
const url = apiUrl(path);
|
||||
const url = apiUrl(path, backendId);
|
||||
|
||||
let response;
|
||||
|
||||
@@ -105,7 +107,7 @@ export async function apiFetchWithParams<T>(
|
||||
params: Record<string, string>,
|
||||
options: ApiFetchOptions = {}
|
||||
): Promise<T> {
|
||||
const url = new URL(apiUrl(basePath), window.location.href);
|
||||
const url = new URL(apiUrl(basePath, options.backendId), window.location.href);
|
||||
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
|
||||
@@ -235,24 +235,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch router models when in router mode (for status and modalities)
|
||||
// Wait for models to be loaded first, run only once
|
||||
let routerModelsFetched = false;
|
||||
|
||||
$effect(() => {
|
||||
const isRouter = serverStore.isRouterMode;
|
||||
const modelsCount = modelsStore.models.length;
|
||||
|
||||
// Only fetch router models once when we have models loaded and in router mode
|
||||
if (isRouter && modelsCount > 0 && !routerModelsFetched) {
|
||||
routerModelsFetched = true;
|
||||
|
||||
untrack(() => {
|
||||
modelsStore.fetchRouterModels();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Live model status and load progress via the /models/sse feed (router mode).
|
||||
// The feed is kept for the session: switching to an external backend and back
|
||||
// must not tear it down and reconnect on every tab switch.
|
||||
|
||||
Reference in New Issue
Block a user