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:
SHL0MS
2026-07-17 18:19:42 -04:00
committed by Teknium
parent cbfe186da8
commit b21e0bd8c9
4 changed files with 411 additions and 19 deletions
+32 -13
View File
@@ -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()