Show active models for Default LLM
Expose sanitized active main and utility model metadata through the model override endpoint, then render those names in the chat model switcher even when no preset override is active. Keep the inline model names hidden on narrow screens and cover the behavior with a regression check. Refresh model names after settings save Refresh the active chat model switcher after _model_config settings are saved so changes to main and utility models appear immediately. Extend the model switcher regression check to cover the save-refresh hook.
Alessandro committed
Apr 28, 2026 at 15:34 UTC
7c59ac9e577694b60ba062f0a6eaab8a2fb5df3c
5 files changed
+148
-28
plugins/_model_config/api/model_override.py
+37
-4
@@ -4,6 +4,23 @@ from agent import AgentContext
4
from plugins._model_config.helpers import model_config
5
6
7
+def _public_model_config(config: dict) -> dict | None:
8
+ if not isinstance(config, dict):
9
+ return None
10
+ provider = str(config.get("provider", "") or "").strip()
11
+ name = str(config.get("name", "") or "").strip()
12
+ if not provider and not name:
13
+ return None
14
+ return {"provider": provider, "name": name}
15
+
16
+
17
+def _active_models(ctx: AgentContext) -> dict:
18
+ return {
19
+ "main": _public_model_config(model_config.get_chat_model_config(ctx.agent0)),
20
+ "utility": _public_model_config(model_config.get_utility_model_config(ctx.agent0)),
21
+ }
22
+
23
+
24
class ModelOverride(ApiHandler):
25
async def process(self, input: dict, request: Request) -> dict | Response:
26
context_id = input.get("context_id", "")
@@ -19,7 +36,11 @@ class ModelOverride(ApiHandler):
36
if action == "get":
37
override = ctx.get_data("chat_model_override")
38
allowed = model_config.is_chat_override_allowed(ctx.agent0)
22
- return {"override": override, "allowed": allowed}
39
+ return {
40
+ "override": override,
41
+ "allowed": allowed,
42
+ "active_models": _active_models(ctx),
43
+ }
44
45
elif action == "set":
46
if not model_config.is_chat_override_allowed(ctx.agent0):
@@ -29,7 +50,11 @@ class ModelOverride(ApiHandler):
50
return Response(status=400, response="Missing or invalid override config")
51
ctx.set_data("chat_model_override", override_config)
52
save_tmp_chat(ctx)
32
- return {"ok": True, "override": override_config}
53
+ return {
54
+ "ok": True,
55
+ "override": override_config,
56
+ "active_models": _active_models(ctx),
57
+ }
58
59
elif action == "set_preset":
60
if not model_config.is_chat_override_allowed(ctx.agent0):
@@ -45,11 +70,19 @@ class ModelOverride(ApiHandler):
70
override_value = {"preset_name": preset_name}
71
ctx.set_data("chat_model_override", override_value)
72
save_tmp_chat(ctx)
48
- return {"ok": True, "preset_name": preset_name}
73
+ return {
74
+ "ok": True,
75
+ "preset_name": preset_name,
76
+ "active_models": _active_models(ctx),
77
+ }
78
79
elif action == "clear":
80
ctx.set_data("chat_model_override", None)
81
save_tmp_chat(ctx)
53
- return {"ok": True, "override": None}
82
+ return {
83
+ "ok": True,
84
+ "override": None,
85
+ "active_models": _active_models(ctx),
86
+ }
87
88
return Response(status=400, response=f"Unknown action: {action}")
plugins/_model_config/extensions/webui/chat-input-progress-start/model-switcher.html
+11
-7
@@ -11,7 +11,10 @@
11
$store.modelConfig.refreshSwitcher($store.chats?.selected || ''),
12
$store.modelConfig.loadAgentProfiles(),
13
]);
14
+ const refreshActiveModels = () => $store.modelConfig.refreshSwitcher($store.chats?.selected || '');
15
$watch('$store.chats.selected', v => $store.modelConfig.refreshSwitcher(v || ''));
16
+ $watch('$store.chats.selectedContext?.project?.name || \'\'' , refreshActiveModels);
17
+ $watch('$store.chats.selectedContext?.agent_profile || \'\'' , refreshActiveModels);
18
">
19
<template x-if="($store.modelConfig.switcherAllowed && !$store.modelConfig.switcherLoading) || $store.chats?.selectedContext?.agent_profile">
20
<div class="model-switcher-container">
@@ -27,18 +30,18 @@
30
x-text="showDropdown ? 'expand_less' : 'expand_more'"></span>
31
</button>
32
30
- <!-- Inline active model pills (shown when a preset is active) -->
31
- <template x-if="$store.modelConfig.switcherOverride">
33
+ <!-- Inline active model pills -->
34
+ <template x-if="$store.modelConfig.hasActiveModelNames()">
35
<div class="model-switcher-active-pills">
33
- <template x-if="$store.modelConfig.getActiveModels().main">
36
+ <template x-if="$store.modelConfig.getActiveModels().main?.name">
37
<div class="model-pill">
35
- <span class="model-pill-role">Main</span>
38
+ <span class="model-pill-role">Main:</span>
39
<span class="model-pill-name" x-text="$store.modelConfig.getActiveModels().main.name"></span>
40
</div>
41
</template>
39
- <template x-if="$store.modelConfig.getActiveModels().utility">
42
+ <template x-if="$store.modelConfig.getActiveModels().utility?.name">
43
<div class="model-pill">
41
- <span class="model-pill-role">Util</span>
44
+ <span class="model-pill-role">Utility:</span>
45
<span class="model-pill-name" x-text="$store.modelConfig.getActiveModels().utility.name"></span>
46
</div>
47
</template>
@@ -256,6 +259,7 @@
259
font-size: 0.68rem;
260
white-space: nowrap;
261
min-width: 0;
262
+ max-width: 240px;
263
overflow: hidden;
264
}
265
.model-pill-role {
@@ -396,7 +400,7 @@
400
}
401
402
/* Responsive: hide pills on narrow screens */
399
- @media (max-width: 600px) {
403
+ @media (max-width: 760px) {
404
.model-switcher-active-pills {
405
display: none;
406
}
plugins/_model_config/webui/model-config-store.js
+10
@@ -200,6 +200,7 @@ export const store = createStore("modelConfig", {
200
/**
201
* Install save and reset hooks on the plugin settings context.
202
* - Save: persists dirty API keys before the normal config save.
203
+ * - Save: refreshes active chat model names after the config is persisted.
204
* - Reset: reloads global presets when settings are reset to defaults.
205
*/
206
installSettingsHooks(context, config) {
@@ -215,6 +216,9 @@ export const store = createStore("modelConfig", {
216
return;
217
}
218
await originalSave();
219
+ if (!context.error) {
220
+ await this.refreshActiveChatModels();
221
+ }
222
};
223
224
const originalReset = context.resetToDefault.bind(context);
@@ -229,6 +233,12 @@ export const store = createStore("modelConfig", {
233
context.__modelConfigHooksInstalled = true;
234
},
235
236
+ async refreshActiveChatModels() {
237
+ const contextId = window.Alpine?.store("chats")?.selected || "";
238
+ if (!contextId) return;
239
+ await this.refreshSwitcher(contextId);
240
+ },
241
+
242
// Model search
243
getProviders(key) {
244
return key === 'embedding_model' ? this.embeddingProviders : this.chatProviders;
plugins/_model_config/webui/switcher-mixin.js
+56
-17
@@ -9,6 +9,7 @@ export const switcherState = {
9
switcherAllowed: false,
10
switcherOverride: null,
11
switcherPresets: [],
12
+ switcherActiveModels: { main: null, utility: null },
13
switcherLoading: true,
14
agentProfiles: [],
15
agentProfilesLoading: true,
@@ -17,6 +18,33 @@ export const switcherState = {
18
};
19
20
export const switcherMethods = {
21
+ normalizeActiveModel(model) {
22
+ if (!model || typeof model !== "object") return null;
23
+ const provider = String(model.provider || "").trim();
24
+ const name = String(model.name || "").trim();
25
+ if (!provider && !name) return null;
26
+ return { provider, name };
27
+ },
28
+
29
+ normalizeActiveModels(models = {}) {
30
+ return {
31
+ main: this.normalizeActiveModel(models.main),
32
+ utility: this.normalizeActiveModel(models.utility),
33
+ };
34
+ },
35
+
36
+ hasModelNames(models) {
37
+ return !!(models?.main?.name || models?.utility?.name);
38
+ },
39
+
40
+ modelsFromPreset(preset) {
41
+ if (!preset) return { main: null, utility: null };
42
+ return this.normalizeActiveModels({
43
+ main: preset.chat,
44
+ utility: preset.utility,
45
+ });
46
+ },
47
+
48
async loadAgentProfiles(force = false) {
49
if (!force && this.agentProfiles.length > 0 && this.agentProfileSettings) return this.agentProfiles;
50
this.agentProfilesLoading = true;
@@ -44,7 +72,7 @@ export const switcherMethods = {
72
},
73
74
async loadSwitcherState(contextId) {
47
- const result = { allowed: false, presets: [], override: null };
75
+ const result = { allowed: false, presets: [], override: null, activeModels: { main: null, utility: null } };
76
try {
77
await this.loadGlobalPresets();
78
result.presets = this.globalPresets.filter(p => p.name);
@@ -57,6 +85,7 @@ export const switcherMethods = {
85
const overData = await overRes.json();
86
result.allowed = !!overData.allowed;
87
result.override = overData.override || null;
88
+ result.activeModels = this.normalizeActiveModels(overData.active_models || {});
89
}
90
} catch (e) {
91
console.error("Model switcher load failed:", e);
@@ -71,10 +100,10 @@ export const switcherMethods = {
100
headers: { "Content-Type": "application/json" },
101
body: JSON.stringify({ action: "set_preset", context_id: contextId, preset_name: presetName }),
102
});
74
- return !!(await res.json()).ok;
103
+ return await res.json();
104
} catch (e) {
105
console.error("Failed to set preset override:", e);
77
- return false;
106
+ return { ok: false };
107
}
108
},
109
@@ -85,10 +114,10 @@ export const switcherMethods = {
114
headers: { "Content-Type": "application/json" },
115
body: JSON.stringify({ action: "clear", context_id: contextId }),
116
});
88
- return !!(await res.json()).ok;
117
+ return await res.json();
118
} catch (e) {
119
console.error("Failed to clear override:", e);
91
- return false;
120
+ return { ok: false };
121
}
122
},
123
@@ -194,6 +223,7 @@ export const switcherMethods = {
223
this.switcherAllowed = state.allowed;
224
this.switcherPresets = state.presets;
225
this.switcherOverride = state.override;
226
+ this.switcherActiveModels = state.activeModels;
227
} catch (e) {
228
console.error('Model switcher refresh failed:', e);
229
} finally {
@@ -202,15 +232,24 @@ export const switcherMethods = {
232
},
233
234
async selectPresetSwitch(contextId, presetName) {
205
- const ok = await this.setPresetOverride(contextId, presetName);
206
- if (ok) this.switcherOverride = { preset_name: presetName };
207
- return ok;
235
+ const data = await this.setPresetOverride(contextId, presetName);
236
+ if (data?.ok) {
237
+ this.switcherOverride = { preset_name: data.preset_name || presetName };
238
+ const activeModels = this.normalizeActiveModels(data.active_models || {});
239
+ this.switcherActiveModels = this.hasModelNames(activeModels)
240
+ ? activeModels
241
+ : this.modelsFromPreset(this.switcherPresets.find(p => p.name === presetName));
242
+ }
243
+ return !!data?.ok;
244
},
245
246
async clearOverrideSwitch(contextId) {
211
- const ok = await this.clearOverride(contextId);
212
- if (ok) this.switcherOverride = null;
213
- return ok;
247
+ const data = await this.clearOverride(contextId);
248
+ if (data?.ok) {
249
+ this.switcherOverride = null;
250
+ this.switcherActiveModels = this.normalizeActiveModels(data.active_models || {});
251
+ }
252
+ return !!data?.ok;
253
},
254
255
getSwitcherLabel() {
@@ -226,11 +265,11 @@ export const switcherMethods = {
265
},
266
267
getActiveModels() {
229
- const preset = this.getActivePreset();
230
- if (!preset) return { main: null, utility: null };
231
- return {
232
- main: preset.chat?.name ? { provider: preset.chat.provider, name: preset.chat.name } : null,
233
- utility: preset.utility?.name ? { provider: preset.utility.provider, name: preset.utility.name } : null,
234
- };
268
+ if (this.hasModelNames(this.switcherActiveModels)) return this.switcherActiveModels;
269
+ return this.modelsFromPreset(this.getActivePreset());
270
+ },
271
+
272
+ hasActiveModelNames() {
273
+ return this.hasModelNames(this.getActiveModels());
274
},
275
};
tests/test_model_config_switcher.py
new
+34
@@ -0,0 +1,34 @@
1
+from pathlib import Path
2
+
3
+
4
+PROJECT_ROOT = Path(__file__).resolve().parents[1]
5
+
6
+
7
+def test_model_switcher_surfaces_default_active_models():
8
+ switcher = (
9
+ PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"
10
+ ).read_text(encoding="utf-8")
11
+ store = (
12
+ PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "model-config-store.js"
13
+ ).read_text(encoding="utf-8")
14
+ template = (
15
+ PROJECT_ROOT
16
+ / "plugins"
17
+ / "_model_config"
18
+ / "extensions"
19
+ / "webui"
20
+ / "chat-input-progress-start"
21
+ / "model-switcher.html"
22
+ ).read_text(encoding="utf-8")
23
+ api = (
24
+ PROJECT_ROOT / "plugins" / "_model_config" / "api" / "model_override.py"
25
+ ).read_text(encoding="utf-8")
26
+
27
+ assert '"active_models": _active_models(ctx)' in api
28
+ assert "switcherActiveModels" in switcher
29
+ assert "hasActiveModelNames()" in template
30
+ assert "Main:" in template
31
+ assert "Utility:" in template
32
+ assert "@media (max-width: 760px)" in template
33
+ assert "await this.refreshActiveChatModels();" in store
34
+ assert 'window.Alpine?.store("chats")?.selected' in store