fix(agent-init): forward custom_headers on OpenAI-wire branch - #28790
fix(agent-init): forward custom_headers on OpenAI-wire branch#28790shailensobhee wants to merge 1 commit into
Conversation
The anthropic_messages branch in init_agent() resolves custom_headers from custom_providers entries (matching by base_url) and forwards them to the Anthropic client as default_headers, so APIM-style gateways that require auth via a subscription header (e.g. Ocp-Apim-Subscription-Key) authenticate correctly. The OpenAI-wire (chat_completions / codex_responses) branch never had this lookup. It only sets default_headers for a fixed list of providers (OpenRouter, NVIDIA NIM, Routermint, Copilot, Kimi, Qwen Portal, Codex Cloudflare) plus a profile.default_headers fallback. For any other custom_providers entry served over the OpenAI wire — including the AMD LLM Gateway, Azure OpenAI behind APIM, and similar enterprise proxies — the subscription header was silently dropped and the gateway rejected every request with HTTP 401: Access denied due to missing subscription key. Make sure to include subscription key when making requests to an API. Because the fallback chain often cycles through several custom_providers entries that hit the same gateway, this manifests as every fallback failing with the same 401 (visible in ~/.hermes/sessions/request_dump_*.json — outbound headers contain only Authorization and Content-Type). Mirror the existing anthropic_messages lookup on the OpenAI-wire branch: match base_url against custom_providers, copy custom_headers into client_kwargs["default_headers"], and forward verify=False via an explicit httpx.Client for gateways behind self-signed certs. Verified end-to-end against AMD LLM Gateway across all three surfaces (Anthropic, OpenAI, OnPrem) — all return HTTP 200 after the fix. Existing tests in tests/run_agent/ continue to pass.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for chasing this. I verified the core premise on current main: the explicit OpenAI-wire init path still never reads custom_providers[*].custom_headers (agent/agent_init.py:750-781), and the later model.default_headers hook is only the separate model-level override (agent/agent_init.py:892-898). The PR is in the right neighborhood, but this patch is not complete yet.
Problems
- The lookup is only added to the explicit
api_key and base_urlinit branch. OpenAI-wire custom clients built through the shared resolver still construct clients without custom provider headers (agent/auxiliary_client.py:3567-3612,agent/auxiliary_client.py:3684-3725), so fallback/auxiliary/named-custom paths can still drop the subscription header. - Credential swaps rebuild headers through
_apply_client_headers_for_base_url, which still only applies host/profile/model.default_headersand would clear a custom-provider subscription header on an unknown gateway (run_agent.py:3890-3933). - No regression test covers a
custom_providersentry withcustom_headers; the existing tests cover provider attribution andmodel.default_headers, not this config shape (tests/run_agent/test_provider_attribution_headers.py:180-273).
Suggested changes
- Centralize custom-provider header/verify resolution and call it from init, resolver/fallback, auxiliary/named-custom, and credential-swap rebuild paths.
- Add tests for legacy
custom_providers.custom_headerson the main OpenAI-wire init path and at least one resolver/fallback path.
Automated hermes-sweeper review.
| # gateway doesn't recognise. Also forward `verify: false` for | ||
| # gateways behind self-signed certs. | ||
| if "default_headers" not in client_kwargs: | ||
| try: |
There was a problem hiding this comment.
Because this lookup lives only in the explicit startup branch, resolver-built custom clients and credential-swap rebuilds can still drop the same gateway header. This should probably be a shared custom-provider header resolver used by init, fallback/resolver, auxiliary, and _apply_client_headers_for_base_url rather than a one-off block here.
| if "default_headers" not in client_kwargs: | ||
| try: | ||
| from hermes_cli.config import load_config as _load_cp_cfg | ||
| _cp_cfg = _load_cp_cfg() |
There was a problem hiding this comment.
Reading raw load_config()["custom_providers"] bypasses the repo's compatible custom-provider view, so any equivalent provider entry represented through the newer providers schema will be missed unless the normalization layer is extended to preserve these fields.
|
Thanks for the gateway-auth fix. Current
For the configuration in this PR, use |
Summary
The
anthropic_messagesbranch ininit_agent()resolvescustom_headersfromcustom_providersentries (matching bybase_url) and forwards them to the Anthropic client asdefault_headers, so APIM-style gateways that require auth via a subscription header (e.g.Ocp-Apim-Subscription-Key) authenticate correctly.The OpenAI-wire branch (
chat_completions/codex_responses) never had this lookup. It only setsdefault_headersfor a fixed list of providers (OpenRouter, NVIDIA NIM, Routermint, Copilot, Kimi, Qwen Portal, Codex Cloudflare) plus aprofile.default_headersfallback. For any othercustom_providersentry served over the OpenAI wire — including the AMD LLM Gateway, Azure OpenAI behind APIM, and similar enterprise proxies — the subscription header was silently dropped and the gateway rejected every request withHTTP 401:Because the fallback chain often cycles through several
custom_providersentries that all share the same gateway, this manifests as every fallback failing with the same 401. Visible in~/.hermes/sessions/request_dump_*.json— outbound headers contain onlyAuthorizationandContent-Type, never the configuredcustom_headers.Fix
Mirror the existing
anthropic_messageslookup on the OpenAI-wire branch:base_urlagainstcustom_providersentries (case-insensitive, trailing slash normalized).custom_headersintoclient_kwargs["default_headers"].verify: falsevia an explicithttpx.Client(verify=False)for gateways behind self-signed certs.Guarded by
if "default_headers" not in client_kwargsso it never overrides the existing per-host branches (OpenRouter, Copilot, etc.) or aprofile.default_headersset earlier.Repro
config.yaml:Before this PR: every request returns HTTP 401 because
Ocp-Apim-Subscription-Keyis never sent.After this PR: request succeeds, header is forwarded.
Test Plan
pytest tests/run_agent/test_provider_attribution_headers.py tests/run_agent/test_callable_api_key.py— 26/26 passing.default_headersis already set by an earlier per-host branch (e.g. OpenRouter), the new block is a no-op.Notes
agent/agent_init.py(theanthropic_messagesbranch).