Skip to content

fix(agent): apply named custom provider extra_headers to auxiliary clients - #61346

Open
Fatmylin wants to merge 2 commits into
NousResearch:mainfrom
Fatmylin:fix/auxiliary-custom-provider-extra-headers
Open

fix(agent): apply named custom provider extra_headers to auxiliary clients#61346
Fatmylin wants to merge 2 commits into
NousResearch:mainfrom
Fatmylin:fix/auxiliary-custom-provider-extra-headers

Conversation

@Fatmylin

@Fatmylin Fatmylin commented Jul 9, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes a bug where a named custom_providers / providers entry's own extra_headers are silently dropped by the auxiliary client (background tasks: title generation, context compression, vision, goal judge/draft, kanban decompose/specify, trajectory compression, mini-swe runner, …), even though the main agent client sends them correctly.

Root cause: the main client applies per-provider headers via apply_custom_provider_extra_headers_to_client_kwargs (hermes_cli/config.py, wired in agent/agent_init.py and run_agent.py), but agent/auxiliary_client.py's named-custom-provider branch only called _apply_user_default_headers, which reads top-level model.default_headers / model.extra_headers and never reads custom_entry["extra_headers"] — even though _get_named_custom_provider already returns that field on the entry dict.

Impact: any header configured per provider (e.g. gateway/WAF auth like x-gateway-auth) applies to main-turn requests but is missing from every background request, so gateways that require it reject or misroute auxiliary traffic.

Fix: add _apply_custom_provider_own_extra_headers and apply it after _apply_user_default_headers on both OpenAI-wire construction sites of the named-custom-provider branch (the chat_completions/codex_responses path and the anthropic_messages SDK-missing fallback). Applying it last matches the main client's precedence, where per-provider extra_headers win over top-level model.default_headers.

Intentionally out of scope (matches main-client behavior, where run_agent.py skips extra_headers for anthropic_messages/bedrock_converse api_modes): the native AnthropicAuxiliaryClient path and the anonymous custom (OPENAI_BASE_URL) branch are unchanged.

Related Issue

No existing issue — searched open PRs/issues for extra_headers / custom provider headers; #28790 covers a different (main-client agent-init) header issue.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • agent/auxiliary_client.py: new helper _apply_custom_provider_own_extra_headers (merges the entry's extra_headers last, same precedence as the main client); called from the two OpenAI-wire header-construction sites in the named-custom-provider branch.
  • tests/agent/test_auxiliary_user_default_headers.py: new class TestAuxClientHonorsPerProviderExtraHeaders with 3 regression tests (header applied; per-provider wins over top-level model.default_headers on conflicting keys while non-conflicting keys survive; anthropic_messages SDK-missing fallback also honors the entry headers).

How to Test

  1. Repro (before the fix): configure a named custom provider with extra_headers (e.g. {"x-gateway-auth": "token"}), trigger any auxiliary task (e.g. title generation), and observe the header is absent from the request — while the main turn sends it.
  2. Run the regression tests: uv run --extra dev pytest tests/agent/test_auxiliary_user_default_headers.py -q → 11 passed (3 new). All 3 new tests fail on main with assert None == 'token' (header missing) and pass with this change.
  3. Related suites: pytest tests/agent/test_auxiliary_named_custom_providers.py tests/agent/test_auxiliary_client_anthropic_custom.py tests/agent/test_auxiliary_client.py -q → 333 passed, no regressions.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q — the touched suites all pass (see above); the full run has pre-existing, order-dependent failures in unrelated files that reproduce identically on main (verified by stashing this diff and re-running those files in isolation: they pass both with and without this change)
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.5 (Apple Silicon)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — docstrings on the new helper; no user-facing config change (N/A otherwise)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys; this makes an existing key work as documented)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — pure dict merge, no platform-specific behavior
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

$ uv run --extra dev pytest tests/agent/test_auxiliary_user_default_headers.py -q
...........                                                              [100%]
11 passed in 2.84s

New tests on main (before fix):

