Server becomes the source of truth for sampling parameter defaults (#20558)

* webui: make server the source of truth for sampling defaults

* webui: fix Custom badge for sampling parameters

* webui: log user overrides after server sync

* chore: update webui build output

* fix: Default values for sampling settings config object

* chore: update webui build output

---------

Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>
This commit is contained in:
Pascal
2026-03-19 13:20:39 +01:00
committed by GitHub
co-authored by Aleksander Grygier
parent 1e64534570
commit 4065c1a3a6
5 changed files with 69 additions and 88 deletions
Binary file not shown.
@@ -5,9 +5,12 @@
import Label from '$lib/components/ui/label/label.svelte'; import Label from '$lib/components/ui/label/label.svelte';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea'; import { Textarea } from '$lib/components/ui/textarea';
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants'; import { SETTING_CONFIG_INFO, SETTINGS_KEYS } from '$lib/constants';
import { SettingsFieldType } from '$lib/enums/settings'; import { SettingsFieldType } from '$lib/enums/settings';
import { settingsStore } from '$lib/stores/settings.svelte'; import { settingsStore } from '$lib/stores/settings.svelte';
import { serverStore } from '$lib/stores/server.svelte';
import { modelsStore, selectedModelName } from '$lib/stores/models.svelte';
import { normalizeFloatingPoint } from '$lib/utils/precision';
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app'; import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
import type { Component } from 'svelte'; import type { Component } from 'svelte';
@@ -20,35 +23,36 @@
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props(); let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
// Helper function to get parameter source info for syncable parameters // server sampling defaults for placeholders
function getParameterSourceInfo(key: string) { let sp = $derived.by(() => {
if (!settingsStore.canSyncParameter(key)) { if (serverStore.isRouterMode) {
return null; const m = selectedModelName();
if (m) {
const p = modelsStore.getModelProps(m);
return (p?.default_generation_settings?.params ?? {}) as Record<string, unknown>;
}
} }
return (serverStore.defaultParams ?? {}) as Record<string, unknown>;
return settingsStore.getParameterInfo(key); });
}
</script> </script>
{#each fields as field (field.key)} {#each fields as field (field.key)}
<div class="space-y-2"> <div class="space-y-2">
{#if field.type === SettingsFieldType.INPUT} {#if field.type === SettingsFieldType.INPUT}
{@const paramInfo = getParameterSourceInfo(field.key)}
{@const currentValue = String(localConfig[field.key] ?? '')} {@const currentValue = String(localConfig[field.key] ?? '')}
{@const propsDefault = paramInfo?.serverDefault} {@const serverDefault = sp[field.key]}
{@const isCustomRealTime = (() => { {@const isCustomRealTime = (() => {
if (!paramInfo || propsDefault === undefined) return false; if (serverDefault == null) return false;
if (currentValue === '') return false;
// Apply same rounding logic for real-time comparison const numericInput = parseFloat(currentValue);
const inputValue = currentValue;
const numericInput = parseFloat(inputValue);
const normalizedInput = !isNaN(numericInput) const normalizedInput = !isNaN(numericInput)
? Math.round(numericInput * 1000000) / 1000000 ? Math.round(numericInput * 1000000) / 1000000
: inputValue; : currentValue;
const normalizedDefault = const normalizedDefault =
typeof propsDefault === 'number' typeof serverDefault === 'number'
? Math.round(propsDefault * 1000000) / 1000000 ? Math.round(serverDefault * 1000000) / 1000000
: propsDefault; : serverDefault;
return normalizedInput !== normalizedDefault; return normalizedInput !== normalizedDefault;
})()} })()}
@@ -74,7 +78,9 @@
// Update local config immediately for real-time badge feedback // Update local config immediately for real-time badge feedback
onConfigChange(field.key, e.currentTarget.value); onConfigChange(field.key, e.currentTarget.value);
}} }}
placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} placeholder={sp[field.key] != null
? `Default: ${normalizeFloatingPoint(sp[field.key])}`
: ''}
class="w-full {isCustomRealTime ? 'pr-8' : ''}" class="w-full {isCustomRealTime ? 'pr-8' : ''}"
/> />
{#if isCustomRealTime} {#if isCustomRealTime}
@@ -82,9 +88,7 @@
type="button" type="button"
onclick={() => { onclick={() => {
settingsStore.resetParameterToServerDefault(field.key); settingsStore.resetParameterToServerDefault(field.key);
// Trigger UI update by calling onConfigChange with the default value onConfigChange(field.key, '');
const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
onConfigChange(field.key, String(defaultValue));
}} }}
class="absolute top-1/2 right-2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted" class="absolute top-1/2 right-2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
aria-label="Reset to default" aria-label="Reset to default"
@@ -112,7 +116,7 @@
id={field.key} id={field.key}
value={String(localConfig[field.key] ?? '')} value={String(localConfig[field.key] ?? '')}
onchange={(e) => onConfigChange(field.key, e.currentTarget.value)} onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`} placeholder=""
class="min-h-[10rem] w-full md:max-w-2xl" class="min-h-[10rem] w-full md:max-w-2xl"
/> />
@@ -140,14 +144,12 @@
(opt: { value: string; label: string; icon?: Component }) => (opt: { value: string; label: string; icon?: Component }) =>
opt.value === localConfig[field.key] opt.value === localConfig[field.key]
)} )}
{@const paramInfo = getParameterSourceInfo(field.key)}
{@const currentValue = localConfig[field.key]} {@const currentValue = localConfig[field.key]}
{@const propsDefault = paramInfo?.serverDefault} {@const serverDefault = sp[field.key]}
{@const isCustomRealTime = (() => { {@const isCustomRealTime = (() => {
if (!paramInfo || propsDefault === undefined) return false; if (serverDefault == null) return false;
if (currentValue === '' || currentValue === undefined) return false;
// For select fields, do direct comparison (no rounding needed) return currentValue !== serverDefault;
return currentValue !== propsDefault;
})()} })()}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -190,9 +192,7 @@
type="button" type="button"
onclick={() => { onclick={() => {
settingsStore.resetParameterToServerDefault(field.key); settingsStore.resetParameterToServerDefault(field.key);
// Trigger UI update by calling onConfigChange with the default value onConfigChange(field.key, '');
const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
onConfigChange(field.key, String(defaultValue));
}} }}
class="absolute top-1/2 right-8 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted" class="absolute top-1/2 right-8 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded transition-colors hover:bg-muted"
aria-label="Reset to default" aria-label="Reset to default"
@@ -1,8 +1,8 @@
import { ColorMode } from '$lib/enums/ui'; import { ColorMode } from '$lib/enums/ui';
import { Monitor, Moon, Sun } from '@lucide/svelte'; import { Monitor, Moon, Sun } from '@lucide/svelte';
export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> = { export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean | undefined> = {
// Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value. Do not use null or undefined for default value. // Note: in order not to introduce breaking changes, please keep the same data type (number, string, etc) if you want to change the default value.
// Do not use nested objects, keep it single level. Prefix the key if you need to group them. // Do not use nested objects, keep it single level. Prefix the key if you need to group them.
apiKey: '', apiKey: '',
systemMessage: '', systemMessage: '',
@@ -30,27 +30,30 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
agenticMaxToolPreviewLines: 25, agenticMaxToolPreviewLines: 25,
showToolCallInProgress: false, showToolCallInProgress: false,
alwaysShowAgenticTurns: false, alwaysShowAgenticTurns: false,
// make sure these default values are in sync with `common.h` // sampling params: empty means "use server default"
samplers: 'top_k;typ_p;top_p;min_p;temperature', // the server / preset is the source of truth
// empty values are shown as placeholders from /props in the UI
// and are NOT sent in API requests, letting the server decide
samplers: '',
backend_sampling: false, backend_sampling: false,
temperature: 0.8, temperature: undefined,
dynatemp_range: 0.0, dynatemp_range: undefined,
dynatemp_exponent: 1.0, dynatemp_exponent: undefined,
top_k: 40, top_k: undefined,
top_p: 0.95, top_p: undefined,
min_p: 0.05, min_p: undefined,
xtc_probability: 0.0, xtc_probability: undefined,
xtc_threshold: 0.1, xtc_threshold: undefined,
typ_p: 1.0, typ_p: undefined,
repeat_last_n: 64, repeat_last_n: undefined,
repeat_penalty: 1.0, repeat_penalty: undefined,
presence_penalty: 0.0, presence_penalty: undefined,
frequency_penalty: 0.0, frequency_penalty: undefined,
dry_multiplier: 0.0, dry_multiplier: undefined,
dry_base: 1.75, dry_base: undefined,
dry_allowed_length: 2, dry_allowed_length: undefined,
dry_penalty_last_n: -1, dry_penalty_last_n: undefined,
max_tokens: -1, max_tokens: undefined,
custom: '', // custom json-stringified object custom: '', // custom json-stringified object
// experimental features // experimental features
pyInterpreterEnabled: false, pyInterpreterEnabled: false,
@@ -289,16 +289,10 @@ class SettingsStore {
const serverDefaults = this.getServerDefaults(); const serverDefaults = this.getServerDefaults();
if (serverDefaults[key] !== undefined) { if (serverDefaults[key] !== undefined) {
const value = normalizeFloatingPoint(serverDefaults[key]); // sampling param known by server: clear it, let server decide
setConfigValue(this.config, key, '');
this.config[key as keyof SettingsConfigType] = } else if (key in SETTING_CONFIG_DEFAULT) {
value as SettingsConfigType[keyof SettingsConfigType]; setConfigValue(this.config, key, getConfigValue(SETTING_CONFIG_DEFAULT, key));
} else {
if (key in SETTING_CONFIG_DEFAULT) {
const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
setConfigValue(this.config, key, defaultValue);
}
} }
this.userOverrides.delete(key); this.userOverrides.delete(key);
@@ -319,12 +313,7 @@ class SettingsStore {
*/ */
syncWithServerDefaults(): void { syncWithServerDefaults(): void {
const propsDefaults = this.getServerDefaults(); const propsDefaults = this.getServerDefaults();
if (Object.keys(propsDefaults).length === 0) return;
if (Object.keys(propsDefaults).length === 0) {
console.warn('No server defaults available for initialization');
return;
}
for (const [key, propsValue] of Object.entries(propsDefaults)) { for (const [key, propsValue] of Object.entries(propsDefaults)) {
const currentValue = getConfigValue(this.config, key); const currentValue = getConfigValue(this.config, key);
@@ -332,17 +321,14 @@ class SettingsStore {
const normalizedCurrent = normalizeFloatingPoint(currentValue); const normalizedCurrent = normalizeFloatingPoint(currentValue);
const normalizedDefault = normalizeFloatingPoint(propsValue); const normalizedDefault = normalizeFloatingPoint(propsValue);
// if user value matches server, it's not a real override
if (normalizedCurrent === normalizedDefault) { if (normalizedCurrent === normalizedDefault) {
this.userOverrides.delete(key); this.userOverrides.delete(key);
setConfigValue(this.config, key, propsValue);
} else if (!this.userOverrides.has(key)) {
setConfigValue(this.config, key, propsValue);
} }
} }
this.saveConfig(); this.saveConfig();
console.log('Settings initialized with props defaults:', propsDefaults); console.log('User overrides after sync:', Array.from(this.userOverrides));
console.log('Current user overrides after sync:', Array.from(this.userOverrides));
} }
/** /**
@@ -352,19 +338,11 @@ class SettingsStore {
*/ */
forceSyncWithServerDefaults(): void { forceSyncWithServerDefaults(): void {
const propsDefaults = this.getServerDefaults(); const propsDefaults = this.getServerDefaults();
const syncableKeys = ParameterSyncService.getSyncableParameterKeys(); for (const key of ParameterSyncService.getSyncableParameterKeys()) {
for (const key of syncableKeys) {
if (propsDefaults[key] !== undefined) { if (propsDefaults[key] !== undefined) {
const normalizedValue = normalizeFloatingPoint(propsDefaults[key]); setConfigValue(this.config, key, '');
} else if (key in SETTING_CONFIG_DEFAULT) {
setConfigValue(this.config, key, normalizedValue); setConfigValue(this.config, key, getConfigValue(SETTING_CONFIG_DEFAULT, key));
} else {
if (key in SETTING_CONFIG_DEFAULT) {
const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
setConfigValue(this.config, key, defaultValue);
}
} }
this.userOverrides.delete(key); this.userOverrides.delete(key);
+1 -1
View File
@@ -5,7 +5,7 @@ import type { DatabaseMessageExtra } from './database';
import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums'; import type { ParameterSource, SyncableParameterType, SettingsFieldType } from '$lib/enums';
import type { Icon } from '@lucide/svelte'; import type { Icon } from '@lucide/svelte';
export type SettingsConfigValue = string | number | boolean; export type SettingsConfigValue = string | number | boolean | undefined;
export interface SettingsFieldConfig { export interface SettingsFieldConfig {
key: string; key: string;