Files
hermes-agent/agent/auxiliary_async_rebuild.py
teknium1 b1db026f3a fix(aux): keep key_cmd bearer and extra_headers on async aux calls to named custom providers
An `auxiliary.<task>.provider` pinned to a bare-named `providers:` entry with
`key_cmd` sent every async aux request (async_call_llm, async vision, the
async fallback ladder) with NO Authorization header: the OpenAI SDK parks a
callable api_key in `_api_key_provider` and leaves `.api_key` == "", and
`_to_async_client` rebuilt AsyncOpenAI from that empty snapshot alone, so the
SDK's auth_headers came back empty. The entry's `extra_headers` were never
lifted onto the aux client at all (sync or async), unlike the main runtime.

- agent/auxiliary_async_rebuild.py: carry the sync client's token provider
  (wrapped for await, run off-loop) and its configured default_headers onto
  the async twin.
- _to_async_client uses both; covers the primary async route and every
  fallback-candidate rebuild.
- _resolve_named_custom_branch lifts the entry's extra_headers onto the
  OpenAI-wire client (both call sites).

key_env (static string) was not affected on main; key_cmd was.

Fixes #109595
Salvages #109626

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-17 08:53:15 -07:00

43 lines
1.7 KiB
Python

"""Carry a sync OpenAI client's credential and configured headers onto its async rebuild.
``_to_async_client`` recreates ``AsyncOpenAI`` from a resolved sync client. Two things do not
live on ``.api_key`` / the header set it recomputes: a per-request token provider (``key_cmd``,
Entra ID — the SDK parks the callable in ``_api_key_provider`` and leaves ``.api_key`` empty, so
an ``auth_headers`` built from the snapshot is ``{}`` and the request carries NO Authorization at
all) and the ``default_headers`` the client was constructed with (a named provider's
``extra_headers``). See #109595.
"""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from typing import Any, Dict
from openai import OpenAI
def async_api_key(sync_client: Any) -> Any:
"""Credential for the async twin: the sync token provider wrapped for ``await``, else the static key.
The sync provider is ``Callable[[], str]``; ``AsyncOpenAI`` awaits its provider before every request,
so the callable is run off-loop (it may shell out to a ``key_cmd``).
"""
provider = getattr(sync_client, "_api_key_provider", None) if isinstance(sync_client, OpenAI) else None
if not callable(provider):
return sync_client.api_key
async def _provide() -> str:
return str(await asyncio.to_thread(provider))
return _provide
def configured_default_headers(sync_client: Any) -> Dict[str, str]:
"""The ``default_headers`` mapping the sync client was constructed with (SDK ``_custom_headers``).
SECURITY: values may carry credentials — never log them.
"""
configured = getattr(sync_client, "_custom_headers", None) if isinstance(sync_client, OpenAI) else None
return dict(configured) if isinstance(configured, Mapping) else {}