From 9a15fad0d6c370ba09bf87af92144f46ccd2bc4a Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:08:08 -0700 Subject: [PATCH] fix(web): preserve declared providers in model writes (#63058) Unify the named-provider fixes from #52506, #57185, #60337, and #60901 at the main-model normalization chokepoint. Co-authored-by: izumi0uu Co-authored-by: liuhao1024 Co-authored-by: Paulo Henrique --- hermes_cli/web_server.py | 25 +++++++++++- ...ain_model_custom_provider_normalization.py | 40 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/hermes_cli/test_main_model_custom_provider_normalization.py diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index d6fa607ac914..b733d7ad79c9 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -1023,19 +1023,42 @@ def _normalize_main_model_assignment(provider: str, model: str) -> tuple[str, st ``normalize_model_for_provider`` (e.g. ``anthropic/claude-opus-4.6`` on native anthropic → ``claude-opus-4-6``). """ + from hermes_cli.config import get_compatible_custom_providers from hermes_cli.models import _KNOWN_PROVIDER_NAMES, normalize_provider from hermes_cli.model_normalize import normalize_model_for_provider + from hermes_cli.providers import resolve_custom_provider, resolve_user_provider prov_in = (provider or "").strip() model_in = (model or "").strip() canonical = normalize_provider(prov_in) + # User-declared providers are real routing targets, not analytics vendor + # labels. Resolve them before the unknown-vendor fallback. ``providers:`` + # keeps its declared bare slug; ``custom_providers:`` canonicalizes both a + # bare display name and ``custom:`` to the durable custom slug. + try: + cfg = load_config() + except Exception: + cfg = {} + user_providers = cfg.get("providers") if isinstance(cfg, dict) else None + user_provider = resolve_user_provider( + prov_in, user_providers if isinstance(user_providers, dict) else {} + ) + custom_provider = resolve_custom_provider( + prov_in, + get_compatible_custom_providers(cfg) if isinstance(cfg, dict) else [], + ) + if user_provider is not None: + return user_provider.id, model_in + if custom_provider is not None: + return custom_provider.id, model_in + if canonical not in _KNOWN_PROVIDER_NAMES and "/" in model_in: # Vendor prefix posing as a provider (analytics fallback). Resolve # against the user's current provider when it's an aggregator that # serves vendor-prefixed slugs; otherwise default to openrouter. try: - cur_cfg = load_config().get("model", {}) + cur_cfg = cfg.get("model", {}) cur_provider = ( str(cur_cfg.get("provider", "") or "").strip().lower() if isinstance(cur_cfg, dict) else "" diff --git a/tests/hermes_cli/test_main_model_custom_provider_normalization.py b/tests/hermes_cli/test_main_model_custom_provider_normalization.py new file mode 100644 index 000000000000..beb846b16971 --- /dev/null +++ b/tests/hermes_cli/test_main_model_custom_provider_normalization.py @@ -0,0 +1,40 @@ +"""Dashboard main-model writes preserve declared provider identities.""" + +from unittest.mock import patch + +from hermes_cli.web_server import _normalize_main_model_assignment + + +def _normalize(config, provider, model="vendor/model-a"): + with patch("hermes_cli.web_server.load_config", return_value=config): + return _normalize_main_model_assignment(provider, model) + + +def test_providers_block_keeps_declared_bare_slug(): + result = _normalize( + {"providers": {"commandcode": {"base_url": "http://localhost:55990/v1"}}}, + "commandcode", + ) + + assert result == ("commandcode", "vendor/model-a") + + +def test_custom_provider_name_canonicalizes_to_durable_slug(): + config = { + "custom_providers": [ + {"name": "US Azure", "base_url": "http://localhost:18025/v1"} + ] + } + + assert _normalize(config, "US Azure") == ("custom:us-azure", "vendor/model-a") + assert _normalize(config, "custom:us-azure") == ( + "custom:us-azure", + "vendor/model-a", + ) + + +def test_unknown_vendor_still_uses_aggregator_fallback(): + assert _normalize({}, "unconfigured-vendor") == ( + "openrouter", + "vendor/model-a", + ) \ No newline at end of file