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 &#x2014; 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