diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 4cc5016702..a67264fe1f 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2943,7 +2943,6 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op "auxiliary.free_only.", or_model, ) - _mark_provider_unhealthy("openrouter", ttl=60) return None, None if not _is_free_model(or_model): _warn_paid_lane_once(or_model) @@ -2969,8 +2968,15 @@ def _try_openrouter(explicit_api_key: str = None, model: str = None) -> Tuple[Op default_headers=build_or_headers()), or_model -def _describe_openrouter_unavailable() -> str: - """Return a more precise OpenRouter auth failure reason for logs.""" +def _describe_openrouter_unavailable(model: str = None) -> str: + """Return the policy or credential reason OpenRouter was unavailable.""" + free_only, cfg_model = _aux_openrouter_settings() + or_model = model or cfg_model + if free_only and not _is_free_model(or_model): + return ( + f"auxiliary.free_only rejected non-free model {or_model!r}; " + "the request was skipped before provider availability checks" + ) pool_present, entry = _select_pool_entry("openrouter") if pool_present: if entry is None: @@ -6489,11 +6495,14 @@ def resolve_provider_client( # ── OpenRouter ─────────────────────────────────────────── if provider == "openrouter": - client, default = _try_openrouter(explicit_api_key=explicit_api_key) + client, default = _try_openrouter( + explicit_api_key=explicit_api_key, + model=model, + ) if client is None: logger.warning( "resolve_provider_client: openrouter requested but %s", - _describe_openrouter_unavailable(), + _describe_openrouter_unavailable(model=model), ) return None, None final_model = _normalize_resolved_model(model or default, provider) diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 1d784efa1e..a345cac3ee 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -1042,6 +1042,51 @@ class TestOpenRouterPaidLaneGuard: assert model is None mock_openai.assert_not_called() + def test_resolver_forwards_explicit_free_model_to_gate(self, monkeypatch): + """The concrete OpenRouter route gates the caller's model, not its default.""" + monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") + with patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)), \ + patch("hermes_cli.config.load_config_readonly", + return_value={"auxiliary": {"free_only": True}}), \ + patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_client = MagicMock(name="openrouter_client") + mock_openai.return_value = mock_client + client, model = resolve_provider_client( + "openrouter", model="nvidia/nemotron-3-ultra-550b-a55b:free" + ) + + assert client is mock_client + assert model == "nvidia/nemotron-3-ultra-550b-a55b:free" + + def test_free_only_gate_does_not_mark_openrouter_unhealthy(self, monkeypatch): + monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") + with patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)), \ + patch("hermes_cli.config.load_config_readonly", + return_value={"auxiliary": {"free_only": True}}), \ + patch("agent.auxiliary_client._mark_provider_unhealthy") as mark_unhealthy: + client, model = resolve_provider_client( + "openrouter", model="google/gemini-3.6-flash" + ) + + assert client is None + assert model is None + mark_unhealthy.assert_not_called() + + def test_free_only_gate_reports_policy_not_credentials(self, monkeypatch, caplog): + import logging + + monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") + with patch("agent.auxiliary_client._select_pool_entry", return_value=(False, None)), \ + patch("hermes_cli.config.load_config_readonly", + return_value={"auxiliary": {"free_only": True}}), \ + caplog.at_level(logging.WARNING, logger="agent.auxiliary_client"): + resolve_provider_client("openrouter", model="google/gemini-3.6-flash") + + messages = [record.getMessage() for record in caplog.records] + assert any("free_only" in message and "google/gemini-3.6-flash" in message + for message in messages) + assert not any("credentials" in message for message in messages) + def test_paid_lane_warns_once(self, monkeypatch, caplog): """Engaging the default paid model logs a WARNING (once per model).""" import logging