Skip to content

Webui dynamic config #13429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

ServeurpersoCom
Copy link

@ServeurpersoCom ServeurpersoCom commented May 10, 2025

Implemented dynamic config loading and reset behavior:
    - On startup, the app checks if localStorage.config exists and is non-empty. If not, it fetches defaults from the server.
    - The "Reset to default" button now properly:
        - fetches fresh config from the server,
        - updates localStorage,
        - calls saveConfig() to refresh the app state instantly.
    - Removed reliance on hardcoded CONFIG_DEFAULT. The server is now the single source of truth.

Copy link
Collaborator

@ngxson ngxson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doesn't work as-is, basically this introduces a weak point that may cause the whole app to crash in the future.

Also I already planned to work on this via #11717 , so I'll implement it using a proper approach

Comment on lines -12 to -13
// 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.
// Do not use nested objects, keep it single level. Prefix the key if you need to group them.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not delete existing comments

showTokensPerSecond: false,
showThoughtInProgress: false,
excludeThoughtOnReq: true,
// make sure these default values are in sync with `common.h`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment on lines -39 to -40
custom: '', // custom json-stringified object
// experimental features
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment on lines -83 to -90
// config keys having numeric value (i.e. temperature, top_k, top_p, etc)
export const CONFIG_NUMERIC_KEYS = Object.entries(CONFIG_DEFAULT)
.filter((e) => isNumeric(e[1]))
.map((e) => e[0]);
// list of themes supported by daisyui
export const THEMES = ['light', 'dark']
// make sure light & dark are always at the beginning
.concat(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Comment on lines +195 to +196
getConfig(): AppConfig {
return JSON.parse(localStorage.getItem('config') || '{}') as AppConfig;
Copy link
Collaborator

@ngxson ngxson May 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a weak typing. Basically the getConfig() is guaranteed to return AppConfig, but now there is a chance it will return empty object {}, which may cause undefined behavior or even crash the whole app if some code tries to read keys from that empty object.

@TeeAaTeeUu
Copy link

TeeAaTeeUu commented May 11, 2025

For me the main problem with the WebUI defaults is that those are always being sent. This seems to be trying to sync WebUI with the llama-server values, but still sending the params even if they would be matching the backend. Specifically these two parts:

const params = {
messages,
stream: true,
cache_prompt: true,
samplers: config.samplers,
temperature: config.temperature,
dynatemp_range: config.dynatemp_range,
dynatemp_exponent: config.dynatemp_exponent,
top_k: config.top_k,
top_p: config.top_p,
min_p: config.min_p,
typical_p: config.typical_p,
xtc_probability: config.xtc_probability,
xtc_threshold: config.xtc_threshold,
repeat_last_n: config.repeat_last_n,
repeat_penalty: config.repeat_penalty,
presence_penalty: config.presence_penalty,
frequency_penalty: config.frequency_penalty,
dry_multiplier: config.dry_multiplier,
dry_base: config.dry_base,
dry_allowed_length: config.dry_allowed_length,
dry_penalty_last_n: config.dry_penalty_last_n,
max_tokens: config.max_tokens,
timings_per_token: !!config.showTokensPerSecond,
...(config.custom.length ? JSON.parse(config.custom) : {}),
};

params.sampling.top_k = json_value(data, "top_k", defaults.sampling.top_k);
params.sampling.top_p = json_value(data, "top_p", defaults.sampling.top_p);
params.sampling.min_p = json_value(data, "min_p", defaults.sampling.min_p);
params.sampling.top_n_sigma = json_value(data, "top_n_sigma", defaults.sampling.top_n_sigma);
params.sampling.xtc_probability = json_value(data, "xtc_probability", defaults.sampling.xtc_probability);
params.sampling.xtc_threshold = json_value(data, "xtc_threshold", defaults.sampling.xtc_threshold);
params.sampling.typ_p = json_value(data, "typical_p", defaults.sampling.typ_p);
params.sampling.temp = json_value(data, "temperature", defaults.sampling.temp);
params.sampling.dynatemp_range = json_value(data, "dynatemp_range", defaults.sampling.dynatemp_range);
params.sampling.dynatemp_exponent = json_value(data, "dynatemp_exponent", defaults.sampling.dynatemp_exponent);
params.sampling.penalty_last_n = json_value(data, "repeat_last_n", defaults.sampling.penalty_last_n);
params.sampling.penalty_repeat = json_value(data, "repeat_penalty", defaults.sampling.penalty_repeat);
params.sampling.penalty_freq = json_value(data, "frequency_penalty", defaults.sampling.penalty_freq);
params.sampling.penalty_present = json_value(data, "presence_penalty", defaults.sampling.penalty_present);
params.sampling.dry_multiplier = json_value(data, "dry_multiplier", defaults.sampling.dry_multiplier);
params.sampling.dry_base = json_value(data, "dry_base", defaults.sampling.dry_base);
params.sampling.dry_allowed_length = json_value(data, "dry_allowed_length", defaults.sampling.dry_allowed_length);
params.sampling.dry_penalty_last_n = json_value(data, "dry_penalty_last_n", defaults.sampling.dry_penalty_last_n);
params.sampling.mirostat = json_value(data, "mirostat", defaults.sampling.mirostat);
params.sampling.mirostat_tau = json_value(data, "mirostat_tau", defaults.sampling.mirostat_tau);
params.sampling.mirostat_eta = json_value(data, "mirostat_eta", defaults.sampling.mirostat_eta);
params.sampling.seed = json_value(data, "seed", defaults.sampling.seed);
params.sampling.n_probs = json_value(data, "n_probs", defaults.sampling.n_probs);
params.sampling.min_keep = json_value(data, "min_keep", defaults.sampling.min_keep);
params.post_sampling_probs = json_value(data, "post_sampling_probs", defaults.post_sampling_probs);

I think it makes sense sync frontend and backend, but because it's not always possible to keep them in sync, allow NOT sending the defaults, to fully rely on the backend values. This would also better align with #13367 and similar, where WebUI might be sending requests to different models with different settings, and keeping multiple defaults in WebUI might become hard to maintain.

@ServeurpersoCom
Copy link
Author

ServeurpersoCom commented May 12, 2025

You're absolutely right — this is the core issue I ran into as well. The current behavior of always sending the full WebUI config overrides any server-side defaults, even when values haven’t been changed by the user. This breaks the ability to tune the server per model, which is especially important when exposing a shared WebUI for others to use.

Ideally, the WebUI should only send parameters that have been explicitly changed by the user. That way:

If a value matches the server default, it’s omitted from the request.

If a value differs (i.e. manually changed in the UI), it’s included as an override.

And there’s a Reset to server defaults button to wipe all local changes and fully re-sync with the backend.

To keep the frontend simple, when resetting a field, we could just clear it or show something like "default from server", allowing the backend to handle the fallback logic. That way, the WebUI doesn’t have to duplicate config logic — it only overrides selectively, when explicitly told to.

For example, if I deploy a well-tuned MoE model on the server, I want a friend using the WebUI to benefit from the correct sampling settings and get optimal performance out-of-the-box — not be stuck with unrelated or legacy settings hardcoded from the webui.

This would make the UI much easier to maintain, while preserving clean server-driven behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants