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