diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz
index c026f36c4844d..1c62ebe968605 100644
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte
index dc617afdcd4cd..d5d4c7fe3f34b 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte
@@ -14,8 +14,7 @@
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
import * as Dialog from '$lib/components/ui/dialog';
import { ScrollArea } from '$lib/components/ui/scroll-area';
- import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
- import { config, updateMultipleConfig, resetConfig } from '$lib/stores/settings.svelte';
+ import { config, updateMultipleConfig } from '$lib/stores/settings.svelte';
import { setMode } from 'mode-watcher';
import type { Component } from 'svelte';
@@ -267,16 +266,13 @@
}
function handleReset() {
- resetConfig();
+ localConfig = { ...config() };
- localConfig = { ...SETTING_CONFIG_DEFAULT };
-
- setMode(SETTING_CONFIG_DEFAULT.theme as 'light' | 'dark' | 'system');
- originalTheme = SETTING_CONFIG_DEFAULT.theme as string;
+ setMode(localConfig.theme as 'light' | 'dark' | 'system');
+ originalTheme = localConfig.theme as string;
}
function handleSave() {
- // Validate custom JSON if provided
if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
try {
JSON.parse(localConfig.custom);
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte
index e06399e0bc163..d17f7e4229af6 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte
@@ -1,4 +1,5 @@
{#each fields as field (field.key)}
{#if field.type === 'input'}
-
+ {@const paramInfo = getParameterSourceInfo(field.key)}
+ {@const currentValue = String(localConfig[field.key] ?? '')}
+ {@const propsDefault = paramInfo?.serverDefault}
+ {@const isCustomRealTime = (() => {
+ if (!paramInfo || propsDefault === undefined) return false;
-
onConfigChange(field.key, e.currentTarget.value)}
- placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
- class="w-full md:max-w-md"
- />
+ // Apply same rounding logic for real-time comparison
+ const inputValue = currentValue;
+ const numericInput = parseFloat(inputValue);
+ const normalizedInput = !isNaN(numericInput)
+ ? Math.round(numericInput * 1000000) / 1000000
+ : inputValue;
+ const normalizedDefault =
+ typeof propsDefault === 'number'
+ ? Math.round(propsDefault * 1000000) / 1000000
+ : propsDefault;
+
+ return normalizedInput !== normalizedDefault;
+ })()}
+
+
+
+ {#if isCustomRealTime}
+
+ {/if}
+
+
+
+ {
+ // Update local config immediately for real-time badge feedback
+ onConfigChange(field.key, e.currentTarget.value);
+ }}
+ placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
+ class="w-full {isCustomRealTime ? 'pr-8' : ''}"
+ />
+ {#if isCustomRealTime}
+
+ {/if}
+
{#if field.help || SETTING_CONFIG_INFO[field.key]}
{field.help || SETTING_CONFIG_INFO[field.key]}
@@ -59,14 +118,28 @@
(opt: { value: string; label: string; icon?: Component }) =>
opt.value === localConfig[field.key]
)}
+ {@const paramInfo = getParameterSourceInfo(field.key)}
+ {@const currentValue = localConfig[field.key]}
+ {@const propsDefault = paramInfo?.serverDefault}
+ {@const isCustomRealTime = (() => {
+ if (!paramInfo || propsDefault === undefined) return false;
-
+ // For select fields, do direct comparison (no rounding needed)
+ return currentValue !== propsDefault;
+ })()}
+
+
+
+ {#if isCustomRealTime}
+
+ {/if}
+
{
if (field.key === 'theme' && value && onThemeChange) {
onThemeChange(value);
@@ -75,16 +148,34 @@
}
}}
>
-
-
- {#if selectedOption?.icon}
- {@const IconComponent = selectedOption.icon}
-
- {/if}
-
- {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
-
-
+
+
+
+ {#if selectedOption?.icon}
+ {@const IconComponent = selectedOption.icon}
+
+ {/if}
+
+ {selectedOption?.label || `Select ${field.label.toLowerCase()}`}
+
+
+ {#if isCustomRealTime}
+
+ {/if}
+
{#if field.options}
{#each field.options as option (option.value)}
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFooter.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFooter.svelte
index 3408fe3ce4257..4f2d978ab8c19 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFooter.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFooter.svelte
@@ -1,6 +1,8 @@
-
+
+
+
@@ -36,8 +46,9 @@
Reset Settings to Default
- Are you sure you want to reset all settings to their default values? This action cannot be
- undone and will permanently remove all your custom configurations.
+ Are you sure you want to reset all settings to their default values? This will reset all
+ parameters to the values provided by the server's /props endpoint and remove all your custom
+ configurations.
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ParameterSourceIndicator.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ParameterSourceIndicator.svelte
new file mode 100644
index 0000000000000..b566985ba05c7
--- /dev/null
+++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ParameterSourceIndicator.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+ Custom
+
diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts
index 63a99f4343320..4c2cbdebe16eb 100644
--- a/tools/server/webui/src/lib/components/app/index.ts
+++ b/tools/server/webui/src/lib/components/app/index.ts
@@ -25,6 +25,7 @@ export { default as ChatScreen } from './chat/ChatScreen/ChatScreen.svelte';
export { default as ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte';
export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte';
export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.svelte';
+export { default as ParameterSourceIndicator } from './chat/ChatSettings/ParameterSourceIndicator.svelte';
export { default as ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
diff --git a/tools/server/webui/src/lib/constants/precision.ts b/tools/server/webui/src/lib/constants/precision.ts
new file mode 100644
index 0000000000000..8df5c4f966656
--- /dev/null
+++ b/tools/server/webui/src/lib/constants/precision.ts
@@ -0,0 +1,2 @@
+export const PRECISION_MULTIPLIER = 1000000;
+export const PRECISION_DECIMAL_PLACES = 6;
diff --git a/tools/server/webui/src/lib/services/parameter-sync.spec.ts b/tools/server/webui/src/lib/services/parameter-sync.spec.ts
new file mode 100644
index 0000000000000..9ced55faa0449
--- /dev/null
+++ b/tools/server/webui/src/lib/services/parameter-sync.spec.ts
@@ -0,0 +1,135 @@
+import { describe, it, expect } from 'vitest';
+import { ParameterSyncService } from './parameter-sync';
+import type { ApiLlamaCppServerProps } from '$lib/types/api';
+
+describe('ParameterSyncService', () => {
+ describe('roundFloatingPoint', () => {
+ it('should fix JavaScript floating-point precision issues', () => {
+ // Test the specific values from the screenshot
+ const mockServerParams = {
+ top_p: 0.949999988079071,
+ min_p: 0.009999999776482582,
+ temperature: 0.800000011920929,
+ top_k: 40,
+ samplers: ['top_k', 'typ_p', 'top_p', 'min_p', 'temperature']
+ };
+
+ const result = ParameterSyncService.extractServerDefaults({
+ ...mockServerParams,
+ // Add other required fields to match the API type
+ n_predict: 512,
+ seed: -1,
+ dynatemp_range: 0.0,
+ dynatemp_exponent: 1.0,
+ xtc_probability: 0.0,
+ xtc_threshold: 0.1,
+ typ_p: 1.0,
+ repeat_last_n: 64,
+ repeat_penalty: 1.0,
+ presence_penalty: 0.0,
+ frequency_penalty: 0.0,
+ dry_multiplier: 0.0,
+ dry_base: 1.75,
+ dry_allowed_length: 2,
+ dry_penalty_last_n: -1,
+ mirostat: 0,
+ mirostat_tau: 5.0,
+ mirostat_eta: 0.1,
+ stop: [],
+ max_tokens: -1,
+ n_keep: 0,
+ n_discard: 0,
+ ignore_eos: false,
+ stream: true,
+ logit_bias: [],
+ n_probs: 0,
+ min_keep: 0,
+ grammar: '',
+ grammar_lazy: false,
+ grammar_triggers: [],
+ preserved_tokens: [],
+ chat_format: '',
+ reasoning_format: '',
+ reasoning_in_content: false,
+ thinking_forced_open: false,
+ 'speculative.n_max': 0,
+ 'speculative.n_min': 0,
+ 'speculative.p_min': 0.0,
+ timings_per_token: false,
+ post_sampling_probs: false,
+ lora: [],
+ top_n_sigma: 0.0,
+ dry_sequence_breakers: []
+ } as ApiLlamaCppServerProps['default_generation_settings']['params']);
+
+ // Check that the problematic floating-point values are rounded correctly
+ expect(result.top_p).toBe(0.95);
+ expect(result.min_p).toBe(0.01);
+ expect(result.temperature).toBe(0.8);
+ expect(result.top_k).toBe(40); // Integer should remain unchanged
+ expect(result.samplers).toBe('top_k;typ_p;top_p;min_p;temperature');
+ });
+
+ it('should preserve non-numeric values', () => {
+ const mockServerParams = {
+ samplers: ['top_k', 'temperature'],
+ max_tokens: -1,
+ temperature: 0.7
+ };
+
+ const result = ParameterSyncService.extractServerDefaults({
+ ...mockServerParams,
+ // Minimal required fields
+ n_predict: 512,
+ seed: -1,
+ dynatemp_range: 0.0,
+ dynatemp_exponent: 1.0,
+ top_k: 40,
+ top_p: 0.95,
+ min_p: 0.05,
+ xtc_probability: 0.0,
+ xtc_threshold: 0.1,
+ typ_p: 1.0,
+ repeat_last_n: 64,
+ repeat_penalty: 1.0,
+ presence_penalty: 0.0,
+ frequency_penalty: 0.0,
+ dry_multiplier: 0.0,
+ dry_base: 1.75,
+ dry_allowed_length: 2,
+ dry_penalty_last_n: -1,
+ mirostat: 0,
+ mirostat_tau: 5.0,
+ mirostat_eta: 0.1,
+ stop: [],
+ n_keep: 0,
+ n_discard: 0,
+ ignore_eos: false,
+ stream: true,
+ logit_bias: [],
+ n_probs: 0,
+ min_keep: 0,
+ grammar: '',
+ grammar_lazy: false,
+ grammar_triggers: [],
+ preserved_tokens: [],
+ chat_format: '',
+ reasoning_format: '',
+ reasoning_in_content: false,
+ thinking_forced_open: false,
+ 'speculative.n_max': 0,
+ 'speculative.n_min': 0,
+ 'speculative.p_min': 0.0,
+ timings_per_token: false,
+ post_sampling_probs: false,
+ lora: [],
+ top_n_sigma: 0.0,
+ dry_sequence_breakers: []
+ } as ApiLlamaCppServerProps['default_generation_settings']['params']);
+
+ expect(result.samplers).toBe('top_k;temperature');
+ expect(result.max_tokens).toBe(-1);
+ expect(result.temperature).toBe(0.7);
+ });
+ });
+});
diff --git a/tools/server/webui/src/lib/services/parameter-sync.ts b/tools/server/webui/src/lib/services/parameter-sync.ts
new file mode 100644
index 0000000000000..ee147ae1941dc
--- /dev/null
+++ b/tools/server/webui/src/lib/services/parameter-sync.ts
@@ -0,0 +1,202 @@
+/**
+ * ParameterSyncService - Handles synchronization between server defaults and user settings
+ *
+ * This service manages the complex logic of merging server-provided default parameters
+ * with user-configured overrides, ensuring the UI reflects the actual server state
+ * while preserving user customizations.
+ *
+ * **Key Responsibilities:**
+ * - Extract syncable parameters from server props
+ * - Merge server defaults with user overrides
+ * - Track parameter sources (server, user, default)
+ * - Provide sync utilities for settings store integration
+ */
+
+import type { ApiLlamaCppServerProps } from '$lib/types/api';
+import { normalizeFloatingPoint } from '$lib/utils/precision';
+
+export type ParameterSource = 'default' | 'custom';
+export type ParameterValue = string | number | boolean;
+export type ParameterRecord = Record;
+
+export interface ParameterInfo {
+ value: string | number | boolean;
+ source: ParameterSource;
+ serverDefault?: string | number | boolean;
+ userOverride?: string | number | boolean;
+}
+
+export interface SyncableParameter {
+ key: string;
+ serverKey: string;
+ type: 'number' | 'string' | 'boolean';
+ canSync: boolean;
+}
+
+/**
+ * Mapping of webui setting keys to server parameter keys
+ * Only parameters that should be synced from server are included
+ */
+export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
+ { key: 'temperature', serverKey: 'temperature', type: 'number', canSync: true },
+ { key: 'top_k', serverKey: 'top_k', type: 'number', canSync: true },
+ { key: 'top_p', serverKey: 'top_p', type: 'number', canSync: true },
+ { key: 'min_p', serverKey: 'min_p', type: 'number', canSync: true },
+ { key: 'dynatemp_range', serverKey: 'dynatemp_range', type: 'number', canSync: true },
+ { key: 'dynatemp_exponent', serverKey: 'dynatemp_exponent', type: 'number', canSync: true },
+ { key: 'xtc_probability', serverKey: 'xtc_probability', type: 'number', canSync: true },
+ { key: 'xtc_threshold', serverKey: 'xtc_threshold', type: 'number', canSync: true },
+ { key: 'typ_p', serverKey: 'typ_p', type: 'number', canSync: true },
+ { key: 'repeat_last_n', serverKey: 'repeat_last_n', type: 'number', canSync: true },
+ { key: 'repeat_penalty', serverKey: 'repeat_penalty', type: 'number', canSync: true },
+ { key: 'presence_penalty', serverKey: 'presence_penalty', type: 'number', canSync: true },
+ { key: 'frequency_penalty', serverKey: 'frequency_penalty', type: 'number', canSync: true },
+ { key: 'dry_multiplier', serverKey: 'dry_multiplier', type: 'number', canSync: true },
+ { key: 'dry_base', serverKey: 'dry_base', type: 'number', canSync: true },
+ { key: 'dry_allowed_length', serverKey: 'dry_allowed_length', type: 'number', canSync: true },
+ { key: 'dry_penalty_last_n', serverKey: 'dry_penalty_last_n', type: 'number', canSync: true },
+ { key: 'max_tokens', serverKey: 'max_tokens', type: 'number', canSync: true },
+ { key: 'samplers', serverKey: 'samplers', type: 'string', canSync: true }
+];
+
+export class ParameterSyncService {
+ /**
+ * Round floating-point numbers to avoid JavaScript precision issues
+ */
+ private static roundFloatingPoint(value: ParameterValue): ParameterValue {
+ return normalizeFloatingPoint(value) as ParameterValue;
+ }
+
+ /**
+ * Extract server default parameters that can be synced
+ */
+ static extractServerDefaults(
+ serverParams: ApiLlamaCppServerProps['default_generation_settings']['params'] | null
+ ): ParameterRecord {
+ if (!serverParams) return {};
+
+ const extracted: ParameterRecord = {};
+
+ for (const param of SYNCABLE_PARAMETERS) {
+ if (param.canSync && param.serverKey in serverParams) {
+ const value = (serverParams as unknown as Record)[param.serverKey];
+ if (value !== undefined) {
+ // Apply precision rounding to avoid JavaScript floating-point issues
+ extracted[param.key] = this.roundFloatingPoint(value);
+ }
+ }
+ }
+
+ // Handle samplers array conversion to string
+ if (serverParams.samplers && Array.isArray(serverParams.samplers)) {
+ extracted.samplers = serverParams.samplers.join(';');
+ }
+
+ return extracted;
+ }
+
+ /**
+ * Merge server defaults with current user settings
+ * Returns updated settings that respect user overrides while using server defaults
+ */
+ static mergeWithServerDefaults(
+ currentSettings: ParameterRecord,
+ serverDefaults: ParameterRecord,
+ userOverrides: Set = new Set()
+ ): ParameterRecord {
+ const merged = { ...currentSettings };
+
+ for (const [key, serverValue] of Object.entries(serverDefaults)) {
+ // Only update if user hasn't explicitly overridden this parameter
+ if (!userOverrides.has(key)) {
+ merged[key] = this.roundFloatingPoint(serverValue);
+ }
+ }
+
+ return merged;
+ }
+
+ /**
+ * Get parameter information including source and values
+ */
+ static getParameterInfo(
+ key: string,
+ currentValue: ParameterValue,
+ propsDefaults: ParameterRecord,
+ userOverrides: Set
+ ): ParameterInfo {
+ const hasPropsDefault = propsDefaults[key] !== undefined;
+ const isUserOverride = userOverrides.has(key);
+
+ // Simple logic: either using default (from props) or custom (user override)
+ const source: ParameterSource = isUserOverride ? 'custom' : 'default';
+
+ return {
+ value: currentValue,
+ source,
+ serverDefault: hasPropsDefault ? propsDefaults[key] : undefined, // Keep same field name for compatibility
+ userOverride: isUserOverride ? currentValue : undefined
+ };
+ }
+
+ /**
+ * Check if a parameter can be synced from server
+ */
+ static canSyncParameter(key: string): boolean {
+ return SYNCABLE_PARAMETERS.some((param) => param.key === key && param.canSync);
+ }
+
+ /**
+ * Get all syncable parameter keys
+ */
+ static getSyncableParameterKeys(): string[] {
+ return SYNCABLE_PARAMETERS.filter((param) => param.canSync).map((param) => param.key);
+ }
+
+ /**
+ * Validate server parameter value
+ */
+ static validateServerParameter(key: string, value: ParameterValue): boolean {
+ const param = SYNCABLE_PARAMETERS.find((p) => p.key === key);
+ if (!param) return false;
+
+ switch (param.type) {
+ case 'number':
+ return typeof value === 'number' && !isNaN(value);
+ case 'string':
+ return typeof value === 'string';
+ case 'boolean':
+ return typeof value === 'boolean';
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Create a diff between current settings and server defaults
+ */
+ static createParameterDiff(
+ currentSettings: ParameterRecord,
+ serverDefaults: ParameterRecord
+ ): Record {
+ const diff: Record<
+ string,
+ { current: ParameterValue; server: ParameterValue; differs: boolean }
+ > = {};
+
+ for (const key of this.getSyncableParameterKeys()) {
+ const currentValue = currentSettings[key];
+ const serverValue = serverDefaults[key];
+
+ if (serverValue !== undefined) {
+ diff[key] = {
+ current: currentValue,
+ server: serverValue,
+ differs: currentValue !== serverValue
+ };
+ }
+ }
+
+ return diff;
+ }
+}
diff --git a/tools/server/webui/src/lib/stores/server.svelte.ts b/tools/server/webui/src/lib/stores/server.svelte.ts
index 0b6855404c7db..1fd4afb04022f 100644
--- a/tools/server/webui/src/lib/stores/server.svelte.ts
+++ b/tools/server/webui/src/lib/stores/server.svelte.ts
@@ -125,6 +125,12 @@ class ServerStore {
return this._slotsEndpointAvailable;
}
+ get serverDefaultParams():
+ | ApiLlamaCppServerProps['default_generation_settings']['params']
+ | null {
+ return this._serverProps?.default_generation_settings?.params || null;
+ }
+
/**
* Check if slots endpoint is available based on server properties and endpoint support
*/
@@ -273,3 +279,4 @@ export const supportedModalities = () => serverStore.supportedModalities;
export const supportsVision = () => serverStore.supportsVision;
export const supportsAudio = () => serverStore.supportsAudio;
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
+export const serverDefaultParams = () => serverStore.serverDefaultParams;
diff --git a/tools/server/webui/src/lib/stores/settings.svelte.ts b/tools/server/webui/src/lib/stores/settings.svelte.ts
index e5bc5ca9c91d0..b330cbb4bf42e 100644
--- a/tools/server/webui/src/lib/stores/settings.svelte.ts
+++ b/tools/server/webui/src/lib/stores/settings.svelte.ts
@@ -33,11 +33,25 @@
import { browser } from '$app/environment';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
+import { normalizeFloatingPoint } from '$lib/utils/precision';
+import { ParameterSyncService } from '$lib/services/parameter-sync';
+import { serverStore } from '$lib/stores/server.svelte';
+import { setConfigValue, getConfigValue, configToParameterRecord } from '$lib/utils/config-helpers';
class SettingsStore {
config = $state({ ...SETTING_CONFIG_DEFAULT });
theme = $state('auto');
isInitialized = $state(false);
+ userOverrides = $state>(new Set());
+
+ /**
+ * Helper method to get server defaults with null safety
+ * Centralizes the pattern of getting and extracting server defaults
+ */
+ private getServerDefaults(): Record {
+ const serverParams = serverStore.serverDefaultParams;
+ return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
+ }
constructor() {
if (browser) {
@@ -67,14 +81,20 @@ class SettingsStore {
try {
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
+
// Merge with defaults to prevent breaking changes
this.config = {
...SETTING_CONFIG_DEFAULT,
...savedVal
};
+
+ // Load user overrides
+ const savedOverrides = JSON.parse(localStorage.getItem('userOverrides') || '[]');
+ this.userOverrides = new Set(savedOverrides);
} catch (error) {
console.warn('Failed to parse config from localStorage, using defaults:', error);
this.config = { ...SETTING_CONFIG_DEFAULT };
+ this.userOverrides = new Set();
}
}
@@ -86,14 +106,30 @@ class SettingsStore {
this.theme = localStorage.getItem('theme') || 'auto';
}
-
/**
* Update a specific configuration setting
* @param key - The configuration key to update
* @param value - The new value for the configuration key
*/
- updateConfig(key: K, value: SettingsConfigType[K]) {
+ updateConfig(key: K, value: SettingsConfigType[K]): void {
this.config[key] = value;
+
+ if (ParameterSyncService.canSyncParameter(key as string)) {
+ const propsDefaults = this.getServerDefaults();
+ const propsDefault = propsDefaults[key as string];
+
+ if (propsDefault !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(value);
+ const normalizedDefault = normalizeFloatingPoint(propsDefault);
+
+ if (normalizedValue === normalizedDefault) {
+ this.userOverrides.delete(key as string);
+ } else {
+ this.userOverrides.add(key as string);
+ }
+ }
+ }
+
this.saveConfig();
}
@@ -103,6 +139,26 @@ class SettingsStore {
*/
updateMultipleConfig(updates: Partial) {
Object.assign(this.config, updates);
+
+ const propsDefaults = this.getServerDefaults();
+
+ for (const [key, value] of Object.entries(updates)) {
+ if (ParameterSyncService.canSyncParameter(key)) {
+ const propsDefault = propsDefaults[key];
+
+ if (propsDefault !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(value);
+ const normalizedDefault = normalizeFloatingPoint(propsDefault);
+
+ if (normalizedValue === normalizedDefault) {
+ this.userOverrides.delete(key);
+ } else {
+ this.userOverrides.add(key);
+ }
+ }
+ }
+ }
+
this.saveConfig();
}
@@ -114,6 +170,8 @@ class SettingsStore {
try {
localStorage.setItem('config', JSON.stringify(this.config));
+
+ localStorage.setItem('userOverrides', JSON.stringify(Array.from(this.userOverrides)));
} catch (error) {
console.error('Failed to save config to localStorage:', error);
}
@@ -185,6 +243,129 @@ class SettingsStore {
getAllConfig(): SettingsConfigType {
return { ...this.config };
}
+
+ /**
+ * Initialize settings with props defaults when server properties are first loaded
+ * This sets up the default values from /props endpoint
+ */
+ syncWithServerDefaults(): void {
+ const serverParams = serverStore.serverDefaultParams;
+ if (!serverParams) {
+ console.warn('No server parameters available for initialization');
+
+ return;
+ }
+
+ const propsDefaults = this.getServerDefaults();
+
+ for (const [key, propsValue] of Object.entries(propsDefaults)) {
+ const currentValue = getConfigValue(this.config, key);
+
+ const normalizedCurrent = normalizeFloatingPoint(currentValue);
+ const normalizedDefault = normalizeFloatingPoint(propsValue);
+
+ if (normalizedCurrent === normalizedDefault) {
+ this.userOverrides.delete(key);
+ setConfigValue(this.config, key, propsValue);
+ } else if (!this.userOverrides.has(key)) {
+ setConfigValue(this.config, key, propsValue);
+ }
+ }
+
+ this.saveConfig();
+ console.log('Settings initialized with props defaults:', propsDefaults);
+ console.log('Current user overrides after sync:', Array.from(this.userOverrides));
+ }
+
+ /**
+ * Clear all user overrides (for debugging)
+ */
+ clearAllUserOverrides(): void {
+ this.userOverrides.clear();
+ this.saveConfig();
+ console.log('Cleared all user overrides');
+ }
+
+ /**
+ * Reset all parameters to their default values (from props)
+ * This is used by the "Reset to Default" functionality
+ * Prioritizes server defaults from /props, falls back to webui defaults
+ */
+ forceSyncWithServerDefaults(): void {
+ const propsDefaults = this.getServerDefaults();
+ const syncableKeys = ParameterSyncService.getSyncableParameterKeys();
+
+ for (const key of syncableKeys) {
+ if (propsDefaults[key] !== undefined) {
+ const normalizedValue = normalizeFloatingPoint(propsDefaults[key]);
+
+ setConfigValue(this.config, key, normalizedValue);
+ } else {
+ if (key in SETTING_CONFIG_DEFAULT) {
+ const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
+
+ setConfigValue(this.config, key, defaultValue);
+ }
+ }
+
+ this.userOverrides.delete(key);
+ }
+
+ this.saveConfig();
+ }
+
+ /**
+ * Get parameter information including source for a specific parameter
+ */
+ getParameterInfo(key: string) {
+ const propsDefaults = this.getServerDefaults();
+ const currentValue = getConfigValue(this.config, key);
+
+ return ParameterSyncService.getParameterInfo(
+ key,
+ currentValue ?? '',
+ propsDefaults,
+ this.userOverrides
+ );
+ }
+
+ /**
+ * Reset a parameter to server default (or webui default if no server default)
+ */
+ resetParameterToServerDefault(key: string): void {
+ const serverDefaults = this.getServerDefaults();
+
+ if (serverDefaults[key] !== undefined) {
+ const value = normalizeFloatingPoint(serverDefaults[key]);
+
+ this.config[key as keyof SettingsConfigType] =
+ value as SettingsConfigType[keyof SettingsConfigType];
+ } else {
+ if (key in SETTING_CONFIG_DEFAULT) {
+ const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
+
+ setConfigValue(this.config, key, defaultValue);
+ }
+ }
+
+ this.userOverrides.delete(key);
+ this.saveConfig();
+ }
+
+ /**
+ * Get diff between current settings and server defaults
+ */
+ getParameterDiff() {
+ const serverDefaults = this.getServerDefaults();
+ if (Object.keys(serverDefaults).length === 0) return {};
+
+ const configAsRecord = configToParameterRecord(
+ this.config,
+ ParameterSyncService.getSyncableParameterKeys()
+ );
+
+ return ParameterSyncService.createParameterDiff(configAsRecord, serverDefaults);
+ }
}
// Create and export the settings store instance
@@ -204,3 +385,11 @@ export const resetTheme = settingsStore.resetTheme.bind(settingsStore);
export const resetAll = settingsStore.resetAll.bind(settingsStore);
export const getConfig = settingsStore.getConfig.bind(settingsStore);
export const getAllConfig = settingsStore.getAllConfig.bind(settingsStore);
+export const syncWithServerDefaults = settingsStore.syncWithServerDefaults.bind(settingsStore);
+export const forceSyncWithServerDefaults =
+ settingsStore.forceSyncWithServerDefaults.bind(settingsStore);
+export const getParameterInfo = settingsStore.getParameterInfo.bind(settingsStore);
+export const resetParameterToServerDefault =
+ settingsStore.resetParameterToServerDefault.bind(settingsStore);
+export const getParameterDiff = settingsStore.getParameterDiff.bind(settingsStore);
+export const clearAllUserOverrides = settingsStore.clearAllUserOverrides.bind(settingsStore);
diff --git a/tools/server/webui/src/lib/utils/config-helpers.ts b/tools/server/webui/src/lib/utils/config-helpers.ts
new file mode 100644
index 0000000000000..2d023f8d5c59f
--- /dev/null
+++ b/tools/server/webui/src/lib/utils/config-helpers.ts
@@ -0,0 +1,53 @@
+/**
+ * Type-safe configuration helpers
+ *
+ * Provides utilities for safely accessing and modifying configuration objects
+ * with dynamic keys while maintaining TypeScript type safety.
+ */
+
+import type { SettingsConfigType } from '$lib/types/settings';
+
+/**
+ * Type-safe helper to access config properties dynamically
+ * Provides better type safety than direct casting to Record
+ */
+export function setConfigValue(
+ config: T,
+ key: string,
+ value: unknown
+): void {
+ if (key in config) {
+ (config as Record)[key] = value;
+ }
+}
+
+/**
+ * Type-safe helper to get config values dynamically
+ */
+export function getConfigValue(
+ config: T,
+ key: string
+): string | number | boolean | undefined {
+ const value = (config as Record)[key];
+ return value as string | number | boolean | undefined;
+}
+
+/**
+ * Convert a SettingsConfigType to a ParameterRecord for specific keys
+ * Useful for parameter synchronization operations
+ */
+export function configToParameterRecord(
+ config: T,
+ keys: string[]
+): Record {
+ const record: Record = {};
+
+ for (const key of keys) {
+ const value = getConfigValue(config, key);
+ if (value !== undefined) {
+ record[key] = value;
+ }
+ }
+
+ return record;
+}
diff --git a/tools/server/webui/src/lib/utils/precision.ts b/tools/server/webui/src/lib/utils/precision.ts
new file mode 100644
index 0000000000000..6da200cf0b7ed
--- /dev/null
+++ b/tools/server/webui/src/lib/utils/precision.ts
@@ -0,0 +1,25 @@
+/**
+ * Floating-point precision utilities
+ *
+ * Provides functions to normalize floating-point numbers for consistent comparison
+ * and display, addressing JavaScript's floating-point precision issues.
+ */
+
+import { PRECISION_MULTIPLIER } from '$lib/constants/precision';
+
+/**
+ * Normalize floating-point numbers for consistent comparison
+ * Addresses JavaScript floating-point precision issues (e.g., 0.949999988079071 → 0.95)
+ */
+export function normalizeFloatingPoint(value: unknown): unknown {
+ return typeof value === 'number'
+ ? Math.round(value * PRECISION_MULTIPLIER) / PRECISION_MULTIPLIER
+ : value;
+}
+
+/**
+ * Type-safe version that only accepts numbers
+ */
+export function normalizeNumber(value: number): number {
+ return Math.round(value * PRECISION_MULTIPLIER) / PRECISION_MULTIPLIER;
+}
diff --git a/tools/server/webui/src/routes/+layout.svelte b/tools/server/webui/src/routes/+layout.svelte
index 0245cf3abcef4..8912f642ceffc 100644
--- a/tools/server/webui/src/routes/+layout.svelte
+++ b/tools/server/webui/src/routes/+layout.svelte
@@ -9,7 +9,7 @@
} from '$lib/stores/chat.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { serverStore } from '$lib/stores/server.svelte';
- import { config } from '$lib/stores/settings.svelte';
+ import { config, settingsStore } from '$lib/stores/settings.svelte';
import { ModeWatcher } from 'mode-watcher';
import { Toaster } from 'svelte-sonner';
import { goto } from '$app/navigation';
@@ -95,6 +95,15 @@
serverStore.fetchServerProps();
});
+ // Sync settings when server props are loaded
+ $effect(() => {
+ const serverProps = serverStore.serverProps;
+
+ if (serverProps?.default_generation_settings?.params) {
+ settingsStore.syncWithServerDefaults();
+ }
+ });
+
// Monitor API key changes and redirect to error page if removed or changed when required
$effect(() => {
const apiKey = config().apiKey;