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:
Teknium
2026-06-05 23:35:36 -07:00
committed by GitHub
parent f8a241e105
commit b91aade176
7 changed files with 217 additions and 3 deletions
+31
View File
@@ -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"