mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
ui : keep model state in memory across backend switches
Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
@@ -210,7 +210,7 @@
|
||||
? Math.round(modelLoadFraction(modelsStore.status.getLoadProgress(triggerModel)) * 100)
|
||||
: 0}
|
||||
|
||||
{#if ms.isMultiModel}
|
||||
{#if ms.isMultiModel || ms.switchingBackends}
|
||||
<DropdownMenu.Root bind:open={isOpen} onOpenChange={ms.handleOpenChange}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
@@ -251,7 +251,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if ms.updating || ms.isLoadingModel}
|
||||
{#if ms.updating || ms.isLoadingModel || ms.switchingBackends}
|
||||
<Loader2 class="h-3 w-3.5 shrink-0 animate-spin" />
|
||||
{:else}
|
||||
<ChevronDown class="h-3 w-3.5 shrink-0" />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { API_MODELS, LOCAL_BACKEND_ID } from '$lib/constants';
|
||||
import type { Backend, ModelOption } from '$lib/types';
|
||||
import type { ApiModelsListResponse, Backend, ModelOption } from '$lib/types';
|
||||
import { isAbortError } from '$lib/utils/abort';
|
||||
import { apiUrl } from '$lib/utils/api-base';
|
||||
import { getAuthHeadersForBackend } from '$lib/utils/api-headers';
|
||||
@@ -19,6 +19,8 @@ export interface BackendModelsResult {
|
||||
models: ModelOption[];
|
||||
ok: boolean;
|
||||
status: number | null;
|
||||
/** Untouched list payload of the local backend, kept so the router rows and their load statuses can be rebuilt without asking again. */
|
||||
raw?: ApiModelsListResponse;
|
||||
}
|
||||
|
||||
/** Outcome of a backend connectivity check. */
|
||||
@@ -64,8 +66,10 @@ export class BackendsService {
|
||||
const body = (await response.json()) as { data?: unknown };
|
||||
const entries = Array.isArray(body?.data) ? body.data : [];
|
||||
const models = entries.flatMap((entry) => normalizeBackendModel(entry));
|
||||
// only the local server needs its raw rows: they carry the load status
|
||||
const raw = backend.id === LOCAL_BACKEND_ID ? (body as ApiModelsListResponse) : undefined;
|
||||
|
||||
return { models, ok: true, status: response.status };
|
||||
return { models, ok: true, raw, status: response.status };
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) {
|
||||
return { models: [], ok: false, status: null };
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
* modelsStore and its status manager.
|
||||
*/
|
||||
|
||||
import { API_MODELS, MODEL_ID, type ModelSidecar, SIDECAR_TOKENS } from '$lib/constants';
|
||||
import {
|
||||
API_MODELS,
|
||||
LOCAL_BACKEND_ID,
|
||||
MODEL_ID,
|
||||
type ModelSidecar,
|
||||
SIDECAR_TOKENS
|
||||
} from '$lib/constants';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import type { ParsedModelId } from '$lib/types/models';
|
||||
import {
|
||||
@@ -346,8 +352,10 @@ export class ModelsService {
|
||||
|
||||
while (!signal.aborted) {
|
||||
try {
|
||||
const response = await fetch(apiUrl(API_MODELS.SSE), {
|
||||
headers: getAuthHeaders(),
|
||||
// the status feed only exists on the local llama.cpp server; pin the
|
||||
// request so an active external backend cannot redirect it
|
||||
const response = await fetch(apiUrl(API_MODELS.SSE, LOCAL_BACKEND_ID), {
|
||||
headers: getAuthHeaders(LOCAL_BACKEND_ID),
|
||||
signal
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { BackendsService } from '$lib/services/backends.service';
|
||||
import { backendsStore } from '$lib/stores/backends.svelte';
|
||||
import type { ApiModelsListResponse } from '$lib/types';
|
||||
import type { ModelOption } from '$lib/types/models';
|
||||
|
||||
export interface BackendModelsState {
|
||||
@@ -16,9 +17,16 @@ export interface BackendModelsState {
|
||||
loaded: boolean;
|
||||
loading: boolean;
|
||||
models: ModelOption[];
|
||||
/** Untouched list payload, kept for the local backend so its router rows survive a tab switch. */
|
||||
raw?: ApiModelsListResponse;
|
||||
}
|
||||
|
||||
const EMPTY_STATE: BackendModelsState = { error: null, loaded: false, loading: false, models: [] };
|
||||
const EMPTY_STATE: BackendModelsState = {
|
||||
error: null,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
models: []
|
||||
};
|
||||
|
||||
class BackendsModelsStore {
|
||||
private states = $state<Record<string, BackendModelsState>>({});
|
||||
@@ -47,7 +55,8 @@ class BackendsModelsStore {
|
||||
error: result.error ?? null,
|
||||
loaded: result.ok,
|
||||
loading: false,
|
||||
models: result.models
|
||||
models: result.models,
|
||||
raw: result.raw
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -404,46 +404,52 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop per-backend state when the active backend changes. The model list,
|
||||
* router rows and selection all belong to the previous backend, so they
|
||||
* must not leak into the next one. Refetches for the new backend.
|
||||
* 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.
|
||||
*/
|
||||
async switchBackend(): Promise<void> {
|
||||
this.status.unsubscribe();
|
||||
this.clearSelection();
|
||||
this.routerModels = [];
|
||||
this.error = null;
|
||||
|
||||
const backend = backendsStore.active;
|
||||
|
||||
// server props describe the local server; drop them only when the next
|
||||
// backend is not the one they describe, refresh in place otherwise
|
||||
if (backend.protocol !== 'llama.cpp') {
|
||||
// local props describe the server the UI is served from; keep them while
|
||||
// an external backend is active instead of dropping and refetching
|
||||
if (backend.protocol === 'llama.cpp') {
|
||||
serverStore.restoreLocalState();
|
||||
} else {
|
||||
serverStore.cacheLocalState();
|
||||
serverStore.clear();
|
||||
}
|
||||
|
||||
// prefer the prefetched list so switching does not refetch
|
||||
const cached = backendsModelsStore.get(backend.id);
|
||||
|
||||
if (cached.loaded) {
|
||||
this.activeModels = cached.models;
|
||||
this.loading = false;
|
||||
|
||||
await serverStore.fetch({ background: true });
|
||||
|
||||
// the cache carries names only; reload the router load status in place
|
||||
if (serverStore.isRouterMode) {
|
||||
await this.fetchRouterModels();
|
||||
}
|
||||
|
||||
if (this.activeModels.length > 0) {
|
||||
await this.ensureFirstModelSelected();
|
||||
}
|
||||
if (!cached.loaded) {
|
||||
// nothing prefetched for this backend (startup prefetch failed): load it once
|
||||
await this.fetch(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fetch(true);
|
||||
if (backend.protocol === 'llama.cpp' && !serverStore.props) {
|
||||
// first visit to the local tab in this session
|
||||
await serverStore.fetch({ background: true });
|
||||
}
|
||||
|
||||
this.activeModels = cached.models;
|
||||
this.loading = false;
|
||||
|
||||
// the local router rows carry the load statuses; the startup prefetch
|
||||
// already returned them, so a tab switch rebuilds the list from memory
|
||||
if (backend.protocol === 'llama.cpp' && this.routerModels.length === 0 && cached.raw) {
|
||||
this.routerModels = cached.raw.data;
|
||||
this.activeModels = this.buildModelOptions(cached.raw);
|
||||
}
|
||||
|
||||
if (this.activeModels.length > 0) {
|
||||
await this.ensureFirstModelSelected();
|
||||
}
|
||||
}
|
||||
|
||||
toDisplayName(id: string): string {
|
||||
|
||||
@@ -24,6 +24,9 @@ class ServerStore {
|
||||
status = $state<number | null>(null);
|
||||
private fetchBackendId: string | undefined;
|
||||
private fetchPromise: Promise<void> | null = null;
|
||||
/** Local server state kept alive while an external backend is active. */
|
||||
private localState: { props: ApiLlamaCppServerProps | null; role: ServerRole | null } | null =
|
||||
null;
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** Features of the active backend. Defaults to full llama.cpp support. */
|
||||
@@ -55,6 +58,14 @@ class ServerStore {
|
||||
return this.props?.ui_settings ?? this.props?.webui_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the local server state before switching to an external backend, so
|
||||
* switching back restores it instead of asking the server again.
|
||||
*/
|
||||
cacheLocalState(): void {
|
||||
this.localState = { props: this.props, role: this.role };
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.clearRetryTimer();
|
||||
this.props = null;
|
||||
@@ -145,6 +156,16 @@ class ServerStore {
|
||||
await promise;
|
||||
}
|
||||
|
||||
/** Restore the state kept by {@link cacheLocalState}; no request is made. */
|
||||
restoreLocalState(): void {
|
||||
if (!this.localState) return;
|
||||
|
||||
this.props = this.localState.props;
|
||||
this.role = this.localState.role;
|
||||
this.error = null;
|
||||
this.status = null;
|
||||
}
|
||||
|
||||
private clearRetryTimer(): void {
|
||||
if (this.retryTimer) {
|
||||
clearTimeout(this.retryTimer);
|
||||
|
||||
@@ -253,7 +253,9 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Live model status and load progress via the /models/sse feed (router mode)
|
||||
// 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.
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
|
||||
@@ -262,10 +264,6 @@
|
||||
untrack(() => {
|
||||
modelsStore.status.subscribe();
|
||||
});
|
||||
|
||||
return () => {
|
||||
modelsStore.status.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
// Background MCP server health checks on app load.
|
||||
|
||||
Reference in New Issue
Block a user