FAILED ...::TestAuxClientHonorsPerProviderExtraHeaders::test_entry_extra_headers_applied_to_aux_client - AssertionError: assert None == 'token'
FAILED ...::test_entry_extra_headers_win_over_top_level_default_headers - AssertionError: assert 'should-lose' == 'token'
FAILED ...::test_entry_extra_headers_applied_on_anthropic_messages_sdk_missing_fallback - AssertionError: assert None == 'token'

🤖 Generated with Claude Code

…ients

The main agent client applies a custom_providers entry's own
extra_headers via apply_custom_provider_extra_headers_to_client_kwargs,
but the auxiliary client (title generation, context compression, vision,
goal judge, etc.) only merged top-level model.default_headers /
model.extra_headers and never read custom_entry["extra_headers"].
Headers used for gateway auth were silently dropped from every
background request.

Merge the entry's extra_headers last (same precedence as the main
client) on both OpenAI-wire construction sites of the named
custom-provider branch: the chat_completions/codex_responses path and
the anthropic_messages SDK-missing fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/openai OpenAI / Codex Responses API area/config Config system, migrations, profiles P3 Low — cosmetic, nice to have labels Jul 9, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing the named-provider path and adding focused sync regressions.

Problems

  • The new merge at agent/auxiliary_client.py:4771 configures only the sync OpenAI client. With async_mode=True, the named-provider branch converts that client through _to_async_client (agent/auxiliary_client.py:4804), which rebuilds AsyncOpenAI kwargs and only reapplies _apply_user_default_headers (agent/auxiliary_client.py:4355-4357). The entry-specific header is therefore lost for async auxiliary calls.

Suggested changes

  • Carry the named entry's normalized extra_headers into _to_async_client, or merge them into its async kwargs after top-level defaults.
  • Add an async_mode=True regression test asserting the constructed async client receives the entry header and its precedence.

Automated hermes-sweeper review.

Comment thread agent/auxiliary_client.py
@@ -4745,6 +4771,7 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
_clean_base2, _dq2 = _extract_url_query_params(openai_base)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This configures the sync client only. With async_mode=True, this branch immediately rebuilds an AsyncOpenAI client via _to_async_client; that helper reapplies only top-level defaults (agent/auxiliary_client.py:4355-4357) and has no access to custom_entry, so this per-provider header is still lost. Please thread or reapply it on the async construction path and add an async regression.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — fixed in 65ee781. _to_async_client now takes an optional custom_entry and re-applies the entry's extra_headers last (same precedence as the sync path), threaded from both named-custom-provider return sites.

While fixing this we found two more async exits with the same loss: fallback_chain entries resolved sync by _resolve_fallback_entry and converted at their call sites, and the vision auto-detect _finalize conversion — neither has the entry in hand. For those, _to_async_client now self-looks-up the entry by the sync client's base_url via get_custom_provider_extra_headers (the same matching apply_custom_provider_extra_headers_to_client_kwargs uses on the main client). The named branch keeps the explicit parameter because its anthropic-fallback URL is rewritten (/anthropic/v1) and would miss the base_url match.

Async regressions added: two through resolve_provider_client(..., async_mode=True) (header applied + precedence over top-level model.default_headers) and direct _to_async_client self-lookup tests (matching and non-matching base_url) covering the fallback/vision exits. All red before the fix (assert None == 'token'), 15 passing after; the named-custom-provider suites (356 tests) show no regressions.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
…t rebuilds

Review follow-up (NousResearch#61346): _to_async_client rebuilds AsyncOpenAI from
scratch and reconstructs default_headers without access to the resolved
custom_providers entry, so per-provider extra_headers were still lost
whenever async_mode=True.

Thread the resolved entry through the two named-custom-provider return
sites, and for every other _to_async_client call site (fallback_chain,
vision auto-detect) self-look-up the entry by the sync client's base_url
via get_custom_provider_extra_headers — the same matching the main
client uses. The named branch keeps the explicit parameter because its
anthropic-fallback URL is rewritten (/anthropic -> /v1) and would miss
the base_url match. Precedence is unchanged: per-provider headers apply
last, over model.default_headers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ciberitch

Copy link
Copy Markdown

Also hitting this — same repro as #84223 (Cloudflare Access extra_headers silently dropped by the aux client on a named custom provider). +1 for merging one of these to consolidate the duplicate fixes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have provider/openai OpenAI / Codex Responses API sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants