diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 43a368ce81..1610c52939 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -112,6 +112,9 @@ def test_startup_runtime_does_not_treat_inference_provider_as_explicit(monkeypat monkeypatch.setenv("HERMES_MODEL", "nous/hermes-test") monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False) monkeypatch.setenv("HERMES_INFERENCE_PROVIDER", "nous") + monkeypatch.setattr( + server, "_detect_static_provider_for_model", lambda model, provider: None + ) assert server._resolve_startup_runtime() == ("nous/hermes-test", None) @@ -127,7 +130,7 @@ def test_startup_runtime_detects_provider_for_model_env(monkeypatch): assert current_provider == "auto" return "anthropic", "anthropic/claude-sonnet-4.6" - monkeypatch.setattr("hermes_cli.models.detect_provider_for_model", fake_detect) + monkeypatch.setattr(server, "_detect_static_provider_for_model", fake_detect) assert server._resolve_startup_runtime() == ( "anthropic/claude-sonnet-4.6", @@ -135,6 +138,22 @@ def test_startup_runtime_detects_provider_for_model_env(monkeypatch): ) +def test_startup_runtime_does_not_call_network_detector(monkeypatch): + monkeypatch.setenv("HERMES_MODEL", "sonnet") + monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False) + monkeypatch.delenv("HERMES_INFERENCE_PROVIDER", raising=False) + monkeypatch.setattr(server, "_load_cfg", lambda: {"model": {"provider": "auto"}}) + monkeypatch.setattr( + "hermes_cli.models.detect_provider_for_model", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("network detector called")), + ) + + model, provider = server._resolve_startup_runtime() + + assert model + assert provider in {None, "anthropic"} + + def _session(agent=None, **extra): return { "agent": agent if agent is not None else types.SimpleNamespace(), diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 9f188de645..557fec19c2 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -574,6 +574,48 @@ def _resolve_model() -> str: return "anthropic/claude-sonnet-4" +def _detect_static_provider_for_model(model_name: str, current_provider: str) -> tuple[str, str] | None: + """Startup-safe provider detection: static catalogs only, no network fetches.""" + name = (model_name or "").strip() + if not name: + return None + + try: + from hermes_cli.models import ( + _PROVIDER_ALIASES, + _PROVIDER_LABELS, + _PROVIDER_MODELS, + normalize_provider, + ) + except Exception: + return None + + name_lower = name.lower() + normalized_current = normalize_provider(current_provider) + resolved_provider = _PROVIDER_ALIASES.get(name_lower, name_lower) + if resolved_provider not in {"custom", "openrouter"}: + default_models = _PROVIDER_MODELS.get(resolved_provider, []) + if ( + resolved_provider in _PROVIDER_LABELS + and default_models + and resolved_provider != normalized_current + ): + return resolved_provider, default_models[0] + + aggregators = {"nous", "openrouter", "ai-gateway", "copilot", "kilocode"} + current_models = _PROVIDER_MODELS.get(normalized_current, []) + if any(name_lower == m.lower() for m in current_models): + return None + + for provider, models in _PROVIDER_MODELS.items(): + if provider == normalized_current or provider in aggregators: + continue + if any(name_lower == m.lower() for m in models): + return provider, name + + return None + + def _resolve_startup_runtime() -> tuple[str, str | None]: model = _resolve_model() explicit_provider = os.environ.get("HERMES_TUI_PROVIDER", "").strip() @@ -588,15 +630,13 @@ def _resolve_startup_runtime() -> tuple[str, str | None]: return model, None try: - from hermes_cli.models import detect_provider_for_model - cfg = _load_cfg().get("model") or {} current_provider = ( str(cfg.get("provider") or "").strip().lower() if isinstance(cfg, dict) else "" ) or os.environ.get("HERMES_INFERENCE_PROVIDER", "").strip().lower() or "auto" - detected = detect_provider_for_model(explicit_model, current_provider) + detected = _detect_static_provider_for_model(explicit_model, current_provider) if detected: provider, detected_model = detected return detected_model, provider