diff --git a/api/routes.py b/api/routes.py index abe3f5817..6cb3c29fb 100644 --- a/api/routes.py +++ b/api/routes.py @@ -5214,50 +5214,52 @@ def _read_profile_model_config( ) -> tuple[str | None, str | None]: """Read model.provider and model.default from the session's profile config. - Returns (profile_provider, profile_default_model). Both are None when - the session has no profile, the profile config is unreadable, or an - explicit ``requested_provider`` is already set (profile should not - override explicit selections). + Returns (profile_provider, profile_default_model). Both are None when the + session has no profile or the profile config is unreadable. + + When the session already has an explicit ``requested_provider``, the profile + ``model.provider`` is not returned (first tuple element is None) so profile + does not override the session provider. ``profile_default_model`` is still + returned for suffix repair (#5127) only when the profile's configured + provider matches ``requested_provider`` after normalization. """ if not getattr(session, "profile", None): return None, None - if _clean_session_model_provider(requested_provider): - # Session already carries an explicit provider; still read profile default - # so process-wakeup can repair bare model suffixes (#5127). - try: - from api.profiles import get_hermes_home_for_profile - _profile_home = get_hermes_home_for_profile(session.profile) - _profile_cfg_path = os.path.join(str(_profile_home), "config.yaml") - if not os.path.isfile(_profile_cfg_path): - return None, None - import yaml - with open(_profile_cfg_path, encoding="utf-8") as _f: - _pcfg = yaml.safe_load(_f) or {} - if not isinstance(_pcfg, dict): - return None, None - _default = (_pcfg.get("model", {}).get("default") or "").strip() or None - return None, _default - except Exception: - logger.warning("profile provider read failed for %r", getattr(session, "profile", None), exc_info=True) - return None, None + try: from api.profiles import get_hermes_home_for_profile + _profile_home = get_hermes_home_for_profile(session.profile) _profile_cfg_path = os.path.join(str(_profile_home), "config.yaml") if not os.path.isfile(_profile_cfg_path): return None, None import yaml + with open(_profile_cfg_path, encoding="utf-8") as _f: _pcfg = yaml.safe_load(_f) or {} if not isinstance(_pcfg, dict): return None, None - _provider = (_pcfg.get("model", {}).get("provider") or "").strip() or None - _default = (_pcfg.get("model", {}).get("default") or "").strip() or None - return _provider, _default + _model_cfg = _pcfg.get("model") or {} + if not isinstance(_model_cfg, dict): + return None, None + _provider = (_model_cfg.get("provider") or "").strip() or None + _default = (_model_cfg.get("default") or "").strip() or None except Exception: - logger.warning("profile provider read failed for %r", getattr(session, "profile", None), exc_info=True) + logger.warning( + "profile provider read failed for %r", + getattr(session, "profile", None), + exc_info=True, + ) return None, None + _requested = _clean_session_model_provider(requested_provider) + if _requested: + _profile_prov = _clean_session_model_provider(_provider) + if _profile_prov != _requested: + return None, None + return None, _default + return _provider, _default + def _resolve_compatible_session_model_state( model_id: str | None, @@ -5322,12 +5324,16 @@ def _resolve_compatible_session_model_state( ) if not explicit_provider and not stale_codex_openai_slash_id: _profile_default = str(profile_default_model or "").strip() + _profile_prov = _clean_session_model_provider(profile_provider) + _providers_match_for_repair = ( + _profile_prov is None or _profile_prov == requested_provider + ) if ( _profile_default and "/" in _profile_default and "/" not in model and _profile_default.rsplit("/", 1)[-1] == model - and requested_provider + and _providers_match_for_repair and ( requested_provider == "custom" or str(requested_provider).startswith("custom:") diff --git a/tests/test_issue5127_process_wakeup_bare_model.py b/tests/test_issue5127_process_wakeup_bare_model.py index f8d8f81be..40ed5f3a9 100644 --- a/tests/test_issue5127_process_wakeup_bare_model.py +++ b/tests/test_issue5127_process_wakeup_bare_model.py @@ -16,6 +16,7 @@ class TestIssue5127CustomProviderBareSuffixRepair: result = _resolve_compatible_session_model_state( "grok-composer-2.5-fast", "custom:my-proxy", + profile_provider="custom:my-proxy", profile_default_model="x-ai/grok-composer-2.5-fast", prefer_cached_catalog=True, ) @@ -23,6 +24,22 @@ class TestIssue5127CustomProviderBareSuffixRepair: assert mock_catalog.call_count == 0 assert result == ("x-ai/grok-composer-2.5-fast", "custom:my-proxy", True) + def test_fast_path_skips_repair_when_profile_provider_mismatches(self): + """Regression: custom:other-proxy must not inherit my-proxy's qualified default.""" + from api.routes import _resolve_compatible_session_model_state + + with patch("api.routes.get_available_models") as mock_catalog: + result = _resolve_compatible_session_model_state( + "grok-composer-2.5-fast", + "custom:other-proxy", + profile_provider="custom:my-proxy", + profile_default_model="x-ai/grok-composer-2.5-fast", + prefer_cached_catalog=True, + ) + + assert mock_catalog.call_count == 0 + assert result == ("grok-composer-2.5-fast", "custom:other-proxy", False) + def test_fast_path_repairs_generic_custom_provider(self): from api.routes import _resolve_compatible_session_model_state @@ -108,4 +125,28 @@ class TestReadProfileModelConfigWithExplicitProvider: provider, default = _read_profile_model_config(_Session(), "custom:my-proxy") assert provider is None - assert default == "x-ai/grok-composer-2.5-fast" \ No newline at end of file + assert default == "x-ai/grok-composer-2.5-fast" + + def test_returns_none_default_when_session_provider_differs_from_profile( + self, tmp_path, monkeypatch, + ): + from api.routes import _read_profile_model_config + + profile_home = tmp_path / "prof" + profile_home.mkdir() + (profile_home / "config.yaml").write_text( + "model:\n provider: custom:my-proxy\n default: x-ai/grok-composer-2.5-fast\n", + encoding="utf-8", + ) + + class _Session: + profile = "testprof" + + monkeypatch.setattr( + "api.profiles.get_hermes_home_for_profile", + lambda _p: str(profile_home), + ) + + provider, default = _read_profile_model_config(_Session(), "custom:other-proxy") + assert provider is None + assert default is None \ No newline at end of file