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.
This commit is contained in:
Alessandro 2026-04-28 15:34:14 +02:00
parent 332ffdcf4d
commit 7c59ac9e57
5 changed files with 148 additions and 28 deletions

View file

@ -4,6 +4,23 @@ from agent import AgentContext
from plugins._model_config.helpers import model_config
def _public_model_config(config: dict) -> dict | None:
if not isinstance(config, dict):
return None
provider = str(config.get("provider", "") or "").strip()
name = str(config.get("name", "") or "").strip()
if not provider and not name:
return None
return {"provider": provider, "name": name}
def _active_models(ctx: AgentContext) -> dict:
return {
"main": _public_model_config(model_config.get_chat_model_config(ctx.agent0)),
"utility": _public_model_config(model_config.get_utility_model_config(ctx.agent0)),
}
class ModelOverride(ApiHandler):
async def process(self, input: dict, request: Request) -> dict | Response:
context_id = input.get("context_id", "")
@ -19,7 +36,11 @@ class ModelOverride(ApiHandler):
if action == "get":
override = ctx.get_data("chat_model_override")
allowed = model_config.is_chat_override_allowed(ctx.agent0)
return {"override": override, "allowed": allowed}
return {
"override": override,
"allowed": allowed,
"active_models": _active_models(ctx),
}
elif action == "set":
if not model_config.is_chat_override_allowed(ctx.agent0):
@ -29,7 +50,11 @@ class ModelOverride(ApiHandler):
return Response(status=400, response="Missing or invalid override config")
ctx.set_data("chat_model_override", override_config)
save_tmp_chat(ctx)
return {"ok": True, "override": override_config}
return {
"ok": True,
"override": override_config,
"active_models": _active_models(ctx),
}
elif action == "set_preset":
if not model_config.is_chat_override_allowed(ctx.agent0):
@ -45,11 +70,19 @@ class ModelOverride(ApiHandler):
override_value = {"preset_name": preset_name}
ctx.set_data("chat_model_override", override_value)
save_tmp_chat(ctx)
return {"ok": True, "preset_name": preset_name}
return {
"ok": True,
"preset_name": preset_name,
"active_models": _active_models(ctx),
}
elif action == "clear":
ctx.set_data("chat_model_override", None)
save_tmp_chat(ctx)
return {"ok": True, "override": None}
return {
"ok": True,
"override": None,
"active_models": _active_models(ctx),
}
return Response(status=400, response=f"Unknown action: {action}")

View file

