feat(desktop): warn when main-model switch leaves auxiliary tasks pinned to another provider (#40286)
Switching the main model never touches auxiliary slot pins (they're independent, sticky per-task overrides). A user who switches main away from a now-unpaid provider keeps paying 402s on every background aux call until they manually reset those pins — silently, with no UI signal. - /api/model/set scope:'main' now returns stale_aux: slots still pinned to a provider different from the new main (additive field). - Desktop Model Settings shows a switch-time notice after Apply AND a persistent banner when any loaded aux slot mismatches the main provider, both wired to the existing 'Reset all to main' action. - Never auto-clears pins — a dedicated cheaper aux model is a legitimate config; surface-and-offer instead of nuking. - Fixes a stale pre-existing assertion in the panel test (main model now renders via selectors, not a standalone label).
This commit is contained in:
@@ -2248,6 +2248,36 @@ async def set_model_assignment(body: ModelAssignment):
|
||||
_log.debug("apply_nous_managed_defaults skipped", exc_info=True)
|
||||
|
||||
save_config(cfg)
|
||||
|
||||
# Surface auxiliary slots still pinned to a *different* provider than
|
||||
# the new main one. Switching the main model does NOT touch aux pins
|
||||
# (they're independent, sticky per-task overrides — see
|
||||
# auxiliary_client._resolve_auto). A user who switches main away from
|
||||
# a now-unpaid provider (e.g. nous with $0 balance) keeps paying 402s
|
||||
# on every background aux call until they reset those pins. We never
|
||||
# auto-clear them — pinning aux to a cheaper/different model is a
|
||||
# legitimate config — but we tell the caller so the UI can offer a
|
||||
# "reset to main" nudge instead of silently burning credits.
|
||||
new_provider = provider.strip().lower()
|
||||
stale_aux: list[dict] = []
|
||||
aux_cfg = cfg.get("auxiliary", {})
|
||||
if isinstance(aux_cfg, dict):
|
||||
for slot in _AUX_TASK_SLOTS:
|
||||
slot_cfg = aux_cfg.get(slot)
|
||||
if not isinstance(slot_cfg, dict):
|
||||
continue
|
||||
slot_provider = str(slot_cfg.get("provider", "") or "").strip()
|
||||
if (
|
||||
slot_provider
|
||||
and slot_provider.lower() not in {"auto", ""}
|
||||
and slot_provider.lower() != new_provider
|
||||
):
|
||||
stale_aux.append({
|
||||
"task": slot,
|
||||
"provider": slot_provider,
|
||||
"model": str(slot_cfg.get("model", "") or ""),
|
||||
})
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"scope": "main",
|
||||
@@ -2255,6 +2285,7 @@ async def set_model_assignment(body: ModelAssignment):
|
||||
"model": model,
|
||||
"base_url": model_cfg.get("base_url", ""),
|
||||
"gateway_tools": gateway_tools,
|
||||
"stale_aux": stale_aux,
|
||||
}
|
||||
|
||||
# scope == "auxiliary"
|
||||
|
||||
Reference in New Issue
Block a user