hermes - 💡(How to fix) Fix `profile.default_headers` silently dropped on OpenRouter path (header replace, not merge) [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When a profile in ~/.hermes/profiles/<agent>/config.yaml declares custom default_headers, those headers are silently dropped on every request whose base_url matches openrouter.ai. The cause is that run_agent.py::AIAgent._apply_client_headers_for_base_url (and the parallel client-construction site at lines 1448-1450) replaces the client's default_headers with build_or_headers() instead of merging.

This blocks per-request attribution use cases (per-venture cost rollups, multi-tenant accounting, custom Referer for affiliate attribution) on the OpenRouter path specifically.

Root Cause

In run_agent.py:

  • Line 6266-6267 inside _apply_client_headers_for_base_url:
    if base_url_host_matches(base_url, "openrouter.ai"):
        self._client_kwargs["default_headers"] = build_or_headers()
  • Lines 1448-1450 at the OpenAI client construction site:
    if base_url_host_matches(effective_base, "openrouter.ai"):
        from agent.auxiliary_client import build_or_headers
        client_kwargs["default_headers"] = build_or_headers()

Both sites assign the OR base headers (defined in agent/auxiliary_client.py::_OR_HEADERS_BASE at lines 311-315) to default_headers without merging in provider_profile.default_headers. Notably, the non-OR branch in the same function (lines 6285-6298) already falls back to provider_profile.default_headers, so the OR branch is the inconsistent one.

The same replace pattern exists for the other recognised hosts (ai-gateway.vercel.sh, api.routermint.com, api.githubcopilot.com, api.kimi.com, portal.qwen.ai, chatgpt.com); this issue scopes to OpenRouter for review tractability, but the fix is straightforward to extend if maintainers want.

Fix Action

Fixed

Code Example

default_headers:
     HTTP-Referer: my-org
     X-Title: my-tenant:myagent

---

HTTP-Referer: https://hermes-agent.nousresearch.com
X-Title: Hermes Agent
X-OpenRouter-Categories: productivity,cli-agent

---

HTTP-Referer: my-org
X-Title: my-tenant:myagent
X-OpenRouter-Categories: productivity,cli-agent

---

if base_url_host_matches(base_url, "openrouter.ai"):
      self._client_kwargs["default_headers"] = build_or_headers()

---

if base_url_host_matches(effective_base, "openrouter.ai"):
      from agent.auxiliary_client import build_or_headers
      client_kwargs["default_headers"] = build_or_headers()
RAW_BUFFERClick to expand / collapse

Summary

When a profile in ~/.hermes/profiles/<agent>/config.yaml declares custom default_headers, those headers are silently dropped on every request whose base_url matches openrouter.ai. The cause is that run_agent.py::AIAgent._apply_client_headers_for_base_url (and the parallel client-construction site at lines 1448-1450) replaces the client's default_headers with build_or_headers() instead of merging.

This blocks per-request attribution use cases (per-venture cost rollups, multi-tenant accounting, custom Referer for affiliate attribution) on the OpenRouter path specifically.

Reproduction

hermes-agent 0.12.0, Python 3.13.

  1. In ~/.hermes/profiles/myagent/config.yaml (or the equivalent provider-profile mechanism), set:
    default_headers:
      HTTP-Referer: my-org
      X-Title: my-tenant:myagent
  2. Configure base_url: https://openrouter.ai/api/v1.
  3. Send any request and capture the outgoing HTTP headers (e.g. via mitmproxy or HTTPX_LOG_LEVEL=trace).

Observed:

HTTP-Referer: https://hermes-agent.nousresearch.com
X-Title: Hermes Agent
X-OpenRouter-Categories: productivity,cli-agent

Expected (caller intent honoured):

HTTP-Referer: my-org
X-Title: my-tenant:myagent
X-OpenRouter-Categories: productivity,cli-agent

Root cause

In run_agent.py:

  • Line 6266-6267 inside _apply_client_headers_for_base_url:
    if base_url_host_matches(base_url, "openrouter.ai"):
        self._client_kwargs["default_headers"] = build_or_headers()
  • Lines 1448-1450 at the OpenAI client construction site:
    if base_url_host_matches(effective_base, "openrouter.ai"):
        from agent.auxiliary_client import build_or_headers
        client_kwargs["default_headers"] = build_or_headers()

Both sites assign the OR base headers (defined in agent/auxiliary_client.py::_OR_HEADERS_BASE at lines 311-315) to default_headers without merging in provider_profile.default_headers. Notably, the non-OR branch in the same function (lines 6285-6298) already falls back to provider_profile.default_headers, so the OR branch is the inconsistent one.

The same replace pattern exists for the other recognised hosts (ai-gateway.vercel.sh, api.routermint.com, api.githubcopilot.com, api.kimi.com, portal.qwen.ai, chatgpt.com); this issue scopes to OpenRouter for review tractability, but the fix is straightforward to extend if maintainers want.

Expected behaviour

profile.default_headers should merge on top of the library defaults returned by build_or_headers(), with the profile's keys winning on conflict. Rationale:

  • Caller intent should override library defaults.
  • The keys most likely to conflict (HTTP-Referer, X-Title) are exactly the keys callers want to override for attribution.
  • The cache-related X-OpenRouter-Cache* headers are not user-set and survive the merge unchanged.
  • Symmetric with the existing else-branch fallback at lines 6285-6298.

Proposed fix

Merge profile.default_headers on top of build_or_headers() at both call sites. Profile wins on conflict. The change is ~10 lines added, zero removed. It is strictly additive: when the profile has no default_headers, behaviour is unchanged.

A draft PR is attached: <link to companion PR>.

Test coverage

The PR adds two new test cases to tests/run_agent/test_provider_attribution_headers.py:

  1. test_openrouter_merges_profile_default_headers — profile keys win on conflict, non-conflicting OR base headers survive.
  2. test_openrouter_no_profile_headers_unchanged — regression test confirming the fix doesn't change behaviour for users who haven't set profile headers.

All existing tests in the module continue to pass unchanged.

Environment

  • hermes-agent 0.12.0
  • Python 3.13
  • openai 2.21.x
  • Linux

Cross-reference

Internally tracked as a downstream activation blocker (B-043 cost telemetry per-venture attribution). Happy to provide additional context on the use case if useful.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING