fix: gate bare-model repair on profile provider match (#5128 review)

When session model_provider differs from profile model.provider, do not
expose profile_default_model for suffix repair. Fast path also requires
profile_provider to match requested_provider when profile_provider is set.

Addresses cross-provider mis-resolution (custom:other-proxy + bare suffix).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
This commit is contained in:
b3nw
2026-06-28 21:52:06 +00:00
parent 159d8a96f4
commit 7a04b7e9aa
2 changed files with 76 additions and 29 deletions
+34 -28
View File
@@ -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:")
@@ -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"
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