Skip to content

fix(primary): preserve routed client headers on auto-routed provider init - #9065

Closed
GabriWar wants to merge 1 commit into
NousResearch:mainfrom
GabriWar:fix/primary-client-header-preservation
Closed

fix(primary): preserve routed client headers on auto-routed provider init#9065
GabriWar wants to merge 1 commit into
NousResearch:mainfrom
GabriWar:fix/primary-client-header-preservation

Conversation

@GabriWar

Copy link
Copy Markdown

Bug

When the primary agent client is built via the auto-routed path (provider auto-detected, no explicit API key in config.yaml), run_agent.py checks hasattr(_routed_client, '_default_headers') to copy provider-specific headers into self._client_kwargs. The OpenAI SDK actually stores headers passed via default_headers= on the constructor in _custom_headers, so the check silently fails and _client_kwargs is saved without any headers.

Every subsequent request-client rebuild via _create_request_openai_client then constructs a bare OpenAI(api_key, base_url) client that drops the Copilot-required Editor-Version and Copilot-Integration-Id headers, causing hard 400 model_not_supported responses from api.githubcopilot.com/chat/completions on every tool-call round after the first request.

The gateway path (WhatsApp, Telegram, Discord, etc.) is the easiest repro because each turn spawns a fresh AIAgent that immediately rebuilds its request client before any assistant output. The CLI path can be mostly unaffected for single-shot queries because the first request uses the original client directly.

Reproduction

  1. model.default: gemini-3.1-pro-preview + model.provider: copilot in config.yaml
  2. No COPILOT_API_KEY env var — rely on gh auth token / ghu_* github user token, so the auto-route path at line ~914 is taken
  3. Start hermes gateway run, send a message via a messaging platform that triggers a tool call
  4. Gateway log:
⚠️ Non-retryable error (HTTP 400) — trying fallback...
❌ Non-retryable error (HTTP 400): HTTP 400: The requested model is not supported.
   📋 Details: {'message': 'The requested model is not supported.', 'code': 'model_not_supported', 'param': 'model', 'type': 'invalid_request_error'}

Inspecting the dumped outgoing request headers shows only Authorization and Content-TypeEditor-Version / Copilot-Integration-Id are missing, confirming the rebuilt client dropped them.

Root cause

from agent.auxiliary_client import resolve_provider_client
c, _ = resolve_provider_client("copilot", model="gemini-3.1-pro-preview", raw_codex=True)
print(hasattr(c, "_default_headers"))  # False
print(hasattr(c, "_custom_headers"))   # True
# _custom_headers contains: Editor-Version, User-Agent, Copilot-Integration-Id,
#                           Openai-Intent, x-initiator

resolve_provider_client() in auxiliary_client.py correctly passes the full copilot header set via default_headers= when constructing the OpenAI client, but the OpenAI SDK stashes them on _custom_headers, not _default_headers.

Fix

Prefer _custom_headers and fall back to _default_headers for forward-compat with any routed client type that uses the older attribute name:

_rh = getattr(_routed_client, '_custom_headers', None) or getattr(_routed_client, '_default_headers', None)
if _rh:
    client_kwargs["default_headers"] = dict(_rh)

This mirrors the pattern used in #6076, which fixed the same class of bug in _try_activate_fallback(). That PR only covered the fallback activation path — the auto-routed primary path was missed.

Verified

After applying the patch + systemctl --user restart hermes-gateway.service + clearing __pycache__, repeated multi-tool-call sessions on gemini-3.1-pro-preview via Copilot now complete cleanly with zero 400 model_not_supported errors. Primary no longer needs to hand off to the fallback chain on every request.

Related

…init

When the primary agent client is built via the auto-routed path
(provider auto-detected, no explicit API key in config.yaml), the
code checked for a ``_default_headers`` attribute on the routed
OpenAI client to copy provider-specific headers into
``self._client_kwargs``. The OpenAI SDK actually stores headers
passed via ``default_headers=`` on the constructor in
``_custom_headers``. As a result, the check silently failed and
``_client_kwargs`` was saved without any headers.

Subsequent request-client rebuilds via ``_create_request_openai_client``
then constructed bare ``OpenAI(api_key, base_url)`` clients that
dropped the Copilot-required ``Editor-Version`` and
``Copilot-Integration-Id`` headers, causing
``400 model_not_supported`` errors from GitHub Copilot on every
tool-call round after the first request. The gateway path (whatsapp,
telegram, etc.) hit this hard because every turn spawns a fresh
agent that immediately rebuilds its request client.

The fix mirrors PR #6076, which fixed the same class of bug in the
fallback activation path: prefer ``_custom_headers`` and fall back to
``_default_headers`` for forward-compat with any routed client type
that uses the older attribute name.
Copilot AI review requested due to automatic review settings April 13, 2026 14:27

Copilot AI 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.

Pull request overview

Fixes a regression where auto-routed provider initialization failed to persist provider-specific HTTP headers (notably GitHub Copilot headers) into _client_kwargs, causing rebuilt request clients to drop required headers on subsequent turns.

Changes:

  • Update auto-routed client initialization to copy headers from the OpenAI SDK’s _custom_headers (with fallback) into client_kwargs["default_headers"].
  • Add explanatory comments tying the fix to the prior fallback-path fix (#6076) and documenting the OpenAI SDK behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread run_agent.py
Comment on lines +923 to +934
# Preserve any default_headers the router set.
# OpenAI SDK stores headers passed via ``default_headers=``
# on the client constructor in ``_custom_headers``; the
# older ``_default_headers`` attribute is not exposed for
# routed clients, so the original check silently failed
# and provider-specific headers (Copilot Editor-Version,
# Copilot-Integration-Id, Kimi User-Agent, etc.) were
# lost on every request-client rebuild. See PR #6076
# for the same fix in the fallback activation path.
_rh = getattr(_routed_client, '_custom_headers', None) or getattr(_routed_client, '_default_headers', None)
if _rh:
client_kwargs["default_headers"] = dict(_rh)
Comment thread run_agent.py
Comment on lines +925 to +932
# on the client constructor in ``_custom_headers``; the
# older ``_default_headers`` attribute is not exposed for
# routed clients, so the original check silently failed
# and provider-specific headers (Copilot Editor-Version,
# Copilot-Integration-Id, Kimi User-Agent, etc.) were
# lost on every request-client rebuild. See PR #6076
# for the same fix in the fallback activation path.
_rh = getattr(_routed_client, '_custom_headers', None) or getattr(_routed_client, '_default_headers', None)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants