mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-17 12:31:20 +00:00
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:
parent
332ffdcf4d
commit
7c59ac9e57
5 changed files with 148 additions and 28 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
};
|
||||
|
|
|
|||
34
tests/test_model_config_switcher.py
Normal file
34
tests/test_model_config_switcher.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue