mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(cli): send User-Agent in /v1/models probe to pass Cloudflare 1010
Custom Claude proxies fronted by Cloudflare with Browser Integrity Check
enabled (e.g. `packyapi.com`) reject requests with the default
`Python-urllib/*` signature, returning HTTP 403 "error code: 1010".
`probe_api_models` swallowed that in its blanket `except Exception:
continue`, so `validate_requested_model` returned the misleading
"Could not reach the <provider> API to validate `<model>`" error even
though the endpoint is reachable and lists the requested model.
Advertise the probe request as `hermes-cli/<version>` so Cloudflare
treats it as a first-party client. This mirrors the pattern already used
by `agent/gemini_native_adapter.py` and `agent/anthropic_adapter.py`,
which set a descriptive UA for the same reason.
Reproduction (pre-fix):
python3 -c "
import urllib.request
req = urllib.request.Request(
'https://www.packyapi.com/v1/models',
headers={'Authorization': 'Bearer sk-...'})
urllib.request.urlopen(req).read()
"
urllib.error.HTTPError: HTTP Error 403: Forbidden
(body: b'error code: 1010')
Any non-urllib UA (Mozilla, curl, reqwest) returns 200 with the
OpenAI-compatible models listing.
Tested on macOS (Python 3.11). No cross-platform concerns — the change
is a single header addition to an existing `urllib.request.Request`.
This commit is contained in:
parent
6cdab70320
commit
23b81ab243
2 changed files with 67 additions and 1 deletions
|
|
@ -540,3 +540,63 @@ class TestValidateCodexAutoCorrection:
|
|||
assert result["recognized"] is False
|
||||
assert result.get("corrected_model") is None
|
||||
assert "not found" in result["message"]
|
||||
|
||||
|
||||
# -- probe_api_models — Cloudflare UA mitigation --------------------------------
|
||||
|
||||
class TestProbeApiModelsUserAgent:
|
||||
"""Probing custom /v1/models must send a Hermes User-Agent.
|
||||
|
||||
Some custom Claude proxies (e.g. ``packyapi.com``) sit behind Cloudflare with
|
||||
Browser Integrity Check enabled. The default ``Python-urllib/3.x`` signature
|
||||
is rejected with HTTP 403 ``error code: 1010``, which ``probe_api_models``
|
||||
swallowed into ``{"models": None}``, surfacing to users as a misleading
|
||||
"Could not reach the ... API to validate ..." error — even though the
|
||||
endpoint is reachable and the listing exists.
|
||||
"""
|
||||
|
||||
def _make_mock_response(self, body: bytes):
|
||||
from unittest.mock import MagicMock
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.__enter__ = MagicMock(return_value=mock_resp)
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_resp.read = MagicMock(return_value=body)
|
||||
return mock_resp
|
||||
|
||||
def test_probe_sends_hermes_user_agent(self):
|
||||
from unittest.mock import patch
|
||||
|
||||
body = b'{"data":[{"id":"claude-opus-4.7"}]}'
|
||||
with patch(
|
||||
"hermes_cli.models.urllib.request.urlopen",
|
||||
return_value=self._make_mock_response(body),
|
||||
) as mock_urlopen:
|
||||
result = probe_api_models("sk-test", "https://example.com/v1")
|
||||
|
||||
assert result["models"] == ["claude-opus-4.7"]
|
||||
# The urlopen call receives a Request object as its first positional arg
|
||||
req = mock_urlopen.call_args[0][0]
|
||||
ua = req.get_header("User-agent") # urllib title-cases header names
|
||||
assert ua, "probe_api_models must send a User-Agent header"
|
||||
assert ua.startswith("hermes-cli/"), (
|
||||
f"User-Agent must advertise hermes-cli, got {ua!r}"
|
||||
)
|
||||
# Must not fall back to urllib's default — that's what Cloudflare 1010 blocks.
|
||||
assert not ua.startswith("Python-urllib")
|
||||
|
||||
def test_probe_user_agent_sent_without_api_key(self):
|
||||
"""UA must be present even for endpoints that don't need auth."""
|
||||
from unittest.mock import patch
|
||||
|
||||
body = b'{"data":[]}'
|
||||
with patch(
|
||||
"hermes_cli.models.urllib.request.urlopen",
|
||||
return_value=self._make_mock_response(body),
|
||||
) as mock_urlopen:
|
||||
probe_api_models(None, "https://example.com/v1")
|
||||
|
||||
req = mock_urlopen.call_args[0][0]
|
||||
ua = req.get_header("User-agent")
|
||||
assert ua and ua.startswith("hermes-cli/")
|
||||
# No Authorization was set, but UA must still be present.
|
||||
assert req.get_header("Authorization") is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue