diff --git a/api/config.py b/api/config.py index 0e6ceac9..add89cfa 100644 --- a/api/config.py +++ b/api/config.py @@ -3555,7 +3555,7 @@ def _apply_advanced_model_options(model_cfg: dict, advanced: dict | None) -> Non model_cfg["api_key"] = api_key -def set_hermes_default_model(model_id: str, advanced: dict | None = None) -> dict: +def set_hermes_default_model(model_id: str, provider: str | None = None, advanced: dict | None = None) -> dict: """Persist the Hermes default model in config.yaml and reload runtime config.""" selected_model = str(model_id or "").strip() if not selected_model: @@ -3572,6 +3572,7 @@ def set_hermes_default_model(model_id: str, advanced: dict | None = None) -> dic model_cfg = {} previous_provider = str(model_cfg.get("provider") or "").strip() + requested_provider = str(provider or "").strip() resolved_model, resolved_provider, resolved_base_url = resolve_model_provider( selected_model ) @@ -3585,7 +3586,7 @@ def set_hermes_default_model(model_id: str, advanced: dict | None = None) -> dic # CLI-shaped bare form via `_applyModelToDropdown()`'s normalising # matcher — see `static/panels.js` (#895). persisted_model = str(resolved_model or selected_model).strip() - persisted_provider = str(resolved_provider or previous_provider or "").strip() + persisted_provider = str(requested_provider or resolved_provider or previous_provider or "").strip() # Never persist the bogus ``local`` value — see #1384. The auto-detect # block in ``_build_available_models_uncached`` was rewriting unknown # loopback hosts to ``provider: "local"``, which is not registered and @@ -3617,7 +3618,7 @@ def set_hermes_default_model(model_id: str, advanced: dict | None = None) -> dic # it triggers a live provider fetch (up to 8s) that blocks the HTTP response # to the browser, causing a visible freeze on every Settings save (#895). invalidate_models_cache() - return {"ok": True, "model": persisted_model} + return {"ok": True, "model": persisted_model, "provider": persisted_provider or None} # ── Auxiliary model configuration ────────────────────────────────────────── diff --git a/api/routes.py b/api/routes.py index 25440494..4308109c 100644 --- a/api/routes.py +++ b/api/routes.py @@ -10943,7 +10943,8 @@ def handle_post(handler, parsed) -> bool: if parsed.path == "/api/default-model": try: advanced = body.get("advanced") if isinstance(body, dict) else None - return j(handler, set_hermes_default_model(body.get("model"), advanced=advanced)) + provider = body.get("provider") if isinstance(body, dict) else None + return j(handler, set_hermes_default_model(body.get("model"), provider=provider, advanced=advanced)) except ValueError as e: return bad(handler, str(e)) except RuntimeError as e: @@ -10964,7 +10965,7 @@ def handle_post(handler, parsed) -> bool: return bad(handler, str(exc), status=400) if scope == "main": try: - return j(handler, set_hermes_default_model(model, advanced=advanced)) + return j(handler, set_hermes_default_model(model, provider=provider, advanced=advanced)) except ValueError as exc: return bad(handler, str(exc), status=400) return bad(handler, f"unknown scope: {scope}", status=400) diff --git a/static/panels.js b/static/panels.js index 5000c239..af48a6c1 100644 --- a/static/panels.js +++ b/static/panels.js @@ -9795,7 +9795,7 @@ async function saveSettings(andClose){ const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(payload)}); if(modelChanged && model){ try{ - await api('/api/default-model',{method:'POST',body:JSON.stringify({model})}); + await api('/api/default-model',{method:'POST',body:JSON.stringify({model,provider:modelState.model_provider||null})}); body.default_model=model; body.default_model_provider=(modelState&&modelState.model===model)?(modelState.model_provider||null):null; }catch(_modelErr){ @@ -9825,7 +9825,7 @@ async function saveSettings(andClose){ const saved=await api('/api/settings',{method:'POST',body:JSON.stringify(body)}); if(modelChanged && model){ try{ - await api('/api/default-model',{method:'POST',body:JSON.stringify({model})}); + await api('/api/default-model',{method:'POST',body:JSON.stringify({model,provider:modelState.model_provider||null})}); body.default_model=model; body.default_model_provider=(modelState&&modelState.model===model)?(modelState.model_provider||null):null; }catch(_modelErr){ diff --git a/tests/test_auxiliary_models_settings.py b/tests/test_auxiliary_models_settings.py index e39c24f4..eca8d677 100644 --- a/tests/test_auxiliary_models_settings.py +++ b/tests/test_auxiliary_models_settings.py @@ -474,6 +474,23 @@ class TestAuxiliaryModelsBackend: assert "reasoning_effort: none" in text assert "DUMMY_KEY_DO_NOT_PRINT" in text + def test_set_hermes_default_model_persists_explicit_provider_override(self, monkeypatch, tmp_path): + from api import config + + config_path = tmp_path / "config.yaml" + config_path.write_text("model:\n provider: openai\n default: gpt-5.5\n", encoding="utf-8") + monkeypatch.setattr(config, "_get_config_path", lambda: config_path) + monkeypatch.setattr(config, "reload_config", lambda: None) + monkeypatch.setattr(config, "invalidate_models_cache", lambda: None) + monkeypatch.setattr(config, "resolve_model_provider", lambda model: (model, "", None)) + + result = config.set_hermes_default_model("gpt-5.5", provider="anthropic") + + assert result["ok"] is True + assert result["provider"] == "anthropic" + text = config_path.read_text(encoding="utf-8") + assert "provider: anthropic" in text + def test_set_auxiliary_model_persists_advanced_options(self, monkeypatch, tmp_path): """Gear-modal payload should persist supported per-slot options.""" from api import config diff --git a/tests/test_new_chat_default_model_frontend.py b/tests/test_new_chat_default_model_frontend.py index 1637125f..e49ffddf 100644 --- a/tests/test_new_chat_default_model_frontend.py +++ b/tests/test_new_chat_default_model_frontend.py @@ -162,6 +162,7 @@ def test_save_settings_syncs_default_model_provider_with_saved_model(): autosave_block = panels_js[panels_js.index("const pwField=$('settingsPassword');"):panels_js.index("if(!pwDirty&&!modelDirty){", panels_js.index("const pwField=$('settingsPassword');")) + 24] assert "_captureModelDropdownSelection($('settingsModel'))" in save_block + assert "JSON.stringify({model,provider:modelState.model_provider||null})" in save_block assert "body.default_model_provider=(modelState&&modelState.model===model)?(modelState.model_provider||null):null;" in save_block assert "const modelChanged=(model||'')!==(_settingsHermesDefaultModelOnOpen||'')||((modelState.model_provider||null)!==(_settingsHermesDefaultModelProviderOnOpen||null));" in save_block assert "if(Object.prototype.hasOwnProperty.call(body,'default_model_provider')) window._activeProvider=body.default_model_provider||null;" in apply_saved_block