@ -11,7 +11,10 @@
$store.modelConfig.refreshSwitcher($store.chats?.selected || ''),
$store.modelConfig.loadAgentProfiles(),
]);
const refreshActiveModels = () => $store.modelConfig.refreshSwitcher($store.chats?.selected || '');
$watch('$store.chats.selected', v => $store.modelConfig.refreshSwitcher(v || ''));
$watch('$store.chats.selectedContext?.project?.name || \'\'' , refreshActiveModels);
$watch('$store.chats.selectedContext?.agent_profile || \'\'' , refreshActiveModels);
">
<template x-if="($store.modelConfig.switcherAllowed && !$store.modelConfig.switcherLoading) || $store.chats?.selectedContext?.agent_profile">
<div class="model-switcher-container">
@ -27,18 +30,18 @@
x-text="showDropdown ? 'expand_less' : 'expand_more'"></span>
</button>
<!-- Inline active model pills (shown when a preset is active) -->
<template x-if="$store.modelConfig.switcherOverride">
<!-- Inline active model pills -->
<template x-if="$store.modelConfig.hasActiveModelNames()">
<div class="model-switcher-active-pills">
<template x-if="$store.modelConfig.getActiveModels().main">
<template x-if="$store.modelConfig.getActiveModels().main?.name">
<div class="model-pill">
<span class="model-pill-role">Main</span>
<span class="model-pill-role">Main:</span>
<span class="model-pill-name" x-text="$store.modelConfig.getActiveModels().main.name"></span>
</div>
</template>
<template x-if="$store.modelConfig.getActiveModels().utility">
<template x-if="$store.modelConfig.getActiveModels().utility?.name">
<div class="model-pill">
<span class="model-pill-role">Util</span>
<span class="model-pill-role">Utility:</span>
<span class="model-pill-name" x-text="$store.modelConfig.getActiveModels().utility.name"></span>
</div>
</template>
@ -256,6 +259,7 @@
font-size: 0.68rem;
white-space: nowrap;
min-width: 0;
max-width: 240px;
overflow: hidden;
}
.model-pill-role {
@ -396,7 +400,7 @@
}
/* Responsive: hide pills on narrow screens */
@media (max-width: 600px) {
@media (max-width: 760px) {
.model-switcher-active-pills {
display: none;
}

View file

@ -200,6 +200,7 @@ export const store = createStore("modelConfig", {
/**
* Install save and reset hooks on the plugin settings context.
* - Save: persists dirty API keys before the normal config save.
* - Save: refreshes active chat model names after the config is persisted.
* - Reset: reloads global presets when settings are reset to defaults.
*/
installSettingsHooks(context, config) {
@ -215,6 +216,9 @@ export const store = createStore("modelConfig", {
return;
}
await originalSave();
if (!context.error) {
await this.refreshActiveChatModels();
}
};
const originalReset = context.resetToDefault.bind(context);
@ -229,6 +233,12 @@ export const store = createStore("modelConfig", {
context.__modelConfigHooksInstalled = true;
},
async refreshActiveChatModels() {
const contextId = window.Alpine?.store("chats")?.selected || "";
if (!contextId) return;
await this.refreshSwitcher(contextId);
},
// Model search
getProviders(key) {
return key === 'embedding_model' ? this.embeddingProviders : this.chatProviders;

View file

@ -9,6 +9,7 @@ export const switcherState = {
switcherAllowed: false,
switcherOverride: null,
switcherPresets: [],
switcherActiveModels: { main: null, utility: null },
switcherLoading: true,
agentProfiles: [],
agentProfilesLoading: true,
@ -17,6 +18,33 @@ export const switcherState = {
};
export const switcherMethods = {
normalizeActiveModel(model) {
if (!model || typeof model !== "object") return null;
const provider = String(model.provider || "").trim();
const name = String(model.name || "").trim();
if (!provider && !name) return null;
return { provider, name };
},
normalizeActiveModels(models = {}) {
return {
main: this.normalizeActiveModel(models.main),
utility: this.normalizeActiveModel(models.utility),
};
},
hasModelNames(models) {
return !!(models?.main?.name || models?.utility?.name);
},
modelsFromPreset(preset) {
if (!preset) return { main: null, utility: null };
return this.normalizeActiveModels({
main: preset.chat,
utility: preset.utility,
});
},
async loadAgentProfiles(force = false) {
if (!force && this.agentProfiles.length > 0 && this.agentProfileSettings) return this.agentProfiles;
this.agentProfilesLoading = true;
@ -44,7 +72,7 @@ export const switcherMethods = {
},
async loadSwitcherState(contextId) {
const result = { allowed: false, presets: [], override: null };
const result = { allowed: false, presets: [], override: null, activeModels: { main: null, utility: null } };
try {
await this.loadGlobalPresets();
result.presets = this.globalPresets.filter(p => p.name);
@ -57,6 +85,7 @@ export const switcherMethods = {
const overData = await overRes.json();
result.allowed = !!overData.allowed;
result.override = overData.override || null;
result.activeModels = this.normalizeActiveModels(overData.active_models || {});
}
} catch (e) {
console.error("Model switcher load failed:", e);
@ -71,10 +100,10 @@ export const switcherMethods = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "set_preset", context_id: contextId, preset_name: presetName }),
});
return !!(await res.json()).ok;
return await res.json();
} catch (e) {
console.error("Failed to set preset override:", e);
return false;
return { ok: false };
}
},
@ -85,10 +114,10 @@ export const switcherMethods = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "clear", context_id: contextId }),
});
return !!(await res.json()).ok;
return await res.json();
} catch (e) {
console.error("Failed to clear override:", e);
return false;
return { ok: false };
}
},
@ -194,6 +223,7 @@ export const switcherMethods = {
this.switcherAllowed = state.allowed;
this.switcherPresets = state.presets;
this.switcherOverride = state.override;
this.switcherActiveModels = state.activeModels;
} catch (e) {
console.error('Model switcher refresh failed:', e);
} finally {
@ -202,15 +232,24 @@ export const switcherMethods = {
},
async selectPresetSwitch(contextId, presetName) {
const ok = await this.setPresetOverride(contextId, presetName);
if (ok) this.switcherOverride = { preset_name: presetName };
return ok;
const data = await this.setPresetOverride(contextId, presetName);
if (data?.ok) {
this.switcherOverride = { preset_name: data.preset_name || presetName };
const activeModels = this.normalizeActiveModels(data.active_models || {});
this.switcherActiveModels = this.hasModelNames(activeModels)
? activeModels
: this.modelsFromPreset(this.switcherPresets.find(p => p.name === presetName));
}
return !!data?.ok;
},
async clearOverrideSwitch(contextId) {
const ok = await this.clearOverride(contextId);
if (ok) this.switcherOverride = null;
return ok;
const data = await this.clearOverride(contextId);
if (data?.ok) {
this.switcherOverride = null;
this.switcherActiveModels = this.normalizeActiveModels(data.active_models || {});
}
return !!data?.ok;
},
getSwitcherLabel() {
@ -226,11 +265,11 @@ export const switcherMethods = {
},
getActiveModels() {
const preset = this.getActivePreset();
if (!preset) return { main: null, utility: null };
return {
main: preset.chat?.name ? { provider: preset.chat.provider, name: preset.chat.name } : null,
utility: preset.utility?.name ? { provider: preset.utility.provider, name: preset.utility.name } : null,
};
if (this.hasModelNames(this.switcherActiveModels)) return this.switcherActiveModels;
return this.modelsFromPreset(this.getActivePreset());
},
hasActiveModelNames() {
return this.hasModelNames(this.getActiveModels());
},
};

View file

@ -0,0 +1,34 @@
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def test_model_switcher_surfaces_default_active_models():
switcher = (
PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "switcher-mixin.js"
).read_text(encoding="utf-8")
store = (
PROJECT_ROOT / "plugins" / "_model_config" / "webui" / "model-config-store.js"
).read_text(encoding="utf-8")
template = (
PROJECT_ROOT
/ "plugins"
/ "_model_config"
/ "extensions"
/ "webui"
/ "chat-input-progress-start"
/ "model-switcher.html"
).read_text(encoding="utf-8")
api = (
PROJECT_ROOT / "plugins" / "_model_config" / "api" / "model_override.py"
).read_text(encoding="utf-8")
assert '"active_models": _active_models(ctx)' in api
assert "switcherActiveModels" in switcher
assert "hasActiveModelNames()" in template
assert "Main:" in template
assert "Utility:" in template
assert "@media (max-width: 760px)" in template
assert "await this.refreshActiveChatModels();" in store
assert 'window.Alpine?.store("chats")?.selected' in store