Files
hermes-webui/tests/test_issue5127_process_wakeup_bare_model.py
T
b3nwandb3nw a477069e3a fix(model): address gate feedback #4857080754 — slug names, list models, get_config, single YAML parse
- _repair_bare_custom_provider_model matches display-named providers via
  _custom_provider_slug_from_name (custom:my-proxy matches 'My Proxy').
- _ordered_custom_provider_model_ids now handles dict keys, list strings,
  and list dicts with id/model/name, aligned with api/config.py catalog.
- config_obj=None fallback uses get_config() instead of raw cfg.
- _read_profile_model_config returns profile config dict too, avoiding a
  second YAML parse on hot display paths.
- Tests added for slug matching, list-form models, and get_config fallback.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-01 20:12:38 +00:00

378 lines
14 KiB
Python

"""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_slow_path_repairs_bare_suffix_to_profile_default_for_custom_provider(self):
"""Regression #5225: async continuations may arrive without usable model_provider."""
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": "openrouter",
"default_model": "openai/gpt-5.5",
"groups": [],
}
result = _resolve_compatible_session_model_state(
"grok-composer-2.5-fast",
None,
profile_provider="custom:my-proxy",
profile_default_model="x-ai/grok-composer-2.5-fast",
prefer_cached_catalog=True,
)
assert mock_catalog.call_count == 1
assert result == ("x-ai/grok-composer-2.5-fast", "custom:my-proxy", True)
def test_fast_path_repairs_non_default_bare_model_of_custom_provider(self):
"""Regression: non-default bare model must be dynamically repaired if listed under the custom provider."""
from api.routes import _resolve_compatible_session_model_state
_mock_cfg = {
"custom_providers": [
{
"name": "my-proxy",
"base_url": "https://llm-proxy.ext.ben.io/v1",
"models": {
"x-ai/grok-composer-2.5-fast": {},
"umans/umans-glm-5.2": {},
}
}
]
}
with patch("api.routes.get_available_models") as mock_catalog, \
patch("api.config.cfg", _mock_cfg):
result = _resolve_compatible_session_model_state(
"grok-composer-2.5-fast",
"custom:my-proxy",
profile_provider="custom:my-proxy",
profile_default_model="umans/umans-glm-5.2",
profile_config=_mock_cfg,
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_slow_path_repairs_non_default_bare_model_of_custom_provider(self):
"""Regression: slow-path must also dynamically repair non-default bare model if listed under custom provider."""
from api.routes import _resolve_compatible_session_model_state
_mock_cfg = {
"custom_providers": [
{
"name": "my-proxy",
"base_url": "https://llm-proxy.ext.ben.io/v1",
"models": {
"x-ai/grok-composer-2.5-fast": {},
"umans/umans-glm-5.2": {},
}
}
]
}
with patch("api.routes.get_available_models") as mock_catalog, \
patch("api.config.cfg", _mock_cfg):
mock_catalog.return_value = {
"active_provider": "openrouter",
"default_model": "openai/gpt-5.5",
"groups": [],
}
result = _resolve_compatible_session_model_state(
"grok-composer-2.5-fast",
None,
profile_provider="custom:my-proxy",
profile_default_model="umans/umans-glm-5.2",
profile_config=_mock_cfg,
prefer_cached_catalog=True,
)
assert mock_catalog.call_count == 1
assert result == ("x-ai/grok-composer-2.5-fast", "custom:my-proxy", True)
def test_slow_path_does_not_rewrite_unrelated_bare_model_for_custom_provider(self):
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": "openrouter",
"default_model": "openai/gpt-5.5",
"groups": [],
}
result = _resolve_compatible_session_model_state(
"other-model",
None,
profile_provider="custom:my-proxy",
profile_default_model="x-ai/grok-composer-2.5-fast",
prefer_cached_catalog=True,
)
assert mock_catalog.call_count == 1
assert result == ("other-model", "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 TestRepairBareCustomProviderModel:
def test_uses_config_order_when_suffixes_collide(self):
from api.routes import _repair_bare_custom_provider_model
custom_cfg = [
{
"name": "llm-proxy",
"model": "org-a/shared-suffix",
"models": {"org-b/shared-suffix": {}},
}
]
with patch("api.config.cfg", {"custom_providers": custom_cfg}):
assert _repair_bare_custom_provider_model(
"shared-suffix", "custom:llm-proxy",
config_obj={"custom_providers": custom_cfg},
) == "org-a/shared-suffix"
def test_repairs_display_name_provider_via_slug(self):
from api.routes import _repair_bare_custom_provider_model
cfg = {
"custom_providers": [
{
"name": "My Proxy",
"models": {"x-ai/grok-composer-2.5-fast": {}},
}
]
}
assert _repair_bare_custom_provider_model(
"grok-composer-2.5-fast",
"custom:my-proxy",
config_obj=cfg,
) == "x-ai/grok-composer-2.5-fast"
def test_repairs_list_form_models_catalog(self):
from api.routes import _repair_bare_custom_provider_model
cfg = {
"custom_providers": [
{
"name": "team",
"models": [
"profile-list/shared",
{"id": "profile-list/other"},
],
}
]
}
assert _repair_bare_custom_provider_model(
"shared",
"custom:team",
config_obj=cfg,
) == "profile-list/shared"
def test_uses_get_config_when_config_obj_is_none(self):
from unittest.mock import patch
from api.routes import _repair_bare_custom_provider_model
cfg = {
"custom_providers": [
{
"name": "team",
"models": {"vendor/shared": {}},
}
]
}
with patch("api.config.get_config") as mock_get_config:
mock_get_config.return_value = cfg
result = _repair_bare_custom_provider_model(
"shared",
"custom:team",
config_obj=None,
)
assert result == "vendor/shared"
mock_get_config.assert_called_once_with()
def test_malformed_custom_provider_name_fails_closed(self):
from api.routes import _repair_bare_custom_provider_model
bad_cfg = {
"custom_providers": [
{"name": None, "models": {"vendor/ok-model": {}}},
{"name": "good-proxy", "models": {"vendor/ok-model": {}}},
]
}
assert _repair_bare_custom_provider_model(
"ok-model",
"custom:good-proxy",
config_obj=bad_cfg,
) == "vendor/ok-model"
def test_profile_config_scoped_not_global_cfg(self):
from api.routes import _repair_bare_custom_provider_model
profile_cfg = {
"custom_providers": [
{
"name": "team",
"models": {"profile-vendor/shared": {}},
}
]
}
global_cfg = {
"custom_providers": [
{
"name": "team",
"models": {"wrong-vendor/shared": {}},
}
]
}
with patch("api.config.cfg", global_cfg):
assert _repair_bare_custom_provider_model(
"shared",
"custom:team",
config_obj=profile_cfg,
) == "profile-vendor/shared"
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