diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 5c9c8cd771d..499d9411d48 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -37,6 +37,7 @@ from tools.terminal_tool import is_persistent_env from utils import base_url_host_matches, base_url_hostname, env_float, env_int logger = logging.getLogger(__name__) +_OPENROUTER_PROVIDER_SORT_VALUES = {"throughput", "latency", "price"} def _ra(): @@ -115,6 +116,23 @@ def _is_openai_codex_backend(agent) -> bool: ) +def _validated_openrouter_provider_sort(raw_sort: Any) -> Optional[str]: + """Return a normalized OpenRouter provider.sort value or None.""" + if not isinstance(raw_sort, str): + return None + sort_value = raw_sort.strip().lower() + if not sort_value: + return None + if sort_value in _OPENROUTER_PROVIDER_SORT_VALUES: + return sort_value + logger.warning( + "Ignoring invalid OpenRouter provider.sort value %r (allowed: %s)", + raw_sort, + ", ".join(sorted(_OPENROUTER_PROVIDER_SORT_VALUES)), + ) + return None + + def _env_float(name: str, default: float) -> float: try: return float(os.getenv(name, str(default))) @@ -698,8 +716,9 @@ def build_api_kwargs(agent, api_messages: list) -> dict: _prefs["ignore"] = agent.providers_ignored if agent.providers_order: _prefs["order"] = agent.providers_order - if agent.provider_sort: - _prefs["sort"] = agent.provider_sort + _provider_sort = _validated_openrouter_provider_sort(agent.provider_sort) + if _provider_sort: + _prefs["sort"] = _provider_sort if agent.provider_require_parameters: _prefs["require_parameters"] = True if agent.provider_data_collection: @@ -1476,8 +1495,9 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str: provider_preferences["ignore"] = agent.providers_ignored if agent.providers_order: provider_preferences["order"] = agent.providers_order - if agent.provider_sort: - provider_preferences["sort"] = agent.provider_sort + _provider_sort = _validated_openrouter_provider_sort(agent.provider_sort) + if _provider_sort: + provider_preferences["sort"] = _provider_sort if provider_preferences and ( (agent.provider or "").strip().lower() == "openrouter" or agent._is_openrouter_url() diff --git a/cron/scheduler.py b/cron/scheduler.py index e469fddcd53..410e9d7dc77 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -2335,7 +2335,7 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: max_iterations = _cfg.get("agent", {}).get("max_turns") or _cfg.get("max_turns") or 90 # Provider routing - pr = _cfg.get("provider_routing", {}) + pr = _cfg.get("provider_routing") or {} from hermes_cli.runtime_provider import ( resolve_runtime_provider, diff --git a/tests/agent/test_chat_completion_helpers_provider_sort.py b/tests/agent/test_chat_completion_helpers_provider_sort.py new file mode 100644 index 00000000000..fa4f75d7444 --- /dev/null +++ b/tests/agent/test_chat_completion_helpers_provider_sort.py @@ -0,0 +1,13 @@ +from agent.chat_completion_helpers import _validated_openrouter_provider_sort + + +def test_validated_openrouter_provider_sort_accepts_valid_values(): + assert _validated_openrouter_provider_sort("price") == "price" + assert _validated_openrouter_provider_sort(" latency ") == "latency" + assert _validated_openrouter_provider_sort("THROUGHPUT") == "throughput" + + +def test_validated_openrouter_provider_sort_rejects_invalid_values(): + assert _validated_openrouter_provider_sort("intelligence") is None + assert _validated_openrouter_provider_sort("") is None + assert _validated_openrouter_provider_sort(None) is None diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index c60bf6e93db..b937c9cf1d5 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -1676,6 +1676,14 @@ class TestBuildApiKwargs: kwargs = agent._build_api_kwargs(messages) assert kwargs["extra_body"]["provider"]["only"] == ["Anthropic"] + def test_provider_preferences_drop_invalid_sort(self, agent): + agent.provider = "openrouter" + agent.base_url = "https://openrouter.ai/api/v1" + agent.provider_sort = "intelligence" + messages = [{"role": "user", "content": "hi"}] + kwargs = agent._build_api_kwargs(messages) + assert "sort" not in kwargs.get("extra_body", {}).get("provider", {}) + def test_reasoning_config_default_openrouter(self, agent): """Default reasoning config for OpenRouter should be medium.""" agent.provider = "openrouter" @@ -3492,6 +3500,20 @@ class TestHandleMaxIterations: kwargs = agent.client.chat.completions.create.call_args.kwargs assert kwargs["extra_body"]["provider"]["only"] == ["Anthropic"] + def test_summary_drops_invalid_provider_sort(self, agent): + agent.base_url = "https://openrouter.ai/api/v1" + agent._base_url_lower = agent.base_url.lower() + agent.provider = "openrouter" + agent.provider_sort = "intelligence" + agent.client.chat.completions.create.return_value = _mock_response(content="Summary") + agent._cached_system_prompt = "You are helpful." + + result = agent._handle_max_iterations([{"role": "user", "content": "do stuff"}], 60) + + assert result == "Summary" + kwargs = agent.client.chat.completions.create.call_args.kwargs + assert "sort" not in kwargs.get("extra_body", {}).get("provider", {}) + def test_codex_summary_sanitizes_orphan_tool_results(self, agent): agent.api_mode = "codex_responses" agent.provider = "openai-codex"