diff --git a/tests/tui_gateway/test_make_agent_provider.py b/tests/tui_gateway/test_make_agent_provider.py index 0b147ee286ed..9cd5b0d5f143 100644 --- a/tests/tui_gateway/test_make_agent_provider.py +++ b/tests/tui_gateway/test_make_agent_provider.py @@ -60,6 +60,103 @@ def test_make_agent_passes_resolved_provider(): assert call_kwargs.kwargs["api_mode"] == "anthropic_messages" +def test_make_agent_forwards_provider_routing(): + """Parity with the messaging gateway + CLI: ``provider_routing`` in + config.yaml must reach AIAgent so OpenRouter honors the user's sort / + only / ignore / order / require_parameters / data_collection prefs. + + Regression for the desktop report (LewisDB): Discord respected + provider_routing but the desktop app (tui_gateway backend) built agents + with no routing prefs, so OpenRouter selected providers at random. + """ + + fake_runtime = { + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-test", + "api_mode": "chat_completions", + "command": None, + "args": None, + "credential_pool": None, + } + fake_cfg = { + "agent": {"system_prompt": ""}, + "model": {"default": "openrouter/some-model"}, + "provider_routing": { + "only": ["anthropic", "google"], + "ignore": ["deepinfra"], + "order": ["anthropic", "together"], + "sort": "throughput", + "require_parameters": True, + "data_collection": "deny", + }, + } + + with ( + patch("tui_gateway.server._load_cfg", return_value=fake_cfg), + patch("tui_gateway.server._get_db", return_value=MagicMock()), + patch("tui_gateway.server._load_reasoning_config", return_value=None), + patch("tui_gateway.server._load_service_tier", return_value=None), + patch("tui_gateway.server._load_enabled_toolsets", return_value=None), + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ), + patch("run_agent.AIAgent") as mock_agent, + ): + from tui_gateway.server import _make_agent + + _make_agent("sid-pr", "key-pr") + + kwargs = mock_agent.call_args.kwargs + assert kwargs["providers_allowed"] == ["anthropic", "google"] + assert kwargs["providers_ignored"] == ["deepinfra"] + assert kwargs["providers_order"] == ["anthropic", "together"] + assert kwargs["provider_sort"] == "throughput" + assert kwargs["provider_require_parameters"] is True + assert kwargs["provider_data_collection"] == "deny" + + +def test_make_agent_provider_routing_defaults_when_unset(): + """No ``provider_routing`` section → no routing prefs forwarded (None / + False), so behavior is unchanged for users who never configured it.""" + + fake_runtime = { + "provider": "openrouter", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "sk-or-test", + "api_mode": "chat_completions", + "command": None, + "args": None, + "credential_pool": None, + } + fake_cfg = {"agent": {"system_prompt": ""}, "model": {"default": "glm-5"}} + + with ( + patch("tui_gateway.server._load_cfg", return_value=fake_cfg), + patch("tui_gateway.server._get_db", return_value=MagicMock()), + patch("tui_gateway.server._load_reasoning_config", return_value=None), + patch("tui_gateway.server._load_service_tier", return_value=None), + patch("tui_gateway.server._load_enabled_toolsets", return_value=None), + patch( + "hermes_cli.runtime_provider.resolve_runtime_provider", + return_value=fake_runtime, + ), + patch("run_agent.AIAgent") as mock_agent, + ): + from tui_gateway.server import _make_agent + + _make_agent("sid-pr-default", "key-pr-default") + + kwargs = mock_agent.call_args.kwargs + assert kwargs["providers_allowed"] is None + assert kwargs["providers_ignored"] is None + assert kwargs["providers_order"] is None + assert kwargs["provider_sort"] is None + assert kwargs["provider_require_parameters"] is False + assert kwargs["provider_data_collection"] is None + + def test_make_agent_ignores_display_personality_without_system_prompt(): """The TUI matches the classic CLI: personality only becomes active once it has been saved to agent.system_prompt.""" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 4c1746b8f779..1228f0d9be0c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1693,6 +1693,20 @@ def _load_service_tier() -> str | None: return None +def _load_provider_routing() -> dict: + """OpenRouter provider-routing prefs from config.yaml (``provider_routing``). + + Parity with the messaging gateway (``gateway/run.py::_load_provider_routing``) + and the classic CLI: without this the desktop/TUI backend builds agents with + no routing prefs, so OpenRouter falls back to its default (effectively random) + provider selection even when the user configured ``provider_routing``. + """ + try: + return _load_cfg().get("provider_routing", {}) or {} + except Exception: + return {} + + def _load_show_reasoning() -> bool: return bool((_load_cfg().get("display") or {}).get("show_reasoning", False)) @@ -3308,6 +3322,7 @@ def _make_agent( requested=requested_provider, target_model=model or None, ) + _pr = _load_provider_routing() return AIAgent( model=model, max_iterations=_cfg_max_turns(cfg, 90), @@ -3335,6 +3350,15 @@ def _make_agent( else _load_service_tier() ), enabled_toolsets=_load_enabled_toolsets(), + # OpenRouter provider-routing prefs (config.yaml `provider_routing`). + # Mirrors the messaging gateway + CLI so the desktop/TUI honors the same + # routing instead of letting OpenRouter pick providers at random. + providers_allowed=_pr.get("only"), + providers_ignored=_pr.get("ignore"), + providers_order=_pr.get("order"), + provider_sort=_pr.get("sort"), + provider_require_parameters=_pr.get("require_parameters", False), + provider_data_collection=_pr.get("data_collection"), platform="tui", session_id=session_id or key, session_db=session_db if session_db is not None else _get_db(),