diff --git a/api/config.py b/api/config.py index c68707b0..f5db4e32 100644 --- a/api/config.py +++ b/api/config.py @@ -5759,10 +5759,9 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa except OSError: _current_path = _get_config_path() _current_mtime = 0.0 - if ( - (_current_mtime != _cfg_mtime or _current_path != _cfg_path) - and not _cfg_has_in_memory_overrides() - ): + path_changed = _current_path != _cfg_path + mtime_stale = _current_mtime != _cfg_mtime + if path_changed or (mtime_stale and not _cfg_has_in_memory_overrides()): reload_config_if_stale() # ── COLD PATH helper ───────────────────────────────────────────────────── # Extracted so it runs inside _available_models_cache_lock (RLock) to diff --git a/tests/test_profile_switch_1200.py b/tests/test_profile_switch_1200.py index f5bdf5ed..9c3f3801 100644 --- a/tests/test_profile_switch_1200.py +++ b/tests/test_profile_switch_1200.py @@ -591,6 +591,79 @@ def test_get_config_reloads_when_request_profile_changes_even_with_rebound_cfg_o config.cfg = orig_cfg +def test_get_available_models_reloads_when_request_profile_changes_even_with_rebound_cfg_override( + tmp_path, monkeypatch +): + """The models payload must follow the new profile even when cfg was rebound.""" + monkeypatch.delenv("HERMES_CONFIG_PATH", raising=False) + import api.config as config + import api.profiles as profiles + + default_home = tmp_path / ".hermes" + work_home = default_home / "profiles" / "work" + work_home.mkdir(parents=True) + default_home.mkdir(exist_ok=True) + (default_home / "config.yaml").write_text( + "model:\n provider: openai-codex\n default: gpt-5.5\n", + encoding="utf-8", + ) + (work_home / "config.yaml").write_text( + "model:\n provider: openrouter\n default: google/gemini-3-flash-preview\n" + "custom_providers:\n llamacpp:\n api_key: work-key\n", + encoding="utf-8", + ) + same_mtime = 1_700_000_000 + os.utime(default_home / "config.yaml", (same_mtime, same_mtime)) + os.utime(work_home / "config.yaml", (same_mtime, same_mtime)) + + monkeypatch.setattr( + config, + "_get_config_path", + lambda: profiles.get_active_hermes_home() / "config.yaml", + ) + monkeypatch.setattr(config, "_LIVE_REBUILD_BUDGET_SECONDS", 0.0, raising=False) + + orig_default_home = profiles._DEFAULT_HERMES_HOME + orig_active = profiles._active_profile + orig_cache = dict(config._cfg_cache) + orig_mtime = config._cfg_mtime + orig_path = getattr(config, "_cfg_path", None) + orig_fingerprint = getattr(config, "_cfg_fingerprint", None) + orig_cfg = config.cfg + profiles._tls.profile = None + try: + profiles._DEFAULT_HERMES_HOME = default_home + profiles._active_profile = "default" + config.reload_config() + config.invalidate_models_cache() + monkeypatch.setattr( + config, + "cfg", + {"model": {"provider": "stale-override", "default": "old-model"}}, + raising=False, + ) + + profiles.set_request_profile("work") + assert config._get_config_path() == work_home / "config.yaml" + result = config.get_available_models() + assert result["active_provider"] == "openrouter" + assert result["default_model"] == "google/gemini-3-flash-preview" + assert config.cfg is config._cfg_cache + finally: + profiles.clear_request_profile() + profiles._DEFAULT_HERMES_HOME = orig_default_home + profiles._active_profile = orig_active + config._cfg_cache.clear() + config._cfg_cache.update(orig_cache) + config._cfg_mtime = orig_mtime + if hasattr(config, "_cfg_path"): + config._cfg_path = orig_path + if hasattr(config, "_cfg_fingerprint"): + config._cfg_fingerprint = orig_fingerprint + config.cfg = orig_cfg + config.invalidate_models_cache() + + def test_chat_start_retags_empty_session_to_request_profile(monkeypatch, tmp_path): """An empty session created under profile A can be sent under profile B after a switch.""" import api.routes as routes