From b1cf8bb814a926eb78c356d681f3ee86fcdab0fa Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Nov 2025 14:05:42 +0100 Subject: [PATCH] refactor: Improve server properties management --- .../app/chat/ChatForm/ChatForm.svelte | 4 +- .../app/chat/ChatScreen/ChatScreen.svelte | 68 ++++-- .../chat/ChatScreen/ChatScreenWarning.svelte | 38 ---- .../webui/src/lib/components/app/index.ts | 1 - .../src/lib/constants/localstorage-keys.ts | 3 - tools/server/webui/src/lib/services/chat.ts | 15 +- .../webui/src/lib/stores/props.svelte.ts | 200 ++---------------- 7 files changed, 91 insertions(+), 238 deletions(-) delete mode 100644 tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreenWarning.svelte diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index bf74790a3f..b53e596da2 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -326,7 +326,9 @@
- {#if serverWarning()} - + {#if hasPropsError} +
+
+
+
+ + Server unavailable + — {propsError()} +
+ +
+
+
{/if}
-{:else if propsStore.error && !propsStore.serverProps} - - -{:else if isServerLoading || !propsStore.serverProps} - +{:else if isServerLoading} + {:else}
llama.cpp

- {propsStore.supportedModalities.includes(ModelModality.AUDIO) + {propsStore.serverProps?.modalities?.audio ? 'Record audio, type a message ' : 'Type a message'} or upload files to get started

- {#if serverWarning()} - + {#if hasPropsError} +
+
+
+
+ + Server unavailable + — {propsError()} +
+ +
+
+
{/if} -
+
- import { AlertTriangle, RefreshCw } from '@lucide/svelte'; - import { propsLoading, propsStore } from '$lib/stores/props.svelte'; - import { fly } from 'svelte/transition'; - - interface Props { - class?: string; - } - - let { class: className = '' }: Props = $props(); - - function handleRefreshServer() { - propsStore.fetch(); - } - - -
-
-
-
- -

- Server `/props` endpoint not available - using cached data -

-
- -
-
-
diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts index a41c83bdff..bcc1f41c53 100644 --- a/tools/server/webui/src/lib/components/app/index.ts +++ b/tools/server/webui/src/lib/components/app/index.ts @@ -25,7 +25,6 @@ export { default as ChatMessages } from './chat/ChatMessages/ChatMessages.svelte export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte'; export { default as ChatScreenHeader } from './chat/ChatScreen/ChatScreenHeader.svelte'; export { default as ChatScreenProcessingInfo } from './chat/ChatScreen/ChatScreenProcessingInfo.svelte'; -export { default as ChatScreenWarning } from './chat/ChatScreen/ChatScreenWarning.svelte'; export { default as ChatSettings } from './chat/ChatSettings/ChatSettings.svelte'; export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte'; diff --git a/tools/server/webui/src/lib/constants/localstorage-keys.ts b/tools/server/webui/src/lib/constants/localstorage-keys.ts index adba65568b..919b6ea06d 100644 --- a/tools/server/webui/src/lib/constants/localstorage-keys.ts +++ b/tools/server/webui/src/lib/constants/localstorage-keys.ts @@ -1,5 +1,2 @@ -export const SERVER_PROPS_LOCALSTORAGE_KEY = 'LlamaCppWebui.serverProps'; -export const SELECTED_MODEL_LOCALSTORAGE_KEY = 'LlamaCppWebui.selectedModel'; - export const CONFIG_LOCALSTORAGE_KEY = 'LlamaCppWebui.config'; export const USER_OVERRIDES_LOCALSTORAGE_KEY = 'LlamaCppWebui.userOverrides'; diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts index c781e3badc..49ec160db4 100644 --- a/tools/server/webui/src/lib/services/chat.ts +++ b/tools/server/webui/src/lib/services/chat.ts @@ -1,6 +1,6 @@ import { config } from '$lib/stores/settings.svelte'; import { selectedModelName } from '$lib/stores/models.svelte'; -import { isRouterMode } from '$lib/stores/props.svelte'; +import { isRouterMode, propsStore } from '$lib/stores/props.svelte'; import type { ApiChatCompletionRequest, ApiChatCompletionResponse, @@ -840,11 +840,24 @@ export class ChatService { * Extracts model name from Chat Completions API response data. * Handles various response formats including streaming chunks and final responses. * + * WORKAROUND: In single model mode, llama-server returns a default/incorrect model name + * in the response. We override it with the actual model name from propsStore. + * * @param data - Raw response data from the Chat Completions API * @returns Model name string if found, undefined otherwise * @private */ private extractModelName(data: unknown): string | undefined { + // WORKAROUND: In single model mode, use model name from props instead of API response + // because llama-server returns `gpt-3.5-turbo` value in the `model` field + const isRouter = isRouterMode(); + if (!isRouter) { + const propsModelName = propsStore.modelName; + if (propsModelName) { + return propsModelName; + } + } + const asRecord = (value: unknown): Record | undefined => { return typeof value === 'object' && value !== null ? (value as Record) diff --git a/tools/server/webui/src/lib/stores/props.svelte.ts b/tools/server/webui/src/lib/stores/props.svelte.ts index 156e7f8327..2d8bdb0ac0 100644 --- a/tools/server/webui/src/lib/stores/props.svelte.ts +++ b/tools/server/webui/src/lib/stores/props.svelte.ts @@ -1,5 +1,3 @@ -import { browser } from '$app/environment'; -import { SERVER_PROPS_LOCALSTORAGE_KEY } from '$lib/constants/localstorage-keys'; import { PropsService } from '$lib/services/props'; import { ServerRole, ModelModality } from '$lib/enums'; @@ -18,24 +16,12 @@ import { ServerRole, ModelModality } from '$lib/enums'; * - **Server Properties**: Model info, context size, build information * - **Mode Detection**: MODEL (single model) vs ROUTER (multi-model) * - **Capability Detection**: Vision and audio modality support - * - **Error Handling**: Graceful degradation with cached values - * - **Persistence**: LocalStorage caching for offline support + * - **Error Handling**: Clear error states when server unavailable */ class PropsStore { - constructor() { - if (!browser) return; - - const cachedProps = this.readCachedServerProps(); - if (cachedProps) { - this._serverProps = cachedProps; - this.detectServerRole(cachedProps); - } - } - private _serverProps = $state(null); private _loading = $state(false); private _error = $state(null); - private _serverWarning = $state(null); private _serverRole = $state(null); private fetchPromise: Promise | null = null; @@ -43,71 +29,6 @@ class PropsStore { private _modelPropsCache = $state>(new Map()); private _modelPropsFetching = $state>(new Set()); - // ───────────────────────────────────────────────────────────────────────────── - // LocalStorage persistence with fingerprint validation - // ───────────────────────────────────────────────────────────────────────────── - - /** - * Read cached server props from localStorage - * Note: Cache should be validated against fresh data using build_info fingerprint - */ - private readCachedServerProps(): ApiLlamaCppServerProps | null { - if (!browser) return null; - - try { - const raw = localStorage.getItem(SERVER_PROPS_LOCALSTORAGE_KEY); - if (!raw) return null; - - return JSON.parse(raw) as ApiLlamaCppServerProps; - } catch (error) { - console.warn('Failed to read cached server props from localStorage:', error); - return null; - } - } - - /** - * Persist server props to localStorage - */ - private persistServerProps(props: ApiLlamaCppServerProps | null): void { - if (!browser) return; - - try { - if (props) { - localStorage.setItem(SERVER_PROPS_LOCALSTORAGE_KEY, JSON.stringify(props)); - } else { - localStorage.removeItem(SERVER_PROPS_LOCALSTORAGE_KEY); - } - } catch (error) { - console.warn('Failed to persist server props to localStorage:', error); - } - } - - /** - * Validate cached props against fresh data using build_info fingerprint - * Returns true if cache is valid (same server instance) - */ - private isCacheValid(freshProps: ApiLlamaCppServerProps): boolean { - const cachedProps = this._serverProps; - if (!cachedProps) return true; // No cache to validate - - // Compare build_info - different build means server was restarted or updated - if (cachedProps.build_info !== freshProps.build_info) { - console.info( - 'Server build_info changed, invalidating cache', - `(${cachedProps.build_info} → ${freshProps.build_info})` - ); - return false; - } - - // Compare model_path - different model loaded means different configuration - if (cachedProps.model_path !== freshProps.model_path) { - console.info('Server model changed, invalidating cache'); - return false; - } - - return true; - } - // ───────────────────────────────────────────────────────────────────────────── // Getters - Server Properties // ───────────────────────────────────────────────────────────────────────────── @@ -124,10 +45,6 @@ class PropsStore { return this._error; } - get serverWarning(): string | null { - return this._serverWarning; - } - /** * Get model name from server props. * In MODEL mode: extracts from model_path or model_alias @@ -232,56 +149,39 @@ class PropsStore { /** * Fetches server properties from the server */ - async fetch(options: { silent?: boolean } = {}): Promise { - const { silent = false } = options; - const isSilent = silent && this._serverProps !== null; - + async fetch(): Promise { if (this.fetchPromise) { return this.fetchPromise; } - if (!isSilent) { - this._loading = true; - this._error = null; - this._serverWarning = null; - } + this._loading = true; + this._error = null; - const hadProps = this._serverProps !== null; + const previousBuildInfo = this._serverProps?.build_info; const fetchPromise = (async () => { try { const props = await PropsService.fetch(); - // Validate cache - if server was restarted, clear model-specific props cache - if (!this.isCacheValid(props)) { + // Clear model-specific props cache if server was restarted + if (previousBuildInfo && previousBuildInfo !== props.build_info) { this._modelPropsCache.clear(); - console.info('Cleared model props cache due to server change'); + console.info('Cleared model props cache due to server restart'); } this._serverProps = props; - this.persistServerProps(props); this._error = null; - this._serverWarning = null; - this.detectServerRole(props); } catch (error) { - if (isSilent && hadProps) { - console.warn('Silent server props refresh failed, keeping cached data:', error); - return; - } - - this.handleFetchError(error, hadProps); + this._error = this.getErrorMessage(error); + console.error('Error fetching server properties:', error); } finally { - if (!isSilent) { - this._loading = false; - } - + this._loading = false; this.fetchPromise = null; } })(); this.fetchPromise = fetchPromise; - await fetchPromise; } @@ -335,84 +235,30 @@ class PropsStore { // Error Handling // ───────────────────────────────────────────────────────────────────────────── - private handleFetchError(error: unknown, hadProps: boolean): void { - const { errorMessage, isOfflineLikeError, isServerSideError } = this.normalizeFetchError(error); - - let cachedProps: ApiLlamaCppServerProps | null = null; - - if (!hadProps) { - cachedProps = this.readCachedServerProps(); - - if (cachedProps) { - this._serverProps = cachedProps; - this.detectServerRole(cachedProps); - this._error = null; - - if (isOfflineLikeError || isServerSideError) { - this._serverWarning = errorMessage; - } - - console.warn( - 'Failed to refresh server properties, using cached values from localStorage:', - errorMessage - ); - } else { - this._error = errorMessage; - } - } else { - this._error = null; - - if (isOfflineLikeError || isServerSideError) { - this._serverWarning = errorMessage; - } - - console.warn( - 'Failed to refresh server properties, continuing with cached values:', - errorMessage - ); - } - - console.error('Error fetching server properties:', error); - } - - private normalizeFetchError(error: unknown): { - errorMessage: string; - isOfflineLikeError: boolean; - isServerSideError: boolean; - } { - let errorMessage = 'Failed to connect to server'; - let isOfflineLikeError = false; - let isServerSideError = false; - + private getErrorMessage(error: unknown): string { if (error instanceof Error) { const message = error.message || ''; if (error.name === 'TypeError' && message.includes('fetch')) { - errorMessage = 'Server is not running or unreachable'; - isOfflineLikeError = true; + return 'Server is not running or unreachable'; } else if (message.includes('ECONNREFUSED')) { - errorMessage = 'Connection refused - server may be offline'; - isOfflineLikeError = true; + return 'Connection refused - server may be offline'; } else if (message.includes('ENOTFOUND')) { - errorMessage = 'Server not found - check server address'; - isOfflineLikeError = true; + return 'Server not found - check server address'; } else if (message.includes('ETIMEDOUT')) { - errorMessage = 'Request timed out - the server took too long to respond'; - isOfflineLikeError = true; + return 'Request timed out'; } else if (message.includes('503')) { - errorMessage = 'Server temporarily unavailable - try again shortly'; - isServerSideError = true; + return 'Server temporarily unavailable'; } else if (message.includes('500')) { - errorMessage = 'Server error - check server logs'; - isServerSideError = true; + return 'Server error - check server logs'; } else if (message.includes('404')) { - errorMessage = 'Server endpoint not found'; + return 'Server endpoint not found'; } else if (message.includes('403') || message.includes('401')) { - errorMessage = 'Access denied'; + return 'Access denied'; } } - return { errorMessage, isOfflineLikeError, isServerSideError }; + return 'Failed to connect to server'; } // ───────────────────────────────────────────────────────────────────────────── @@ -425,11 +271,10 @@ class PropsStore { clear(): void { this._serverProps = null; this._error = null; - this._serverWarning = null; this._loading = false; this._serverRole = null; this.fetchPromise = null; - this.persistServerProps(null); + this._modelPropsCache.clear(); } } @@ -442,7 +287,6 @@ export const propsStore = new PropsStore(); export const serverProps = () => propsStore.serverProps; export const propsLoading = () => propsStore.loading; export const propsError = () => propsStore.error; -export const serverWarning = () => propsStore.serverWarning; export const modelName = () => propsStore.modelName; export const supportedModalities = () => propsStore.supportedModalities; export const supportsVision = () => propsStore.supportsVision;