diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index c539fc69528..1b9d857140a 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -902,6 +902,108 @@ def test_startup_runtime_detects_provider_for_model_env(monkeypatch): ) +def test_load_fallback_model_prefers_fallback_providers(monkeypatch): + fallback_chain = [ + {"provider": "openrouter", "model": "openai/gpt-5.5"}, + {"provider": "anthropic", "model": "claude-sonnet-4-6"}, + ] + monkeypatch.setattr( + server, + "_load_cfg", + lambda: { + "fallback_model": {"provider": "legacy", "model": "legacy-model"}, + "fallback_providers": fallback_chain, + }, + ) + + assert server._load_fallback_model() == fallback_chain + + +def test_make_agent_passes_configured_fallback_chain(monkeypatch): + captured = {} + fallback_chain = [ + {"provider": "openrouter", "model": "openai/gpt-5.5"}, + ] + + def fake_agent(**kwargs): + captured.update(kwargs) + return types.SimpleNamespace(model=kwargs.get("model")) + + monkeypatch.delenv("HERMES_MODEL", raising=False) + monkeypatch.delenv("HERMES_INFERENCE_MODEL", raising=False) + monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False) + monkeypatch.setattr( + server, + "_load_cfg", + lambda: { + "model": {"default": "gpt-5.5", "provider": "openai-codex"}, + "fallback_providers": fallback_chain, + }, + ) + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + lambda requested=None, target_model=None: { + "provider": "openai-codex", + "base_url": "https://chatgpt.com/backend-api/codex", + "api_key": "token", + "api_mode": "codex_responses", + "credential_pool": None, + }, + ) + monkeypatch.setattr("run_agent.AIAgent", fake_agent) + monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: ["file"]) + monkeypatch.setattr(server, "_get_db", lambda: None) + + agent = server._make_agent("sid", "session-key") + + assert agent.model == "gpt-5.5" + assert captured["fallback_model"] == fallback_chain + assert captured["platform"] == "tui" + + +def test_background_agent_kwargs_preserves_full_fallback_chain(monkeypatch): + chain = [ + {"provider": "openrouter", "model": "openai/gpt-5.5"}, + {"provider": "anthropic", "model": "claude-sonnet-4-6"}, + ] + agent = types.SimpleNamespace( + model="gpt-5.5", + provider="openai-codex", + _fallback_chain=chain, + ) + monkeypatch.setattr(server, "_load_cfg", lambda: {"max_turns": 25}) + monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: ["file"]) + monkeypatch.setattr(server, "_get_db", lambda: None) + + kwargs = server._background_agent_kwargs(agent, "task-id") + + assert kwargs["fallback_model"] == chain + + +def test_background_agent_kwargs_preserves_empty_fallback_chain(monkeypatch): + agent = types.SimpleNamespace( + model="gpt-5.5", + provider="anthropic", + _fallback_chain=[], + ) + monkeypatch.setattr( + server, + "_load_cfg", + lambda: { + "max_turns": 25, + "fallback_providers": [ + {"provider": "openrouter", "model": "openai/gpt-5.5"}, + ], + }, + ) + monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: ["file"]) + monkeypatch.setattr(server, "_get_db", lambda: None) + + kwargs = server._background_agent_kwargs(agent, "task-id") + + assert kwargs["fallback_model"] == [] + + def test_startup_runtime_resolves_short_alias_without_network(monkeypatch): monkeypatch.setenv("HERMES_MODEL", "sonnet") monkeypatch.delenv("HERMES_TUI_PROVIDER", raising=False) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 75e629c9b56..7d9e2fd7c57 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -2587,6 +2587,34 @@ def _parse_tui_skills_env() -> list[str]: return skills +def _load_fallback_model(): + """Return the configured fallback chain for TUI-created agents. + + Keep this in parity with ``HermesCLI.__init__``: prefer the new + ``fallback_providers`` list and accept the legacy single-dict + ``fallback_model`` shape. + """ + cfg = _load_cfg() + fb = cfg.get("fallback_providers") or cfg.get("fallback_model") or [] + if isinstance(fb, dict): + fb = [fb] if fb.get("provider") and fb.get("model") else [] + if isinstance(fb, list): + return [ + f for f in fb + if isinstance(f, dict) and f.get("provider") and f.get("model") + ] + return [] + + +def _agent_fallback_model(agent): + """Return an agent's fallback chain without rehydrating deliberately empty chains.""" + if hasattr(agent, "_fallback_chain"): + return getattr(agent, "_fallback_chain") or [] + if hasattr(agent, "_fallback_model"): + return getattr(agent, "_fallback_model", None) + return _load_fallback_model() + + def _background_agent_kwargs(agent, task_id: str) -> dict: cfg = _load_cfg() @@ -2621,7 +2649,7 @@ def _background_agent_kwargs(agent, task_id: str) -> dict: "request_overrides": dict(getattr(agent, "request_overrides", {}) or {}), "platform": "tui", "session_db": _get_db(), - "fallback_model": getattr(agent, "_fallback_model", None), + "fallback_model": _agent_fallback_model(agent), } @@ -2880,6 +2908,7 @@ def _make_agent( pass_session_id=is_truthy_value(os.environ.get("HERMES_TUI_PASS_SESSION_ID")), skip_context_files=is_truthy_value(os.environ.get("HERMES_IGNORE_RULES")), skip_memory=is_truthy_value(os.environ.get("HERMES_IGNORE_RULES")), + fallback_model=_load_fallback_model(), **_agent_cbs(sid), )