diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index b4edb01458..d074dac14d 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -112,6 +112,7 @@ from agent.model_metadata import ( strip_codex_context_variant_suffix as _strip_codex_ctx_variant, ) from hermes_cli.config import get_hermes_home +from hermes_cli.route_identity import normalize_route_base_url from hermes_constants import OPENROUTER_BASE_URL from utils import base_url_host_matches, base_url_hostname, env_float, is_truthy_value, model_forces_max_completion_tokens, normalize_proxy_env_vars @@ -2903,7 +2904,7 @@ def _normalize_chain_label(provider: str) -> str: def _unhealthy_cache_key(provider: str, base_url: Optional[str] = None) -> Any: """Provider-wide key, or endpoint-specific key for an explicit custom endpoint.""" label = _normalize_chain_label(provider) - endpoint = _custom_health_base_url(provider, base_url).lower().rstrip("/") + endpoint = normalize_route_base_url(_custom_health_base_url(provider, base_url)) if endpoint: return "custom-endpoint", endpoint return label diff --git a/agent/backend_identity.py b/agent/backend_identity.py index ecf9f2f66b..434a8a57e4 100644 --- a/agent/backend_identity.py +++ b/agent/backend_identity.py @@ -15,6 +15,8 @@ from dataclasses import dataclass from enum import Enum from typing import Optional +from hermes_cli.route_identity import normalize_route_base_url + logger = logging.getLogger(__name__) @@ -51,7 +53,8 @@ class BackendIdentity: base_url: Optional[str] = None, ) -> "BackendIdentity": return cls( - provider=_norm(provider), model=_norm(model), base_url=_norm(base_url).rstrip("/"), + provider=_norm(provider), model=_norm(model), + base_url=normalize_route_base_url(base_url), ) diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index a5875c7504..4054fc7466 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -4156,6 +4156,19 @@ class TestAuxUnhealthyCache: or_try.assert_not_called() custom_try.assert_not_called() + def test_custom_health_url_identity_preserves_path_and_query_case(self): + from agent.auxiliary_client import _is_provider_unhealthy, _mark_provider_unhealthy + + _mark_provider_unhealthy("custom", base_url="https://Example.test/API/v1/") + + assert _is_provider_unhealthy("custom", "https://example.TEST/API/v1") is True + assert _is_provider_unhealthy("custom", "https://example.test/api/v1") is False + + _mark_provider_unhealthy("custom", base_url="https://example.test/API/v1?token=AbC") + assert _is_provider_unhealthy( + "custom", "https://example.test/API/v1?token=abc", + ) is False + def test_call_llm_marks_provider_unhealthy_on_402(self, monkeypatch): """A 402 from call_llm causes the provider to be marked unhealthy so the next call skips it instead of re-trying the same depleted diff --git a/tests/agent/test_backend_identity.py b/tests/agent/test_backend_identity.py index 4cb6360975..1cc9db84b8 100644 --- a/tests/agent/test_backend_identity.py +++ b/tests/agent/test_backend_identity.py @@ -85,10 +85,24 @@ class TestSameCredentialSurface: class TestSameEndpoint: def test_same_explicit_url_is_same_endpoint(self): a = _id("a", "m1", "http://host:8000/v1/") - b = _id("b", "m2", "http://HOST:8000/v1") # trailing slash + case + b = _id("b", "m2", "http://HOST:8000/v1") # trailing slash + host case assert same_endpoint(a, b) assert should_skip_candidate(a, b, FailureScope.ENDPOINT) + def test_path_and_query_case_distinguish_endpoints(self): + failed = _id("custom", "m", "https://Example.test/API/v1?token=AbC") + + assert not should_skip_candidate( + _id("custom", "m", "https://example.test/api/v1?token=AbC"), + failed, + FailureScope.ENDPOINT, + ) + assert not should_skip_candidate( + _id("custom", "m", "https://example.test/API/v1?token=abc"), + failed, + FailureScope.ENDPOINT, + ) + def test_different_urls_are_different_endpoints(self): assert not same_endpoint( _id("a", "m", "http://h1/v1"), _id("a2", "m", "http://h2/v1")