fix(agent): apply named custom provider extra_headers to auxiliary clients - #61346
fix(agent): apply named custom provider extra_headers to auxiliary clients#61346Fatmylin wants to merge 2 commits into
Conversation
…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>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the named-provider path and adding focused sync regressions.
Problems
- The new merge at
agent/auxiliary_client.py:4771configures only the sync OpenAI client. Withasync_mode=True, the named-provider branch converts that client through_to_async_client(agent/auxiliary_client.py:4804), which rebuildsAsyncOpenAIkwargs 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_headersinto_to_async_client, or merge them into its async kwargs after top-level defaults. - Add an
async_mode=Trueregression test asserting the constructed async client receives the entry header and its precedence.
Automated hermes-sweeper review.
| @@ -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) | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…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>
|
Also hitting this — same repro as #84223 (Cloudflare Access |
What does this PR do?
Fixes a bug where a named
custom_providers/providersentry's ownextra_headersare 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 inagent/agent_init.pyandrun_agent.py), butagent/auxiliary_client.py's named-custom-provider branch only called_apply_user_default_headers, which reads top-levelmodel.default_headers/model.extra_headersand never readscustom_entry["extra_headers"]— even though_get_named_custom_provideralready 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_headersand apply it after_apply_user_default_headerson 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-providerextra_headerswin over top-levelmodel.default_headers.Intentionally out of scope (matches main-client behavior, where
run_agent.pyskips extra_headers foranthropic_messages/bedrock_converseapi_modes): the nativeAnthropicAuxiliaryClientpath and the anonymouscustom(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-clientagent-init) header issue.Type of Change
Changes Made
agent/auxiliary_client.py: new helper_apply_custom_provider_own_extra_headers(merges the entry'sextra_headerslast, 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 classTestAuxClientHonorsPerProviderExtraHeaderswith 3 regression tests (header applied; per-provider wins over top-levelmodel.default_headerson conflicting keys while non-conflicting keys survive; anthropic_messages SDK-missing fallback also honors the entry headers).How to Test
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.uv run --extra dev pytest tests/agent/test_auxiliary_user_default_headers.py -q→ 11 passed (3 new). All 3 new tests fail onmainwithassert None == 'token'(header missing) and pass with this change.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
fix(scope):,feat(scope):, etc.)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 onmain(verified by stashing this diff and re-running those files in isolation: they pass both with and without this change)Documentation & Housekeeping
docs/, docstrings) — docstrings on the new helper; no user-facing config change (N/A otherwise)cli-config.yaml.exampleif I added/changed config keys — N/A (no new config keys; this makes an existing key work as documented)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
New tests on
main(before fix):🤖 Generated with Claude Code