mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-10-29 08:41:22 +00:00
Add server-driven parameter defaults and syncing (#16515)
This commit is contained in:
committed by
GitHub
parent
f4ce81c45e
commit
f9fb33f263
Binary file not shown.
@@ -14,8 +14,7 @@
|
|||||||
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
|
import { ChatSettingsFooter, ChatSettingsFields } from '$lib/components/app';
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||||
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
import { config, updateMultipleConfig } from '$lib/stores/settings.svelte';
|
||||||
import { config, updateMultipleConfig, resetConfig } from '$lib/stores/settings.svelte';
|
|
||||||
import { setMode } from 'mode-watcher';
|
import { setMode } from 'mode-watcher';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
@@ -267,16 +266,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
resetConfig();
|
localConfig = { ...config() };
|
||||||
|
|
||||||
localConfig = { ...SETTING_CONFIG_DEFAULT };
|
setMode(localConfig.theme as 'light' | 'dark' | 'system');
|
||||||
|
originalTheme = localConfig.theme as string;
|
||||||
setMode(SETTING_CONFIG_DEFAULT.theme as 'light' | 'dark' | 'system');
|
|
||||||
originalTheme = SETTING_CONFIG_DEFAULT.theme as string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
// Validate custom JSON if provided
|
|
||||||
if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
|
if (localConfig.custom && typeof localConfig.custom === 'string' && localConfig.custom.trim()) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(localConfig.custom);
|
JSON.parse(localConfig.custom);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { RotateCcw } from '@lucide/svelte';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import Label from '$lib/components/ui/label/label.svelte';
|
import Label from '$lib/components/ui/label/label.svelte';
|
||||||
@@ -6,6 +7,9 @@
|
|||||||
import { Textarea } from '$lib/components/ui/textarea';
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
|
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
|
||||||
import { supportsVision } from '$lib/stores/server.svelte';
|
import { supportsVision } from '$lib/stores/server.svelte';
|
||||||
|
import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
|
||||||
|
import { ParameterSyncService } from '$lib/services/parameter-sync';
|
||||||
|
import ParameterSourceIndicator from './ParameterSourceIndicator.svelte';
|
||||||
import type { Component } from 'svelte';
|
import type { Component } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -16,22 +20,77 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
|
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
|
||||||
|
|
||||||
|
// Helper function to get parameter source info for syncable parameters
|
||||||
|
function getParameterSourceInfo(key: string) {
|
||||||
|
if (!ParameterSyncService.canSyncParameter(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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 === 'input'}
|
{#if field.type === 'input'}
|
||||||
<Label for={field.key} class="block text-sm font-medium">
|
{@const paramInfo = getParameterSourceInfo(field.key)}
|
||||||
|
{@const currentValue = String(localConfig[field.key] ?? '')}
|
||||||
|
{@const propsDefault = paramInfo?.serverDefault}
|
||||||
|
{@const isCustomRealTime = (() => {
|
||||||
|
if (!paramInfo || propsDefault === undefined) return false;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Label for={field.key} class="text-sm font-medium">
|
||||||
{field.label}
|
{field.label}
|
||||||
</Label>
|
</Label>
|
||||||
|
{#if isCustomRealTime}
|
||||||
|
<ParameterSourceIndicator />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative w-full md:max-w-md">
|
||||||
<Input
|
<Input
|
||||||
id={field.key}
|
id={field.key}
|
||||||
value={String(localConfig[field.key] ?? '')}
|
value={currentValue}
|
||||||
onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
|
oninput={(e) => {
|
||||||
|
// Update local config immediately for real-time badge feedback
|
||||||
|
onConfigChange(field.key, e.currentTarget.value);
|
||||||
|
}}
|
||||||
placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
|
placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
|
||||||
class="w-full md:max-w-md"
|
class="w-full {isCustomRealTime ? 'pr-8' : ''}"
|
||||||
/>
|
/>
|
||||||
|
{#if isCustomRealTime}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
resetParameterToServerDefault(field.key);
|
||||||
|
// Trigger UI update by calling onConfigChange with the default value
|
||||||
|
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"
|
||||||
|
aria-label="Reset to default"
|
||||||
|
title="Reset to default"
|
||||||
|
>
|
||||||
|
<RotateCcw class="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if field.help || SETTING_CONFIG_INFO[field.key]}
|
{#if field.help || SETTING_CONFIG_INFO[field.key]}
|
||||||
<p class="mt-1 text-xs text-muted-foreground">
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
{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: 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 propsDefault = paramInfo?.serverDefault}
|
||||||
|
{@const isCustomRealTime = (() => {
|
||||||
|
if (!paramInfo || propsDefault === undefined) return false;
|
||||||
|
|
||||||
<Label for={field.key} class="block text-sm font-medium">
|
// For select fields, do direct comparison (no rounding needed)
|
||||||
|
return currentValue !== propsDefault;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Label for={field.key} class="text-sm font-medium">
|
||||||
{field.label}
|
{field.label}
|
||||||
</Label>
|
</Label>
|
||||||
|
{#if isCustomRealTime}
|
||||||
|
<ParameterSourceIndicator />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select.Root
|
<Select.Root
|
||||||
type="single"
|
type="single"
|
||||||
value={localConfig[field.key]}
|
value={currentValue}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (field.key === 'theme' && value && onThemeChange) {
|
if (field.key === 'theme' && value && onThemeChange) {
|
||||||
onThemeChange(value);
|
onThemeChange(value);
|
||||||
@@ -75,7 +148,8 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Select.Trigger class="w-full md:w-auto md:max-w-md">
|
<div class="relative w-full md:w-auto md:max-w-md">
|
||||||
|
<Select.Trigger class="w-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if selectedOption?.icon}
|
{#if selectedOption?.icon}
|
||||||
{@const IconComponent = selectedOption.icon}
|
{@const IconComponent = selectedOption.icon}
|
||||||
@@ -85,6 +159,23 @@
|
|||||||
{selectedOption?.label || `Select ${field.label.toLowerCase()}`}
|
{selectedOption?.label || `Select ${field.label.toLowerCase()}`}
|
||||||
</div>
|
</div>
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
|
{#if isCustomRealTime}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => {
|
||||||
|
resetParameterToServerDefault(field.key);
|
||||||
|
// Trigger UI update by calling onConfigChange with the default value
|
||||||
|
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"
|
||||||
|
aria-label="Reset to default"
|
||||||
|
title="Reset to default"
|
||||||
|
>
|
||||||
|
<RotateCcw class="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<Select.Content>
|
<Select.Content>
|
||||||
{#if field.options}
|
{#if field.options}
|
||||||
{#each field.options as option (option.value)}
|
{#each field.options as option (option.value)}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
import * as AlertDialog from '$lib/components/ui/alert-dialog';
|
||||||
|
import { forceSyncWithServerDefaults } from '$lib/stores/settings.svelte';
|
||||||
|
import { RotateCcw } from '@lucide/svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onReset?: () => void;
|
onReset?: () => void;
|
||||||
@@ -16,7 +18,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmReset() {
|
function handleConfirmReset() {
|
||||||
|
forceSyncWithServerDefaults();
|
||||||
onReset?.();
|
onReset?.();
|
||||||
|
|
||||||
showResetDialog = false;
|
showResetDialog = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +30,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between border-t border-border/30 p-6">
|
<div class="flex justify-between border-t border-border/30 p-6">
|
||||||
<Button variant="outline" onclick={handleResetClick}>Reset to default</Button>
|
<div class="flex gap-2">
|
||||||
|
<Button variant="outline" onclick={handleResetClick}>
|
||||||
|
<RotateCcw class="h-3 w-3" />
|
||||||
|
|
||||||
|
Reset to default
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button onclick={handleSave}>Save settings</Button>
|
<Button onclick={handleSave}>Save settings</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,8 +46,9 @@
|
|||||||
<AlertDialog.Header>
|
<AlertDialog.Header>
|
||||||
<AlertDialog.Title>Reset Settings to Default</AlertDialog.Title>
|
<AlertDialog.Title>Reset Settings to Default</AlertDialog.Title>
|
||||||
<AlertDialog.Description>
|
<AlertDialog.Description>
|
||||||
Are you sure you want to reset all settings to their default values? This action cannot be
|
Are you sure you want to reset all settings to their default values? This will reset all
|
||||||
undone and will permanently remove all your custom configurations.
|
parameters to the values provided by the server's /props endpoint and remove all your custom
|
||||||
|
configurations.
|
||||||
</AlertDialog.Description>
|
</AlertDialog.Description>
|
||||||
</AlertDialog.Header>
|
</AlertDialog.Header>
|
||||||
<AlertDialog.Footer>
|
<AlertDialog.Footer>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Wrench } from '@lucide/svelte';
|
||||||
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { class: className = '' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
class="h-5 bg-orange-100 px-1.5 py-0.5 text-xs text-orange-800 dark:bg-orange-900 dark:text-orange-200 {className}"
|
||||||
|
>
|
||||||
|
<Wrench class="mr-1 h-3 w-3" />
|
||||||
|
Custom
|
||||||
|
</Badge>
|
||||||
@@ -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 ChatSettingsDialog } from './chat/ChatSettings/ChatSettingsDialog.svelte';
|
||||||
export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte';
|
export { default as ChatSettingsFooter } from './chat/ChatSettings/ChatSettingsFooter.svelte';
|
||||||
export { default as ChatSettingsFields } from './chat/ChatSettings/ChatSettingsFields.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 ChatSidebar } from './chat/ChatSidebar/ChatSidebar.svelte';
|
||||||
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
|
export { default as ChatSidebarConversationItem } from './chat/ChatSidebar/ChatSidebarConversationItem.svelte';
|
||||||
|
|||||||
2
tools/server/webui/src/lib/constants/precision.ts
Normal file
2
tools/server/webui/src/lib/constants/precision.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const PRECISION_MULTIPLIER = 1000000;
|
||||||
|
export const PRECISION_DECIMAL_PLACES = 6;
|
||||||
135
tools/server/webui/src/lib/services/parameter-sync.spec.ts
Normal file
135
tools/server/webui/src/lib/services/parameter-sync.spec.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
202
tools/server/webui/src/lib/services/parameter-sync.ts
Normal file
202
tools/server/webui/src/lib/services/parameter-sync.ts
Normal file
@@ -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<string, ParameterValue>;
|
||||||
|
|
||||||
|
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<string, ParameterValue>)[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<string> = 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<string>
|
||||||
|
): 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<string, { current: ParameterValue; server: ParameterValue; differs: boolean }> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -125,6 +125,12 @@ class ServerStore {
|
|||||||
return this._slotsEndpointAvailable;
|
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
|
* 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 supportsVision = () => serverStore.supportsVision;
|
||||||
export const supportsAudio = () => serverStore.supportsAudio;
|
export const supportsAudio = () => serverStore.supportsAudio;
|
||||||
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
export const slotsEndpointAvailable = () => serverStore.slotsEndpointAvailable;
|
||||||
|
export const serverDefaultParams = () => serverStore.serverDefaultParams;
|
||||||
|
|||||||
@@ -33,11 +33,25 @@
|
|||||||
|
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
|
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 {
|
class SettingsStore {
|
||||||
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
|
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
|
||||||
theme = $state<string>('auto');
|
theme = $state<string>('auto');
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
|
userOverrides = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get server defaults with null safety
|
||||||
|
* Centralizes the pattern of getting and extracting server defaults
|
||||||
|
*/
|
||||||
|
private getServerDefaults(): Record<string, string | number | boolean> {
|
||||||
|
const serverParams = serverStore.serverDefaultParams;
|
||||||
|
return serverParams ? ParameterSyncService.extractServerDefaults(serverParams) : {};
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
@@ -67,14 +81,20 @@ class SettingsStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
|
const savedVal = JSON.parse(localStorage.getItem('config') || '{}');
|
||||||
|
|
||||||
// Merge with defaults to prevent breaking changes
|
// Merge with defaults to prevent breaking changes
|
||||||
this.config = {
|
this.config = {
|
||||||
...SETTING_CONFIG_DEFAULT,
|
...SETTING_CONFIG_DEFAULT,
|
||||||
...savedVal
|
...savedVal
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load user overrides
|
||||||
|
const savedOverrides = JSON.parse(localStorage.getItem('userOverrides') || '[]');
|
||||||
|
this.userOverrides = new Set(savedOverrides);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to parse config from localStorage, using defaults:', error);
|
console.warn('Failed to parse config from localStorage, using defaults:', error);
|
||||||
this.config = { ...SETTING_CONFIG_DEFAULT };
|
this.config = { ...SETTING_CONFIG_DEFAULT };
|
||||||
|
this.userOverrides = new Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +106,30 @@ class SettingsStore {
|
|||||||
|
|
||||||
this.theme = localStorage.getItem('theme') || 'auto';
|
this.theme = localStorage.getItem('theme') || 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a specific configuration setting
|
* Update a specific configuration setting
|
||||||
* @param key - The configuration key to update
|
* @param key - The configuration key to update
|
||||||
* @param value - The new value for the configuration key
|
* @param value - The new value for the configuration key
|
||||||
*/
|
*/
|
||||||
updateConfig<K extends keyof SettingsConfigType>(key: K, value: SettingsConfigType[K]) {
|
updateConfig<K extends keyof SettingsConfigType>(key: K, value: SettingsConfigType[K]): void {
|
||||||
this.config[key] = value;
|
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();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +139,26 @@ class SettingsStore {
|
|||||||
*/
|
*/
|
||||||
updateMultipleConfig(updates: Partial<SettingsConfigType>) {
|
updateMultipleConfig(updates: Partial<SettingsConfigType>) {
|
||||||
Object.assign(this.config, updates);
|
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();
|
this.saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +170,8 @@ class SettingsStore {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('config', JSON.stringify(this.config));
|
localStorage.setItem('config', JSON.stringify(this.config));
|
||||||
|
|
||||||
|
localStorage.setItem('userOverrides', JSON.stringify(Array.from(this.userOverrides)));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save config to localStorage:', error);
|
console.error('Failed to save config to localStorage:', error);
|
||||||
}
|
}
|
||||||
@@ -185,6 +243,129 @@ class SettingsStore {
|
|||||||
getAllConfig(): SettingsConfigType {
|
getAllConfig(): SettingsConfigType {
|
||||||
return { ...this.config };
|
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
|
// 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 resetAll = settingsStore.resetAll.bind(settingsStore);
|
||||||
export const getConfig = settingsStore.getConfig.bind(settingsStore);
|
export const getConfig = settingsStore.getConfig.bind(settingsStore);
|
||||||
export const getAllConfig = settingsStore.getAllConfig.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);
|
||||||
|
|||||||
53
tools/server/webui/src/lib/utils/config-helpers.ts
Normal file
53
tools/server/webui/src/lib/utils/config-helpers.ts
Normal file
@@ -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<T extends SettingsConfigType>(
|
||||||
|
config: T,
|
||||||
|
key: string,
|
||||||
|
value: unknown
|
||||||
|
): void {
|
||||||
|
if (key in config) {
|
||||||
|
(config as Record<string, unknown>)[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe helper to get config values dynamically
|
||||||
|
*/
|
||||||
|
export function getConfigValue<T extends SettingsConfigType>(
|
||||||
|
config: T,
|
||||||
|
key: string
|
||||||
|
): string | number | boolean | undefined {
|
||||||
|
const value = (config as Record<string, unknown>)[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<T extends SettingsConfigType>(
|
||||||
|
config: T,
|
||||||
|
keys: string[]
|
||||||
|
): Record<string, string | number | boolean> {
|
||||||
|
const record: Record<string, string | number | boolean> = {};
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = getConfigValue(config, key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
record[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
25
tools/server/webui/src/lib/utils/precision.ts
Normal file
25
tools/server/webui/src/lib/utils/precision.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
} from '$lib/stores/chat.svelte';
|
} from '$lib/stores/chat.svelte';
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||||
import { serverStore } from '$lib/stores/server.svelte';
|
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 { ModeWatcher } from 'mode-watcher';
|
||||||
import { Toaster } from 'svelte-sonner';
|
import { Toaster } from 'svelte-sonner';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
@@ -95,6 +95,15 @@
|
|||||||
serverStore.fetchServerProps();
|
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
|
// Monitor API key changes and redirect to error page if removed or changed when required
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const apiKey = config().apiKey;
|
const apiKey = config().apiKey;
|
||||||
|
|||||||
Reference in New Issue
Block a user