fix: repair bare custom-provider model on process-wakeup fast path

Refs #5127.

When _resolve_compatible_session_model_state takes the #1855 fast path with
a bare runtime model suffix and a slash-qualified profile default, repair to
the full qualified ID for custom/custom:* providers.

Also load profile model.default when the session already has model_provider
so wakeup turns can apply that repair.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
This commit is contained in:
b3nwandb3nw committed 2026-06-28 18:34:10 +00:00
1 parent e24e6c54a6
commit 159d8a96f4
2 files changed
+144 -1

No files matched your search

+33 -1
View File
@@ -5219,8 +5219,27 @@ def _read_profile_model_config(
explicit ``requested_provider`` is already set (profile should not
override explicit selections).
"""
if _clean_session_model_provider(requested_provider) or not getattr(session, "profile", None):
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)
@@ -5302,6 +5321,19 @@ def _resolve_compatible_session_model_state(
and model_prefix == "openai"
)
if not explicit_provider and not stale_codex_openai_slash_id:
_profile_default = str(profile_default_model or "").strip()
if (
_profile_default
and "/" in _profile_default
and "/" not in model
and _profile_default.rsplit("/", 1)[-1] == model
and requested_provider
and (
requested_provider == "custom"
or str(requested_provider).startswith("custom:")
)
):
return _profile_default, requested_provider, True
return model, requested_provider, False
# Default (human chat/start) path calls get_available_models() with NO
@@ -0,0 +1,111 @@
"""Tests for issue #5127 — process-wakeup bare custom-provider model repair.
When ``_resolve_compatible_session_model_state`` takes the #1855 fast path
(both model and model_provider set), a bare runtime model that matches the
suffix of a slash-qualified ``profile_default_model`` must repair to the full
qualified ID for named custom providers.
"""
from unittest.mock import patch
class TestIssue5127CustomProviderBareSuffixRepair:
def test_fast_path_repairs_bare_suffix_to_profile_default(self):
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:my-proxy",
profile_default_model="x-ai/grok-composer-2.5-fast",
prefer_cached_catalog=True,
)
assert mock_catalog.call_count == 0
assert result == ("x-ai/grok-composer-2.5-fast", "custom:my-proxy", True)
def test_fast_path_repairs_generic_custom_provider(self):
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(
"some-model",
"custom",
profile_default_model="vendor/some-model",
prefer_cached_catalog=True,
)
assert mock_catalog.call_count == 0
assert result == ("vendor/some-model", "custom", True)
def test_fast_path_does_not_rewrite_unrelated_bare_model(self):
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(
"other-model",
"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 == ("other-model", "custom:my-proxy", False)
def test_fast_path_keeps_qualified_model_unchanged(self):
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(
"x-ai/grok-composer-2.5-fast",
"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 == ("x-ai/grok-composer-2.5-fast", "custom:my-proxy", False)
def test_openai_codex_stale_openai_slash_still_uses_slow_path(self):
"""Regression: openai/... under openai-codex must not take bare fast return."""
from api.routes import _resolve_compatible_session_model_state
with patch("api.routes.get_available_models") as mock_catalog:
mock_catalog.return_value = {
"active_provider": "openai-codex",
"default_model": "gpt-5.5",
"groups": [],
}
result = _resolve_compatible_session_model_state(
"openai/gpt-5.4-mini",
"openai-codex",
profile_default_model="gpt-5.5",
prefer_cached_catalog=True,
)
assert mock_catalog.call_count >= 1
assert result[0] == "gpt-5.5"
assert result[2] is True
class TestReadProfileModelConfigWithExplicitProvider:
def test_returns_profile_default_when_session_has_model_provider(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:my-proxy")
assert provider is None
assert default == "x-ai/grok-composer-2.5-fast"