mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
Remove unused imports (F401) and duplicate/shadowed import redefinitions (F811) across the codebase using ruff's safe autofixes. No behavioral changes -- imports only. - ~1400 safe autofixes applied across 644 files (net -1072 lines) - __init__.py re-exports preserved (excluded from F401 removal so public re-export surfaces stay intact) - Re-exports that are imported or monkeypatched by tests but look unused in their defining module are kept with explicit # noqa: F401 (gateway/run.py load_dotenv; run_agent re-exports from agent.message_sanitization, agent.context_compressor, agent.retry_utils, agent.prompt_builder, agent.process_bootstrap, agent.codex_responses_adapter) - Unsafe F841 (unused-variable) fixes deliberately skipped -- those can change behavior when the RHS has side effects - ruff lints remain disabled in pyproject.toml (only PLW1514 is selected); this is a one-time cleanup, not a config change Verification: - python -m compileall: clean - pytest --collect-only: all 27161 tests collect (zero import errors) - core entry points import clean (run_agent, model_tools, cli, toolsets, hermes_state, batch_runner, gateway) - static scan: every name any test imports directly from an edited module still resolves
349 lines
14 KiB
Python
349 lines
14 KiB
Python
"""Tests for auxiliary client routing of the ``azure-foundry`` provider.
|
|
|
|
Covers the dedicated branch in ``agent.auxiliary_client.resolve_provider_client``
|
|
that delegates to :func:`hermes_cli.runtime_provider._resolve_azure_foundry_runtime`
|
|
instead of falling into the generic ``resolve_api_key_provider_credentials``
|
|
path (which only knows about ``AZURE_FOUNDRY_API_KEY`` and would 401 for
|
|
Entra ID users and miss ``model.base_url`` overrides for api-key users
|
|
with non-standard Foundry-projects endpoints).
|
|
|
|
Pinned scenarios:
|
|
|
|
* ``auth_mode: api_key`` → plain OpenAI client with the static string
|
|
key for ``chat_completions``.
|
|
* ``auth_mode: entra_id`` + ``chat_completions`` → plain OpenAI
|
|
client with a callable ``api_key`` (the bearer-token provider) —
|
|
confirms the callable survives the auxiliary path end-to-end.
|
|
* ``auth_mode: entra_id`` + GPT-5.x model → CodexAuxiliaryClient
|
|
wrapping the OpenAI client (api_mode auto-upgrades to
|
|
codex_responses).
|
|
* Anthropic-style + entra_id → rejected at the runtime resolver,
|
|
so the aux path returns ``(None, None)``.
|
|
* Failure path when no model is configured returns ``(None, None)``
|
|
cleanly so the auto chain falls through.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_credential_cache():
|
|
from agent.azure_identity_adapter import reset_credential_cache
|
|
reset_credential_cache()
|
|
yield
|
|
reset_credential_cache()
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_azure_identity(monkeypatch):
|
|
"""Stand-in for azure.identity (keeps CI hermetic when the SDK is
|
|
not installed)."""
|
|
from agent import azure_identity_adapter as _adapter
|
|
|
|
last = {"scope": None}
|
|
|
|
def _provider(scope):
|
|
return lambda: f"jwt-for-{scope}"
|
|
|
|
fake_module = SimpleNamespace(
|
|
DefaultAzureCredential=lambda **kw: SimpleNamespace(
|
|
kwargs=kw,
|
|
get_token=lambda scope: SimpleNamespace(token="fake", expires_on=9999999999),
|
|
),
|
|
get_bearer_token_provider=lambda credential, scope: (
|
|
last.__setitem__("scope", scope),
|
|
_provider(scope),
|
|
)[-1],
|
|
)
|
|
monkeypatch.setattr(_adapter, "_require_azure_identity", lambda: fake_module)
|
|
monkeypatch.setitem(sys.modules, "azure.identity", fake_module)
|
|
return last
|
|
|
|
|
|
@pytest.fixture
|
|
def patch_load_config(monkeypatch):
|
|
"""Helper to set model_cfg seen by _try_azure_foundry."""
|
|
def _apply(model_cfg):
|
|
monkeypatch.setattr(
|
|
"hermes_cli.config.load_config",
|
|
lambda: {"model": model_cfg},
|
|
)
|
|
return _apply
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# auth_mode: api_key (default) — regression for the legacy path
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAuxAzureFoundryApiKey:
|
|
def test_chat_completions_returns_plain_openai_client(self, monkeypatch, patch_load_config):
|
|
from agent.auxiliary_client import _try_azure_foundry
|
|
from openai import OpenAI as _OpenAI
|
|
|
|
monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "sk-azure-static-key")
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"default": "gpt-4o",
|
|
})
|
|
client, resolved = _try_azure_foundry(model="gpt-4o")
|
|
assert client is not None
|
|
assert resolved == "gpt-4o"
|
|
assert isinstance(client, _OpenAI)
|
|
assert client.api_key == "sk-azure-static-key"
|
|
|
|
def test_codex_responses_wraps_in_codex_aux_client(self, monkeypatch, patch_load_config):
|
|
from agent.auxiliary_client import _try_azure_foundry, CodexAuxiliaryClient
|
|
|
|
monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "sk-azure-static-key")
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"default": "gpt-5.4-mini",
|
|
})
|
|
# GPT-5.x → runtime auto-upgrades to codex_responses
|
|
client, resolved = _try_azure_foundry(model="gpt-5.4-mini")
|
|
assert resolved == "gpt-5.4-mini"
|
|
assert isinstance(client, CodexAuxiliaryClient)
|
|
assert client.api_key == "sk-azure-static-key"
|
|
|
|
def test_no_key_returns_none(self, monkeypatch, patch_load_config):
|
|
from agent.auxiliary_client import _try_azure_foundry
|
|
|
|
monkeypatch.delenv("AZURE_FOUNDRY_API_KEY", raising=False)
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"default": "gpt-4o",
|
|
})
|
|
client, resolved = _try_azure_foundry(model="gpt-4o")
|
|
assert client is None
|
|
assert resolved is None
|
|
|
|
def test_no_model_returns_none(self, monkeypatch, patch_load_config):
|
|
"""Azure has no fallback aux model — fail soft so the auto chain
|
|
can try other providers."""
|
|
from agent.auxiliary_client import _try_azure_foundry
|
|
|
|
monkeypatch.setenv("AZURE_FOUNDRY_API_KEY", "sk-azure-static-key")
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
# No default model
|
|
})
|
|
client, resolved = _try_azure_foundry()
|
|
assert client is None
|
|
assert resolved is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# auth_mode: entra_id — callable api_key survives end-to-end
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAuxAzureFoundryEntra:
|
|
def test_callable_api_key_reaches_openai_constructor(
|
|
self, monkeypatch, fake_azure_identity, patch_load_config,
|
|
):
|
|
"""The token provider callable must arrive at ``OpenAI(api_key=...)``
|
|
intact — never stringified to ``"no-key-required"`` or to the
|
|
SDK-internal empty-string representation BEFORE we hand it off.
|
|
|
|
We assert on the public SDK contract (constructor receives the
|
|
callable) rather than ``client.api_key``, because OpenAI 2.24.0
|
|
stores callable api_keys in a private attribute and exposes
|
|
``client.api_key`` as ``""``. The SDK still calls the callable
|
|
per request to mint ``Authorization: Bearer <token>``; that
|
|
behaviour is the documented Microsoft/OpenAI contract we rely on.
|
|
"""
|
|
from agent import auxiliary_client as _aux
|
|
|
|
received = {}
|
|
|
|
class _FakeOpenAI:
|
|
def __init__(self, **kwargs):
|
|
received.update(kwargs)
|
|
# Mirror the fields downstream callers read.
|
|
self.api_key = kwargs.get("api_key", "")
|
|
self.base_url = kwargs.get("base_url", "")
|
|
|
|
monkeypatch.setattr(_aux, "OpenAI", _FakeOpenAI)
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"auth_mode": "entra_id",
|
|
"default": "gpt-4o",
|
|
})
|
|
client, resolved = _aux._try_azure_foundry(model="gpt-4o")
|
|
assert client is not None
|
|
assert resolved == "gpt-4o"
|
|
# Public-contract assertion: the OpenAI SDK constructor saw the
|
|
# callable, exactly as Microsoft's Foundry sample requires.
|
|
assert callable(received["api_key"])
|
|
assert not isinstance(received["api_key"], str)
|
|
assert received["api_key"]().startswith("jwt-for-")
|
|
# Base URL forwarded verbatim (no /responses suffix stripping
|
|
# in this path — that's a separate concern handled by the
|
|
# runtime resolver only when the user re-saves config).
|
|
assert received["base_url"] == "https://r.openai.azure.com/openai/v1"
|
|
|
|
def test_codex_responses_with_entra_wraps_correctly(
|
|
self, monkeypatch, fake_azure_identity, patch_load_config,
|
|
):
|
|
"""GPT-5.x deployment on Entra ID — auto-upgraded to
|
|
codex_responses, wrapped in CodexAuxiliaryClient, callable
|
|
api_key handed to the underlying OpenAI SDK."""
|
|
from agent import auxiliary_client as _aux
|
|
|
|
received = {}
|
|
|
|
class _FakeOpenAI:
|
|
def __init__(self, **kwargs):
|
|
received.update(kwargs)
|
|
self.api_key = kwargs.get("api_key", "")
|
|
self.base_url = kwargs.get("base_url", "")
|
|
|
|
monkeypatch.setattr(_aux, "OpenAI", _FakeOpenAI)
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"auth_mode": "entra_id",
|
|
"default": "gpt-5.4-mini",
|
|
})
|
|
client, resolved = _aux._try_azure_foundry(model="gpt-5.4-mini")
|
|
assert resolved == "gpt-5.4-mini"
|
|
assert isinstance(client, _aux.CodexAuxiliaryClient)
|
|
# The Codex wrapper received an OpenAI client built with the
|
|
# callable api_key — verify against the SDK constructor record,
|
|
# not the wrapper attribute (which mirrors the SDK's empty-
|
|
# string representation).
|
|
assert callable(received["api_key"])
|
|
assert received["api_key"]().startswith("jwt-for-")
|
|
|
|
def test_entra_anthropic_messages_uses_bearer_hook(
|
|
self, monkeypatch, fake_azure_identity, patch_load_config,
|
|
):
|
|
"""Entra ID + anthropic_messages: runtime returns a callable
|
|
api_key; ``_maybe_wrap_anthropic`` → ``build_anthropic_client``
|
|
detects the callable and installs the bearer-injecting httpx
|
|
event hook on a custom ``httpx.Client`` passed to the
|
|
Anthropic SDK via ``http_client=``."""
|
|
from agent import auxiliary_client as _aux
|
|
from agent import anthropic_adapter as _anthropic
|
|
|
|
received = {}
|
|
|
|
class _FakeOpenAI:
|
|
def __init__(self, **kwargs):
|
|
received["openai"] = kwargs
|
|
self.api_key = kwargs.get("api_key", "")
|
|
self.base_url = kwargs.get("base_url", "")
|
|
|
|
class _FakeAnthropicSDK:
|
|
class Anthropic:
|
|
def __init__(self, **kwargs):
|
|
received["anthropic"] = kwargs
|
|
|
|
monkeypatch.setattr(_aux, "OpenAI", _FakeOpenAI)
|
|
monkeypatch.setattr(_anthropic, "_get_anthropic_sdk", lambda: _FakeAnthropicSDK)
|
|
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.services.ai.azure.com/anthropic",
|
|
"api_mode": "anthropic_messages",
|
|
"auth_mode": "entra_id",
|
|
"default": "claude-sonnet-4-5",
|
|
})
|
|
client, resolved = _aux._try_azure_foundry(model="claude-sonnet-4-5")
|
|
assert client is not None
|
|
assert resolved == "claude-sonnet-4-5"
|
|
# The Anthropic SDK constructor received a custom http_client
|
|
# (the bearer-injecting hook) and a placeholder auth_token.
|
|
anthropic_kwargs = received.get("anthropic") or {}
|
|
assert "http_client" in anthropic_kwargs, (
|
|
"build_anthropic_client must pass a custom http_client when "
|
|
"given a callable api_key, otherwise the SDK cannot mint "
|
|
"fresh tokens per request"
|
|
)
|
|
assert anthropic_kwargs.get("auth_token") == "entra-id-bearer-via-http-hook"
|
|
# Verify the http_client actually has our event hook installed.
|
|
http_client = anthropic_kwargs["http_client"]
|
|
hooks = getattr(http_client, "event_hooks", {})
|
|
assert "request" in hooks and len(hooks["request"]) >= 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# resolve_provider_client → azure-foundry dispatch
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestResolveProviderClientAzureFoundry:
|
|
def test_dispatches_to_azure_branch_not_generic_api_key_path(
|
|
self, monkeypatch, fake_azure_identity, patch_load_config,
|
|
):
|
|
"""End-to-end: the public ``resolve_provider_client`` entry
|
|
point must take the dedicated azure-foundry branch, NOT the
|
|
generic api-key registry path that would call
|
|
``resolve_api_key_provider_credentials`` and return None for
|
|
Entra users."""
|
|
from agent import auxiliary_client as _aux
|
|
|
|
received = {}
|
|
|
|
class _FakeOpenAI:
|
|
def __init__(self, **kwargs):
|
|
received.update(kwargs)
|
|
self.api_key = kwargs.get("api_key", "")
|
|
self.base_url = kwargs.get("base_url", "")
|
|
|
|
monkeypatch.setattr(_aux, "OpenAI", _FakeOpenAI)
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
"auth_mode": "entra_id",
|
|
"default": "gpt-4o",
|
|
})
|
|
client, resolved = _aux.resolve_provider_client("azure-foundry", "gpt-4o")
|
|
assert client is not None
|
|
assert resolved == "gpt-4o"
|
|
# The callable made it through resolve_provider_client → _try_azure_foundry
|
|
# → OpenAI(api_key=...).
|
|
assert callable(received["api_key"])
|
|
|
|
def test_warns_and_returns_none_on_failure(
|
|
self, monkeypatch, patch_load_config, caplog,
|
|
):
|
|
"""When azure-foundry is requested but cannot be resolved
|
|
(e.g. no model + no key), we return (None, None) and log a
|
|
clear warning pointing at ``hermes doctor``."""
|
|
import logging
|
|
from agent.auxiliary_client import resolve_provider_client
|
|
|
|
monkeypatch.delenv("AZURE_FOUNDRY_API_KEY", raising=False)
|
|
patch_load_config({
|
|
"provider": "azure-foundry",
|
|
"base_url": "https://r.openai.azure.com/openai/v1",
|
|
"api_mode": "chat_completions",
|
|
# No default → resolver yields no model → bail
|
|
})
|
|
with caplog.at_level(logging.WARNING, logger="agent.auxiliary_client"):
|
|
client, resolved = resolve_provider_client("azure-foundry")
|
|
assert client is None
|
|
assert resolved is None
|
|
assert any(
|
|
"azure-foundry" in rec.message and "hermes doctor" in rec.message
|
|
for rec in caplog.records
|
|
)
|