fix(providers): honor per-provider TLS on custom /models and pricing probes
Per-provider ssl_ca_cert / ssl_verify reached the httpx chat client and the auxiliary clients (#56681), but the endpoint discovery and pricing probes did not. Both probe families resolved TLS from process-wide env vars only: - the requests-based metadata/pricing probe (agent/model_metadata.py::_resolve_requests_verify) - the urllib-based /models catalog probe (hermes_cli/models.py::probe_api_models) A custom endpoint whose chain verifies against the provider's configured bundle, but not the process SSL_CERT_FILE, then logged a spurious CERTIFICATE_VERIFY_FAILED on every probe even though the chat path worked. Pointing a global CA env var at the bundle fixes it but changes verification for every provider, defeating the point of a per-provider setting. This threads the selected provider's TLS settings into both probe paths, reusing get_custom_provider_tls_settings so there is no second precedence chain: - _resolve_requests_verify(base_url) looks up the provider's ssl_verify / ssl_ca_cert before falling back to the env vars. Callers with no base_url keep the exact env-only behavior. - probe_api_models builds an ssl.SSLContext from the provider settings and passes it through open_credentialed_url, which gains an ssl_context seam on the cloned secure opener. Unmatched or public endpoints pass None and keep urllib's default policy. Tests: tests/agent/test_custom_provider_ca_probes.py covers both probe families (provider CA, ssl_verify:false, unmatched, missing file, config lookup failure) plus end-to-end assertions that the resolved verify value and SSLContext actually reach the request seam. Verified against the neighboring metadata, pricing, TLS, and urllib-security suites (266 tests) with no regressions.
This commit is contained in:
+32
-13
@@ -48,18 +48,37 @@ def __getattr__(name: str):
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
def _resolve_requests_verify() -> bool | str:
|
||||
"""Resolve SSL verify setting for `requests` calls from env vars.
|
||||
def _resolve_requests_verify(base_url: str = "") -> bool | str:
|
||||
"""Resolve SSL verify setting for `requests` calls.
|
||||
|
||||
The `requests` library only honours REQUESTS_CA_BUNDLE / CURL_CA_BUNDLE
|
||||
by default. Hermes also honours HERMES_CA_BUNDLE (its own convention)
|
||||
and SSL_CERT_FILE (used by the stdlib `ssl` module and by httpx), so
|
||||
that a single env var can cover both `requests` and `httpx` callsites
|
||||
inside the same process.
|
||||
Priority (mirrors ``agent.ssl_verify.resolve_httpx_verify`` so the
|
||||
``requests``-based ``/models`` probes agree with the httpx chat client):
|
||||
|
||||
Returns either a filesystem path to a CA bundle, or True to defer to
|
||||
the requests default (certifi).
|
||||
1. Per-provider ``ssl_verify: false`` for ``base_url`` — disable verification.
|
||||
2. Per-provider ``ssl_ca_cert`` for ``base_url`` — an explicit CA bundle.
|
||||
Without this, a custom endpoint whose chain only verifies against the
|
||||
provider's configured bundle (not the process ``SSL_CERT_FILE``) logs a
|
||||
spurious CERTIFICATE_VERIFY_FAILED on every probe even though the chat
|
||||
path succeeds (per-provider ``ssl_ca_cert`` was reaching only httpx).
|
||||
3. Env vars ``HERMES_CA_BUNDLE`` / ``REQUESTS_CA_BUNDLE`` / ``SSL_CERT_FILE``
|
||||
(a single var covers both ``requests`` and ``httpx`` in-process).
|
||||
4. ``True`` — defer to the requests default (certifi).
|
||||
|
||||
``base_url`` is optional so existing callers (OpenRouter, etc.) keep the
|
||||
env-only behavior unchanged; only probes that pass a base_url pick up the
|
||||
per-provider override.
|
||||
"""
|
||||
if base_url:
|
||||
try:
|
||||
from hermes_cli.config import get_custom_provider_tls_settings
|
||||
tls = get_custom_provider_tls_settings(base_url)
|
||||
if tls.get("ssl_verify") is False:
|
||||
return False
|
||||
ca = tls.get("ssl_ca_cert")
|
||||
if isinstance(ca, str) and ca and os.path.isfile(ca):
|
||||
return ca
|
||||
except Exception:
|
||||
pass # fall through to env vars — never break a probe on config lookup
|
||||
for env_var in ("HERMES_CA_BUNDLE", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE"):
|
||||
val = os.getenv(env_var)
|
||||
if val and os.path.isfile(val):
|
||||
@@ -1261,7 +1280,7 @@ def fetch_endpoint_model_metadata(
|
||||
server_url.rstrip("/") + "/api/v1/models",
|
||||
headers=headers,
|
||||
timeout=(5, 10),
|
||||
verify=_resolve_requests_verify(),
|
||||
verify=_resolve_requests_verify(normalized),
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
@@ -1324,7 +1343,7 @@ def fetch_endpoint_model_metadata(
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=(5, 10),
|
||||
verify=_resolve_requests_verify(),
|
||||
verify=_resolve_requests_verify(normalized),
|
||||
stream=True,
|
||||
)
|
||||
if response.status_code in (401, 403):
|
||||
@@ -1364,7 +1383,7 @@ def fetch_endpoint_model_metadata(
|
||||
try:
|
||||
# Try /v1/props first (current llama.cpp); fall back to /props for older builds
|
||||
base = request_candidate.rstrip("/").replace("/v1", "")
|
||||
_verify = _resolve_requests_verify()
|
||||
_verify = _resolve_requests_verify(normalized)
|
||||
props_resp = requests.get(base + "/v1/props", headers=headers, timeout=5, verify=_verify)
|
||||
if not props_resp.ok:
|
||||
props_resp = requests.get(base + "/props", headers=headers, timeout=5, verify=_verify)
|
||||
@@ -2284,7 +2303,7 @@ def _query_anthropic_context_length(model: str, base_url: str, api_key: str) ->
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
_ensure_requests()
|
||||
resp = requests.get(url, headers=headers, timeout=(5, 10), verify=_resolve_requests_verify())
|
||||
resp = requests.get(url, headers=headers, timeout=(5, 10), verify=_resolve_requests_verify(base_url))
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
data = resp.json()
|
||||
|
||||
Reference in New Issue
Block a user