feat: Preset reset on Default, expanded fields & cross-modal nav
- Reset presets to system defaults when "Default" is clicked in Configure Models (resetGlobalPresets() + backend reset action) - Expand preset fields to full config parity: ctx_length, ctx_history/ctx_input, vision, rate limits, kwargs, browser_http_headers - Extract _normalizePresets() helper inside store to eliminate duplication - Hide "Edit Presets" link when Per-Chat Override is disabled - Add Settings + API Keys navigation buttons to Model Presets page - Add dynamic import guard for plugin-settings-store.js in cross-modal navigation buttons - Rename model switcher label from "Default" to "Default LLM"
keyboardstaff committed
Mar 19, 2026 at 08:16 UTC
da7e33549c9a3ab635aa22f5d8b4e60300f29a4b
8 files changed
+241
-23
plugins/_model_config/api/model_presets.py
+4
@@ -17,4 +17,8 @@ class ModelPresets(ApiHandler):
17
model_config.save_presets(presets)
18
return {"ok": True, "presets": presets}
19
20
+ elif action == "reset":
21
+ presets = model_config.reset_presets()
22
+ return {"ok": True, "presets": presets}
23
+
24
return Response(status=400, response=f"Unknown action: {action}")
plugins/_model_config/default_presets.yaml
+10
@@ -4,19 +4,29 @@
4
name: "openai/gpt-5.2-chat"
5
api_key: ""
6
api_base: ""
7
+ ctx_length: 128000
8
+ ctx_history: 0.7
9
+ vision: true
10
utility:
11
provider: "openrouter"
12
name: "openai/gpt-5-nano"
13
api_key: ""
14
api_base: ""
15
+ ctx_length: 128000
16
+ ctx_input: 0.7
17
- name: "Intelligence"
18
chat:
19
provider: "openrouter"
20
name: "anthropic/claude-opus-4.6"
21
api_key: ""
22
api_base: ""
23
+ ctx_length: 128000
24
+ ctx_history: 0.7
25
+ vision: true
26
utility:
27
provider: "openrouter"
28
name: "google/gemini-3-flash-preview"
29
api_key: ""
30
api_base: ""
31
+ ctx_length: 128000
32
+ ctx_input: 0.7
plugins/_model_config/extensions/webui/chat-nav-after/model-switcher.html
+2
-2
@@ -81,7 +81,7 @@
81
openModal('/plugins/_model_config/webui/main.html');
82
showDropdown = false;
83
">
84
- <span class="material-symbols-outlined" style="font-size: 14px;">settings</span>
84
+ <span class="material-symbols-outlined" style="font-size: 14px;">tune</span>
85
<span>Edit Presets</span>
86
</div>
87
</div>
@@ -113,7 +113,7 @@
113
border: none;
114
}
115
.model-switcher-btn.has-override {
116
- color: var(--color-text);
116
+ /* color: var(--color-text); */
117
}
118
.model-switcher-label {
119
max-width: 160px;
plugins/_model_config/helpers/model_config.py
+9
@@ -1,3 +1,4 @@
1
+import os
2
import models
3
from helpers import plugins, files
4
from helpers import yaml as yaml_helper
@@ -50,6 +51,14 @@ def save_presets(presets: list) -> None:
51
files.write_file(path, yaml_helper.dumps(presets))
52
53
54
+def reset_presets() -> list:
55
+ """Delete user presets so get_presets() falls back to bundled defaults. Returns the default presets."""
56
+ path = _get_presets_path()
57
+ if os.path.exists(path):
58
+ os.remove(path)
59
+ return get_presets()
60
+
61
+
62
def get_preset_by_name(name: str) -> dict | None:
63
"""Find a preset by name from the global presets list."""
64
for p in get_presets():
plugins/_model_config/webui/config.html
+22
@@ -11,6 +11,14 @@
11
x-init="
12
await $store.modelConfig.ensureLoaded();
13
$store.modelConfig.initConfigFields(config);
14
+ const _origReset = context.resetToDefault.bind(context);
15
+ context.resetToDefault = async () => {
16
+ const before = context.settings;
17
+ await _origReset();
18
+ if (context.settings !== before) {
19
+ await $store.modelConfig.resetGlobalPresets();
20
+ }
21
+ };
22
">
23
<template x-if="config && $store.modelConfig._loaded">
24
<div class="model-config-sections">
@@ -32,6 +40,13 @@
40
</label>
41
</div>
42
</div>
43
+
44
+ <div class="override-presets-link" x-show="config.chat_model.allow_chat_override">
45
+ <button class="text-button" @click="openModal('/plugins/_model_config/webui/main.html')">
46
+ <span class="material-symbols-outlined" style="font-size:15px;">tune</span>
47
+ <span>Edit Presets</span>
48
+ </button>
49
+ </div>
50
</div>
51
52
<!-- Model Sections (Main, Utility, Embedding) -->
@@ -280,6 +295,13 @@
295
.model-section .section-title {
296
margin-top: 0;
297
}
298
+ .override-presets-link {
299
+ margin-top: 4px;
300
+ }
301
+ .override-presets-link .text-button {
302
+ font-size: 0.78rem;
303
+ gap: 4px;
304
+ }
305
.eye-toggle {
306
position: absolute;
307
right: 8px;
plugins/_model_config/webui/main.html
+151
-13
@@ -97,7 +97,7 @@
97
<div class="field">
98
<div class="field-label">
99
<div class="field-title">API key</div>
100
- <div class="field-description">Leave empty to use the default API key for this provider.</div>
100
+ <div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
101
</div>
102
<div class="field-control" style="position:relative;" x-data="{ showKey: false, _revealed: '' }">
103
<input :type="showKey ? 'text' : 'password'" x-model="preset.chat.api_key" autocomplete="off"
@@ -123,6 +123,76 @@
123
</div>
124
<div class="field-control"><input type="text" x-model="preset.chat.api_base" /></div>
125
</div>
126
+ <div class="field">
127
+ <div class="field-label">
128
+ <div class="field-title">Context length</div>
129
+ <div class="field-description">Maximum number of tokens in the context window. System prompt, chat history, RAG and response all count towards this limit.</div>
130
+ </div>
131
+ <div class="field-control"><input type="number" x-model.number="preset.chat.ctx_length" /></div>
132
+ </div>
133
+ <div class="field">
134
+ <div class="field-label">
135
+ <div class="field-title">Context window space for chat history</div>
136
+ <div class="field-description">Portion of context window dedicated to chat history visible to the agent. Smaller size will result in shorter and more summarized history.</div>
137
+ </div>
138
+ <div class="field-control">
139
+ <input type="range" min="0.01" max="1" step="0.01" x-model.number="preset.chat.ctx_history" />
140
+ <span class="range-value" x-text="preset.chat.ctx_history || ''"></span>
141
+ </div>
142
+ </div>
143
+ <div class="field">
144
+ <div class="field-label">
145
+ <div class="field-title">Supports Vision</div>
146
+ <div class="field-description">Models capable of Vision can for example natively see the content of image attachments.</div>
147
+ </div>
148
+ <div class="field-control">
149
+ <label class="toggle">
150
+ <input type="checkbox" x-model="preset.chat.vision" />
151
+ <span class="toggler"></span>
152
+ </label>
153
+ </div>
154
+ </div>
155
+ <div class="field">
156
+ <div class="field-label">
157
+ <div class="field-title">Requests per minute limit</div>
158
+ <div class="field-description">Limits the number of requests per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
159
+ </div>
160
+ <div class="field-control"><input type="number" x-model.number="preset.chat.rl_requests" /></div>
161
+ </div>
162
+ <div class="field">
163
+ <div class="field-label">
164
+ <div class="field-title">Input tokens per minute limit</div>
165
+ <div class="field-description">Limits the number of input tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
166
+ </div>
167
+ <div class="field-control"><input type="number" x-model.number="preset.chat.rl_input" /></div>
168
+ </div>
169
+ <div class="field">
170
+ <div class="field-label">
171
+ <div class="field-title">Output tokens per minute limit</div>
172
+ <div class="field-description">Limits the number of output tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
173
+ </div>
174
+ <div class="field-control"><input type="number" x-model.number="preset.chat.rl_output" /></div>
175
+ </div>
176
+ <div class="field field-full">
177
+ <div class="field-label">
178
+ <div class="field-title">Additional parameters</div>
179
+ <div class="field-description">Any other parameters supported by <a href='https://docs.litellm.ai/docs/set_keys' target='_blank'>LiteLLM</a>. Format is KEY=VALUE on individual lines. Value can be JSON objects; unquoted is treated as object/number, quoted as string.</div>
180
+ </div>
181
+ <div class="field-control">
182
+ <textarea x-model="preset.chat._kwargs_text"
183
+ @change="preset.chat.kwargs = $store.modelConfig.textToKwargs(preset.chat._kwargs_text)"></textarea>
184
+ </div>
185
+ </div>
186
+ <div class="field field-full">
187
+ <div class="field-label">
188
+ <div class="field-title">Browser HTTP Headers</div>
189
+ <div class="field-description">Custom HTTP headers sent with browser requests. The browser agent uses the main model. Format is KEY=VALUE, one per line.</div>
190
+ </div>
191
+ <div class="field-control">
192
+ <textarea x-model="preset.chat._browser_headers_text"
193
+ @change="preset.chat.browser_http_headers = $store.modelConfig.textToHeaders(preset.chat._browser_headers_text)"></textarea>
194
+ </div>
195
+ </div>
196
197
<div class="preset-subheader">Utility Model <span style="opacity:0.5; font-size:0.75rem;">(optional — falls back to the configured Utility Model)</span></div>
198
<div class="field">
@@ -176,7 +246,7 @@
246
<div class="field">
247
<div class="field-label">
248
<div class="field-title">API key</div>
179
- <div class="field-description">Leave empty to use the default API key for this provider.</div>
249
+ <div class="field-description">Authentication key for this provider. Shared across all model slots using the same provider.</div>
250
</div>
251
<div class="field-control" style="position:relative;" x-data="{ showKey: false, _revealed: '' }">
252
<input :type="showKey ? 'text' : 'password'" x-model="preset.utility.api_key" autocomplete="off"
@@ -203,20 +273,84 @@
273
</div>
274
<div class="field-control"><input type="text" x-model="preset.utility.api_base" /></div>
275
</div>
276
+ <div class="field">
277
+ <div class="field-label">
278
+ <div class="field-title">Context length</div>
279
+ <div class="field-description">Maximum number of tokens in the context window. System prompt, chat history, RAG and response all count towards this limit.</div>
280
+ </div>
281
+ <div class="field-control"><input type="number" x-model.number="preset.utility.ctx_length" /></div>
282
+ </div>
283
+ <div class="field">
284
+ <div class="field-label">
285
+ <div class="field-title">Context window space for utility input</div>
286
+ <div class="field-description">Portion of context window used for utility model input messages.</div>
287
+ </div>
288
+ <div class="field-control">
289
+ <input type="range" min="0.01" max="1" step="0.01" x-model.number="preset.utility.ctx_input" />
290
+ <span class="range-value" x-text="preset.utility.ctx_input || ''"></span>
291
+ </div>
292
+ </div>
293
+ <div class="field">
294
+ <div class="field-label">
295
+ <div class="field-title">Requests per minute limit</div>
296
+ <div class="field-description">Limits the number of requests per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
297
+ </div>
298
+ <div class="field-control"><input type="number" x-model.number="preset.utility.rl_requests" /></div>
299
+ </div>
300
+ <div class="field">
301
+ <div class="field-label">
302
+ <div class="field-title">Input tokens per minute limit</div>
303
+ <div class="field-description">Limits the number of input tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
304
+ </div>
305
+ <div class="field-control"><input type="number" x-model.number="preset.utility.rl_input" /></div>
306
+ </div>
307
+ <div class="field">
308
+ <div class="field-label">
309
+ <div class="field-title">Output tokens per minute limit</div>
310
+ <div class="field-description">Limits the number of output tokens per minute. Waits if the limit is exceeded. Set to 0 to disable.</div>
311
+ </div>
312
+ <div class="field-control"><input type="number" x-model.number="preset.utility.rl_output" /></div>
313
+ </div>
314
+ <div class="field field-full">
315
+ <div class="field-label">
316
+ <div class="field-title">Additional parameters</div>
317
+ <div class="field-description">Any other parameters supported by <a href='https://docs.litellm.ai/docs/set_keys' target='_blank'>LiteLLM</a>. Format is KEY=VALUE on individual lines. Value can be JSON objects; unquoted is treated as object/number, quoted as string.</div>
318
+ </div>
319
+ <div class="field-control">
320
+ <textarea x-model="preset.utility._kwargs_text"
321
+ @change="preset.utility.kwargs = $store.modelConfig.textToKwargs(preset.utility._kwargs_text)"></textarea>
322
+ </div>
323
+ </div>
324
</div>
325
</div>
326
</template>
327
210
- <button class="text-button preset-add-btn"
211
- @click="
212
- presets = [...presets, {
213
- name: 'Preset ' + (presets.length + 1),
214
- chat: { provider: '', name: '', api_key: '', api_base: '' },
215
- utility: { provider: '', name: '', api_key: '', api_base: '' }
216
- }]">
217
- <span class="material-symbols-outlined" style="font-size:16px;">add</span>
218
- <span>Add Preset</span>
219
- </button>
328
+ <div class="presets-actions">
329
+ <button class="text-button preset-add-btn"
330
+ @click="
331
+ presets = [...presets, {
332
+ name: 'Preset ' + (presets.length + 1),
333
+ chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, browser_http_headers: {}, _kwargs_text: '', _browser_headers_text: '' },
334
+ utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: '' }
335
+ }]">
336
+ <span class="material-symbols-outlined" style="font-size:16px;">add</span>
337
+ <span>Add Preset</span>
338
+ </button>
339
+ <button class="text-button" @click="(async () => {
340
+ await import('/components/plugins/plugin-settings-store.js');
341
+ const pluginMeta = { name: '_model_config', always_enabled: true, per_project_config: true, per_agent_config: true, has_config_screen: true };
342
+ if ($store.pluginToggle?.open) await $store.pluginToggle.open(pluginMeta);
343
+ await $store.pluginSettingsPrototype.open('_model_config', { perProjectConfig: true, perAgentConfig: true });
344
+ openModal('components/plugins/plugin-settings.html');
345
+ })()">
346
+ <span class="material-symbols-outlined" style="font-size:15px;">settings</span>
347
+ <span>Settings</span>
348
+ </button>
349
+ <button class="text-button" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
350
+ <span class="material-symbols-outlined" style="font-size:15px;">key</span>
351
+ <span>API Keys</span>
352
+ </button>
353
+ </div>
354
355
<div class="presets-footer">
356
<button class="button" @click="$store.modelConfig.saveGlobalPresets(presets)">
@@ -290,7 +424,11 @@
424
padding-top: 8px;
425
border-top: 1px dashed var(--color-border);
426
}
293
- .preset-add-btn {
427
+ .presets-actions {
428
+ display: flex;
429
+ gap: 12px;
430
+ flex-wrap: wrap;
431
+ align-items: center;
432
margin-top: 4px;
433
}
434
.eye-toggle {
plugins/_model_config/webui/model-config-store.js
+38
-7
@@ -67,6 +67,14 @@ export const store = createStore("modelConfig", {
67
68
init() {},
69
70
+ _normalizePresets(rawPresets) {
71
+ return (rawPresets || []).map(p => ({
72
+ name: p.name || '',
73
+ chat: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_history: 0.7, vision: true, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, browser_http_headers: {}, _kwargs_text: kwargsToText(p.chat?.kwargs), _browser_headers_text: Object.entries(p.chat?.browser_http_headers || {}).map(([k, v]) => k + '=' + v).join('\n'), ...(p.chat || {}) },
74
+ utility: { provider: '', name: '', api_key: '', api_base: '', ctx_length: 128000, ctx_input: 0.7, rl_requests: 0, rl_input: 0, rl_output: 0, kwargs: {}, _kwargs_text: kwargsToText(p.utility?.kwargs), ...(p.utility || {}) },
75
+ }));
76
+ },
77
+
78
async ensureLoaded() {
79
if (this._loaded) return;
80
const data = await this._fetchConfigData();
@@ -121,11 +129,7 @@ export const store = createStore("modelConfig", {
129
body: JSON.stringify({ action: 'get' })
130
});
131
const data = await res.json();
124
- this.globalPresets = (data.presets || []).map(p => ({
125
- name: p.name || '',
126
- chat: { provider: '', name: '', api_key: '', api_base: '', ...(p.chat || {}) },
127
- utility: { provider: '', name: '', api_key: '', api_base: '', ...(p.utility || {}) },
128
- }));
132
+ this.globalPresets = this._normalizePresets(data.presets);
133
} catch (e) {
134
console.error('Failed to load global presets:', e);
135
this.globalPresets = [];
@@ -134,11 +138,22 @@ export const store = createStore("modelConfig", {
138
},
139
140
async saveGlobalPresets(presets) {
141
+ // Strip UI-only fields before saving
142
+ const clean = presets.map(p => {
143
+ const c = { name: p.name };
144
+ for (const slot of ['chat', 'utility']) {
145
+ if (p[slot]) {
146
+ const { _kwargs_text, _browser_headers_text, ...rest } = p[slot];
147
+ c[slot] = rest;
148
+ }
149
+ }
150
+ return c;
151
+ });
152
try {
153
await fetchApi(`${API_BASE}/model_presets`, {
154
method: 'POST',
155
headers: { 'Content-Type': 'application/json' },
141
- body: JSON.stringify({ action: 'save', presets })
156
+ body: JSON.stringify({ action: 'save', presets: clean })
157
});
158
this.globalPresets = presets;
159
this.switcherPresets = presets.filter(p => p.name);
@@ -149,6 +164,22 @@ export const store = createStore("modelConfig", {
164
}
165
},
166
167
+ async resetGlobalPresets() {
168
+ try {
169
+ const res = await fetchApi(`${API_BASE}/model_presets`, {
170
+ method: 'POST',
171
+ headers: { 'Content-Type': 'application/json' },
172
+ body: JSON.stringify({ action: 'reset' })
173
+ });
174
+ const data = await res.json();
175
+ this.globalPresets = this._normalizePresets(data.presets);
176
+ this.switcherPresets = this.globalPresets.filter(p => p.name);
177
+ this._presetsLoaded = true;
178
+ } catch (e) {
179
+ console.error('Failed to reset presets:', e);
180
+ }
181
+ },
182
+
183
// API Key operations
184
async saveApiKey(provider, value) {
185
await fetchApi(`${API_BASE}/api_keys`, {
@@ -318,7 +349,7 @@ export const store = createStore("modelConfig", {
349
350
getSwitcherLabel() {
351
const o = this.switcherOverride;
321
- if (!o) return 'Default';
352
+ if (!o) return 'Default LLM';
353
return o.preset_name || o.name || o.provider || 'Custom';
354
},
355
plugins/_model_config/webui/models-summary.html
+5
-1
@@ -18,6 +18,7 @@
18
19
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;">
20
<button class="btn btn-field" @click="(async () => {
21
+ await import('/components/plugins/plugin-settings-store.js');
22
const pluginMeta = { name: '_model_config', always_enabled: true, per_project_config: true, per_agent_config: true, has_config_screen: true };
23
if ($store.pluginToggle?.open) await $store.pluginToggle.open(pluginMeta);
24
await $store.pluginSettingsPrototype.open('_model_config', { perProjectConfig: true, perAgentConfig: true });
@@ -25,8 +26,11 @@
26
})()">
27
Configure Models
28
</button>
29
+ <button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
30
+ Configure Presets
31
+ </button>
32
<button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
29
- Manage API Keys
33
+ Configure API Keys
34
</button>
35
</div>
36