feat(agent): allow user-supplied default_headers per provider in config.yaml - #18322
Closed
coldxiangyu163 wants to merge 1 commit into
Closed
Conversation
…ig.yaml Hermes hardcodes a host-specific if/elif chain for HTTP default_headers (openrouter, routermint, copilot, kimi, qwen, codex). Any other base_url falls through and the OpenAI Python SDK's default User-Agent (``OpenAI/Python <version>``) leaks unchanged. That UA is on the WAF block list of several third-party OpenAI-compatible relays — typically Chinese "new-api" channels. Such relays return HTTP 403 "Your request was blocked." for every Hermes call even though direct curl with any other UA succeeds, leaving users with no in-tree escape hatch short of a source patch. This adds a ``default_headers`` mapping that users can declare in ``config.yaml`` either at the top of the ``model:`` block (applies to whichever provider is active) or under a ``providers.<name>:`` entry (per-provider override). Per-provider entries override top-level on conflicts. Hermes' built-in host defaults are merged underneath so user keys win without erasing things like OpenRouter's ``HTTP-Referer`` attribution headers. Implementation: - ``hermes_cli/config.py``: add ``default_headers`` to the custom-provider schema and a shared ``_sanitize_default_headers`` validator that drops malformed entries with a warning instead of raising. - ``run_agent.py``: ``AIAgent._resolve_user_default_headers`` reads from ``model.default_headers`` and ``providers.<provider>.default_headers``; the result is merged into ``client_kwargs["default_headers"]`` both at init time and inside ``_apply_client_headers_for_base_url`` (so client rebuilds during ``/model`` switches and credential refresh keep the headers). - ``cli-config.yaml.example``: documents the new field next to the existing ``providers:`` timeout examples, including the WAF-bypass motivation. - ``tests/hermes_cli/test_default_headers.py``: 21 new tests covering sanitizer edge cases, schema flow-through, and resolver precedence. Verified locally on macOS 15 (darwin 25.2): with ``model.default_headers.User-Agent: claude-code/0.1.0`` the previously-403 relay now returns 200 end-to-end.
Collaborator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Adds a
default_headersconfig knob so users can override or augment the HTTP headers Hermes sends to OpenAI-compatible providers — without editing source.Today
run_agent.py(AIAgent.__init__and_apply_client_headers_for_base_url) hardcodes a host-specific if/elif chain that only setsdefault_headersfor six known hosts (openrouter, routermint, copilot, kimi, qwen, codex). For any otherbase_urlit falls through and the OpenAI Python SDK's defaultUser-Agent: OpenAI/Python <version>leaks unchanged.That User-Agent is on the WAF block list of several third-party OpenAI-compatible relays — typically Chinese "new-api" channels. Such relays return HTTP 403
Your request was blocked.for every Hermes call, even though directcurlwith any other UA succeeds against the very same endpoint. There is currently no in-tree escape hatch —_VALID_CUSTOM_PROVIDER_FIELDSdoes not include any header field, no env var injects one, and there is no--headerCLI flag.This PR makes that escape hatch official.
Related Issue
Fixes #
Type of Change
Changes Made
hermes_cli/config.pydefault_headersto_VALID_CUSTOM_PROVIDER_FIELDSand to the_KNOWN_KEYSset inside_normalize_custom_provider_entry._sanitize_default_headers(raw, source)— accepts a mapping of header name to scalar value, drops malformed entries with alogger.warning, returnsNonefor empty/invalid input.run_agent.pyAIAgent._resolve_user_default_headers()readsmodel.default_headers(applies to whichever provider is active) andproviders.<provider_name>.default_headers(per-provider override). Per-provider entries override top-level on conflicts.client_kwargs["default_headers"]at init time (after the existing host-specific chain and the OpenRouter Claude beta logic, so user keys win without erasing host-specific keys likeHTTP-Referer)._apply_client_headers_for_base_urlis refactored to assemblehost_headersfirst, merge user headers on top, and onlypop("default_headers")when the merged result is empty. This keeps the headers intact across/modelswitches and credential refreshes.cli-config.yaml.exampleproviders:block, with the WAF-bypass motivation and a working example.tests/hermes_cli/test_default_headers.py_normalize_custom_provider_entry, and_resolve_user_default_headersprecedence (top-level vs per-provider, case-insensitive provider matching, malformed config tolerance,load_configfailure swallowing).How to Test
New tests pass
21 passed locally.
Existing config / provider tests still pass (sanity check that schema additions didn't regress validation)
pytest tests/hermes_cli/test_config.py \ tests/hermes_cli/test_config_validation.py \ tests/hermes_cli/test_custom_provider_context_length.py \ tests/hermes_cli/test_custom_provider_model_switch.py \ tests/hermes_cli/test_user_providers_model_switch.py \ tests/hermes_cli/test_model_switch_custom_providers.py188 passed locally.
End-to-end against a real WAF-blocked relay
In
~/.hermes/config.yaml:Before the PR:
hermes chat -q "say pong"→HTTP 403: Your request was blocked.After the PR:
hermes chat -q "say pong"→pong. Verified locally on a "new-api" channel that blocksOpenAI/PythonUAs.Tested on
macOS 15 (darwin 25.2), Python 3.14, against a third-party new-api relay.
Checklist
Code
Documentation & Housekeeping
cli-config.yaml.examplewith the new config keydocs//CONTRIBUTING.md/AGENTS.mdnot affecteddictoperations, no FS / process / terminal touch)Notes for reviewers
default_headersproduce identical behavior to today (the new merge step is a no-op when_resolve_user_default_headers()returns{}). Existing tests confirm this.User-Agentif they really want to, but won't accidentally eraseHTTP-Referer/X-OpenRouter-Titleby setting a single key. Documented incli-config.yaml.exampleand the docstring on_resolve_user_default_headers.model.default_headersandproviders.<name>.default_headers) is the natural specificity rule; tested explicitly intest_per_provider_overrides_top_level_on_conflict._normalize_custom_provider_entryflow which also.warning()s on unknown keys instead of failing.boto3rather than the OpenAI SDK, mirroring the existing scope note next torequest_timeout_secondsincli-config.yaml.example.