merge #5128 custom-provider bare-model wakeup repair
This commit is contained in:
commit
1a4dcd4fb5
2 changed files with 199 additions and 9 deletions
|
|
@ -5215,31 +5215,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 _clean_session_model_provider(requested_provider) or not getattr(session, "profile", None):
|
||||
if not getattr(session, "profile", None):
|
||||
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,
|
||||
|
|
@ -5303,6 +5324,23 @@ 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()
|
||||
_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 _providers_match_for_repair
|
||||
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
|
||||
|
|
|
|||
152
tests/test_issue5127_process_wakeup_bare_model.py
Normal file
152
tests/test_issue5127_process_wakeup_bare_model.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"""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_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 == ("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
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
Loading…
Reference in a new issue