Skip to content

fix(aux): preserve named provider headers in auxiliary clients - #84223

Open
Eklps wants to merge 1 commit into
NousResearch:mainfrom
Eklps:fix/auxiliary-custom-provider-headers-pr
Open

fix(aux): preserve named provider headers in auxiliary clients#84223
Eklps wants to merge 1 commit into
NousResearch:mainfrom
Eklps:fix/auxiliary-custom-provider-headers-pr

Conversation

@Eklps

@Eklps Eklps commented Aug 12, 2026

Copy link
Copy Markdown

What does this PR do?

Propagates per-provider extra_headers from named providers / custom_providers entries to auxiliary OpenAI-wire clients.

The main agent already applies these headers, but auxiliary calls such as title generation, context compression, vision routing, and web extraction construct separate sync/async clients. A custom gateway that requires a provider-scoped header override can therefore accept the main turn while rejecting the auxiliary request from the same turn.

The selected named-provider identity is carried through client construction instead of being recovered only from base_url. This matters when multiple named providers share an endpoint: each provider must receive only its own routing/auth headers, and a headerless provider must not inherit another provider's headers through URL fallback.

This follows up on #40033 / #41096, which added global model.default_headers support. This PR covers the more specific providers.<name>.extra_headers / custom_providers[].extra_headers path.

Related Issue

Related to #40033 and #41096.

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
    • applies matching custom-provider headers at both sync and async OpenAI client construction points;
    • preserves the exact named-provider identity when providers share a base_url;
    • suppresses URL fallback for a selected named provider even when its own header set is empty;
    • preserves existing default headers, with provider-specific values taking precedence.
  • tests/agent/test_auxiliary_custom_provider_extra_headers.py
    • covers named-provider propagation, precedence, unmatched endpoints, and async conversion;
    • covers shared-endpoint providers with distinct headers;
    • covers headerless shared-endpoint providers to prevent cross-provider credential/header leakage.

How to Test

  1. Run the focused CI-parity suite:

    scripts/run_tests.sh \
      tests/agent/test_auxiliary_custom_provider_extra_headers.py \
      tests/agent/test_auxiliary_user_default_headers.py \
      tests/hermes_cli/test_custom_provider_extra_headers.py \
      tests/run_agent/test_custom_provider_extra_headers_client.py
  2. Expected result: 27 tests passed, 0 failed.

  3. Configure a named custom OpenAI-compatible provider with a provider-scoped extra_headers.User-Agent, start a new Hermes process, and verify both the main response and automatic title generation complete successfully.

Manual E2E on macOS: before the change, the main turn succeeded with the provider-scoped header while title generation returned HTTP 502; after the change and a Desktop restart, the same custom provider completed the conversation and title-generation path.

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 and all tests pass — focused canonical suite passed: 27/27
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.2, Hermes Desktop and CLI

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A; no user-facing key or API changed
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A; uses the existing extra_headers key
  • 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 Python config/header propagation; no OS-specific APIs
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

Focused canonical test runner:

Summary: 4 files, 27 tests passed, 0 failed (100% complete)

@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 sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 12, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(aux): preserve named provider headers in auxiliary clients

  1. agent/auxiliary_client.py — the generic (non-named) fallback in _apply_custom_provider_extra_headers wraps apply_custom_provider_extra_headers_to_client_kwargs in except Exception: logger.debug(...). A config error silently drops the headers at debug level — which is exactly the failure mode this PR fixes (aux calls 502ing behind a WAF). logger.warning would make a config regression visible instead of invisible.

  2. The fix covers _create_openai_client and _to_async_client, but the codex_responses branch wraps the client in CodexAuxiliaryClient (near line ~6535). If that wrapper builds its own OpenAI/Responses client internally rather than wrapping the passed one, the headers may still be lost on the codex path. Verify that route inherits default_headers or thread the headers through there as well.

  3. The named-identity suppression (a headerless named provider must not inherit the URL-matching provider's headers) is a security-relevant behavior change, well covered by the tenant-a/tenant-b tests. One asymmetry worth documenting: the suppression only exists for the named resolution path — a non-named base-url-only resolution hitting a config with two entries sharing a base_url still gets the first URL match's headers arbitrarily. If that scenario is reachable, it deserves the same treatment or an explicit note.

@ciberitch

Copy link
Copy Markdown

Hit this exact bug with a Cloudflare Access–gated endpoint (named custom provider, OpenAI-compatible, Service Auth policy requiring CF-Access-Client-Id/CF-Access-Client-Secret headers).

The main agent honored providers.<name>.extra_headers and worked, but the auxiliary client (auxiliary_client.pyresolve_provider_client, named-custom arm) silently dropped extra_headers — so compression / title generation / web-extract 401'd the moment Access enforcement went live, while the main chat path stayed fine.

Confirmed the fix direction: merging the resolved provider's extra_headers into the client's default_headers (provider-level last, so it wins over model.* defaults) restores a clean end-to-end 200 through the gate.

Thanks for the fix — it's a silent capability drop for anyone behind a gated gateway with a named custom provider.

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:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants