Skip to content

Commit 1a18927

Browse files
Allow viewing conversations even when llama server is down (#16255)
* webui: allow viewing conversations and sending messages even if llama-server is down - Cached llama.cpp server properties in browser localStorage on startup, persisting successful fetches and reloading them when refresh attempts fail so the chat UI continues to render while the backend is unavailable. - Cleared the stored server properties when resetting the store to prevent stale capability data after cache-backed operation. - Kept the original error-splash behavior when no cached props exist so fresh installs still surface a clear failure state instead of rendering stale data. * feat: Add UI for `props` endpoint unavailable + cleanup logic * webui: extend cached props fallback to offline errors Treat connection failures (refused, DNS, timeout, fetch) the same way as server 5xx so the warning banner shows up when cache is available, instead of falling back to a full error screen. * webui: Left the chat form enabled when a server warning is present so operators can keep sending messages e.g., to restart the backend over llama-swap, even while cached /props data is in use * chore: update webui build output --------- Co-authored-by: Pascal <[email protected]>
1 parent e0539eb commit 1a18927

File tree

7 files changed

+149
-6
lines changed

7 files changed

+149
-6
lines changed

tools/server/public/index.html.gz

985 Bytes
Binary file not shown.

tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import {
44
ChatForm,
55
ChatScreenHeader,
6+
ChatScreenWarning,
67
ChatMessages,
78
ChatProcessingInfo,
89
EmptyFileAlertDialog,
10+
ServerErrorSplash,
911
ServerInfo,
1012
ServerLoadingSplash,
1113
ConfirmationDialog
@@ -29,6 +31,7 @@
2931
supportsVision,
3032
supportsAudio,
3133
serverLoading,
34+
serverWarning,
3235
serverStore
3336
} from '$lib/stores/server.svelte';
3437
import { contextService } from '$lib/services';
@@ -303,6 +306,10 @@
303306
>
304307
<ChatProcessingInfo />
305308

309+
{#if serverWarning()}
310+
<ChatScreenWarning class="pointer-events-auto mx-auto max-w-[48rem] px-4" />
311+
{/if}
312+
306313
<div class="conversation-chat-form pointer-events-auto rounded-t-3xl pb-4">
307314
<ChatForm
308315
isLoading={isLoading()}
@@ -319,6 +326,8 @@
319326
{:else if isServerLoading}
320327
<!-- Server Loading State -->
321328
<ServerLoadingSplash />
329+
{:else if serverStore.error && !serverStore.modelName}
330+
<ServerErrorSplash error={serverStore.error} />
322331
{:else if serverStore.modelName}
323332
<div
324333
aria-label="Welcome screen with file drop zone"
@@ -340,6 +349,10 @@
340349
<ServerInfo />
341350
</div>
342351

352+
{#if serverWarning()}
353+
<ChatScreenWarning />
354+
{/if}
355+
343356
<div in:fly={{ y: 10, duration: 250, delay: 300 }}>
344357
<ChatForm
345358
isLoading={isLoading()}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script lang="ts">
2+
import { AlertTriangle, RefreshCw } from '@lucide/svelte';
3+
import { serverLoading, serverStore } from '$lib/stores/server.svelte';
4+
import { fly } from 'svelte/transition';
5+
6+
interface Props {
7+
class?: string;
8+
}
9+
10+
let { class: className = '' }: Props = $props();
11+
12+
function handleRefreshServer() {
13+
serverStore.fetchServerProps();
14+
}
15+
</script>
16+
17+
<div class="mb-3 {className}" in:fly={{ y: 10, duration: 250 }}>
18+
<div
19+
class="rounded-md border border-yellow-200 bg-yellow-50 px-3 py-2 dark:border-yellow-800 dark:bg-yellow-950"
20+
>
21+
<div class="flex items-center justify-between">
22+
<div class="flex items-center">
23+
<AlertTriangle class="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
24+
<p class="ml-2 text-sm text-yellow-800 dark:text-yellow-200">
25+
Server `/props` endpoint not available - using cached data
26+
</p>
27+
</div>
28+
<button
29+
onclick={handleRefreshServer}
30+
disabled={serverLoading()}
31+
class="ml-3 flex items-center gap-1.5 rounded bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-800 hover:bg-yellow-200 disabled:opacity-50 dark:bg-yellow-900 dark:text-yellow-200 dark:hover:bg-yellow-800"
32+
>
33+
<RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
34+
{serverLoading() ? 'Checking...' : 'Retry'}
35+
</button>
36+
</div>
37+
</div>
38+
</div>

tools/server/webui/src/lib/components/app/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export { default as MessageBranchingControls } from './chat/ChatMessages/ChatMes
1919
export { default as ChatProcessingInfo } from './chat/ChatProcessingInfo.svelte';
2020

2121
export { default as ChatScreenHeader } from './chat/ChatScreen/ChatScreenHeader.svelte';
22+
export { default as ChatScreenWarning } from './chat/ChatScreen/ChatScreenWarning.svelte';
2223
export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte';
2324

2425
export { default as ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SERVER_PROPS_LOCALSTORAGE_KEY = 'LlamaCppWebui.serverProps';

tools/server/webui/src/lib/stores/server.svelte.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { browser } from '$app/environment';
2+
import { SERVER_PROPS_LOCALSTORAGE_KEY } from '$lib/constants/localstorage-keys';
13
import { ChatService } from '$lib/services/chat';
24
import { config } from '$lib/stores/settings.svelte';
35

@@ -34,12 +36,51 @@ import { config } from '$lib/stores/settings.svelte';
3436
* - Slots endpoint availability (for processing state monitoring)
3537
* - Context window size and token limits
3638
*/
39+
3740
class ServerStore {
41+
constructor() {
42+
if (!browser) return;
43+
44+
const cachedProps = this.readCachedServerProps();
45+
if (cachedProps) {
46+
this._serverProps = cachedProps;
47+
}
48+
}
49+
3850
private _serverProps = $state<ApiLlamaCppServerProps | null>(null);
3951
private _loading = $state(false);
4052
private _error = $state<string | null>(null);
53+
private _serverWarning = $state<string | null>(null);
4154
private _slotsEndpointAvailable = $state<boolean | null>(null);
4255

56+
private readCachedServerProps(): ApiLlamaCppServerProps | null {
57+
if (!browser) return null;
58+
59+
try {
60+
const raw = localStorage.getItem(SERVER_PROPS_LOCALSTORAGE_KEY);
61+
if (!raw) return null;
62+
63+
return JSON.parse(raw) as ApiLlamaCppServerProps;
64+
} catch (error) {
65+
console.warn('Failed to read cached server props from localStorage:', error);
66+
return null;
67+
}
68+
}
69+
70+
private persistServerProps(props: ApiLlamaCppServerProps | null): void {
71+
if (!browser) return;
72+
73+
try {
74+
if (props) {
75+
localStorage.setItem(SERVER_PROPS_LOCALSTORAGE_KEY, JSON.stringify(props));
76+
} else {
77+
localStorage.removeItem(SERVER_PROPS_LOCALSTORAGE_KEY);
78+
}
79+
} catch (error) {
80+
console.warn('Failed to persist server props to localStorage:', error);
81+
}
82+
}
83+
4384
get serverProps(): ApiLlamaCppServerProps | null {
4485
return this._serverProps;
4586
}
@@ -52,6 +93,10 @@ class ServerStore {
5293
return this._error;
5394
}
5495

96+
get serverWarning(): string | null {
97+
return this._serverWarning;
98+
}
99+
55100
get modelName(): string | null {
56101
if (!this._serverProps?.model_path) return null;
57102
return this._serverProps.model_path.split(/(\\|\/)/).pop() || null;
@@ -123,38 +168,81 @@ class ServerStore {
123168
async fetchServerProps(): Promise<void> {
124169
this._loading = true;
125170
this._error = null;
171+
this._serverWarning = null;
126172

127173
try {
128174
console.log('Fetching server properties...');
129175
const props = await ChatService.getServerProps();
130176
this._serverProps = props;
177+
this.persistServerProps(props);
131178
console.log('Server properties loaded:', props);
132179

133180
// Check slots endpoint availability after server props are loaded
134181
await this.checkSlotsEndpointAvailability();
135182
} catch (error) {
183+
const hadCachedProps = this._serverProps !== null;
136184
let errorMessage = 'Failed to connect to server';
185+
let isOfflineLikeError = false;
186+
let isServerSideError = false;
137187

138188
if (error instanceof Error) {
139189
// Handle specific error types with user-friendly messages
140190
if (error.name === 'TypeError' && error.message.includes('fetch')) {
141191
errorMessage = 'Server is not running or unreachable';
192+
isOfflineLikeError = true;
142193
} else if (error.message.includes('ECONNREFUSED')) {
143194
errorMessage = 'Connection refused - server may be offline';
195+
isOfflineLikeError = true;
144196
} else if (error.message.includes('ENOTFOUND')) {
145197
errorMessage = 'Server not found - check server address';
198+
isOfflineLikeError = true;
146199
} else if (error.message.includes('ETIMEDOUT')) {
147200
errorMessage = 'Connection timeout - server may be overloaded';
201+
isOfflineLikeError = true;
202+
} else if (error.message.includes('503')) {
203+
errorMessage = 'Server temporarily unavailable - try again shortly';
204+
isServerSideError = true;
148205
} else if (error.message.includes('500')) {
149206
errorMessage = 'Server error - check server logs';
207+
isServerSideError = true;
150208
} else if (error.message.includes('404')) {
151209
errorMessage = 'Server endpoint not found';
152210
} else if (error.message.includes('403') || error.message.includes('401')) {
153211
errorMessage = 'Access denied';
154212
}
155213
}
156214

157-
this._error = errorMessage;
215+
let cachedProps: ApiLlamaCppServerProps | null = null;
216+
217+
if (!hadCachedProps) {
218+
cachedProps = this.readCachedServerProps();
219+
if (cachedProps) {
220+
this._serverProps = cachedProps;
221+
this._error = null;
222+
223+
if (isOfflineLikeError || isServerSideError) {
224+
this._serverWarning = errorMessage;
225+
}
226+
227+
console.warn(
228+
'Failed to refresh server properties, using cached values from localStorage:',
229+
errorMessage
230+
);
231+
} else {
232+
this._error = errorMessage;
233+
}
234+
} else {
235+
this._error = null;
236+
237+
if (isOfflineLikeError || isServerSideError) {
238+
this._serverWarning = errorMessage;
239+
}
240+
241+
console.warn(
242+
'Failed to refresh server properties, continuing with cached values:',
243+
errorMessage
244+
);
245+
}
158246
console.error('Error fetching server properties:', error);
159247
} finally {
160248
this._loading = false;
@@ -167,8 +255,10 @@ class ServerStore {
167255
clear(): void {
168256
this._serverProps = null;
169257
this._error = null;
258+
this._serverWarning = null;
170259
this._loading = false;
171260
this._slotsEndpointAvailable = null;
261+
this.persistServerProps(null);
172262
}
173263
}
174264

@@ -177,6 +267,7 @@ export const serverStore = new ServerStore();
177267
export const serverProps = () => serverStore.serverProps;
178268
export const serverLoading = () => serverStore.loading;
179269
export const serverError = () => serverStore.error;
270+
export const serverWarning = () => serverStore.serverWarning;
180271
export const modelName = () => serverStore.modelName;
181272
export const supportedModalities = () => serverStore.supportedModalities;
182273
export const supportsVision = () => serverStore.supportsVision;

tools/server/webui/src/lib/utils/api-key-validation.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
2727
if (!response.ok) {
2828
if (response.status === 401 || response.status === 403) {
2929
throw error(401, 'Access denied');
30-
} else if (response.status >= 500) {
31-
throw error(response.status, 'Server error - check if llama.cpp server is running');
32-
} else {
33-
throw error(response.status, `Server responded with status ${response.status}`);
3430
}
31+
32+
console.warn(`Server responded with status ${response.status} during API key validation`);
33+
return;
3534
}
3635
} catch (err) {
3736
// If it's already a SvelteKit error, re-throw it
@@ -40,6 +39,6 @@ export async function validateApiKey(fetch: typeof globalThis.fetch): Promise<vo
4039
}
4140

4241
// Network or other errors
43-
throw error(503, 'Cannot connect to server - check if llama.cpp server is running');
42+
console.warn('Cannot connect to server for API key validation:', err);
4443
}
4544
}

0 commit comments

Comments
 (0)