Bundled OAuth providers (nous, openai-codex) declare oauth_* auth types on their bundled profiles but their login lives in core; the fail-loud guard was refusing `hermes auth add nous`. Track the names register_plugin_provider mirrored and gate on that set, not on the auth_type alone. Also point the aux-client seam test at the moved helper (internal path, not API).
115 lines
4.0 KiB
Python
115 lines
4.0 KiB
Python
"""Auxiliary ``api_key`` routes honor ``ProviderProfile.create_client()`` — parity with the main agent.
|
|
|
|
``resolve_provider_client()``'s ``api_key`` branch used to build ``openai.OpenAI`` directly,
|
|
so an out-of-tree provider registered with ``auth_type="api_key"`` lost its native transport
|
|
for auxiliary tasks even though the main-agent path
|
|
(``agent_runtime_helpers._provider_supplied_client``) honors the same hook (#112384).
|
|
These tests pin the seam through the real resolution entry point: the native client
|
|
(sync + async), and the fall-through to the standard client for ordinary providers and
|
|
for a broken plugin.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import providers as _providers
|
|
from providers.base import ProviderProfile
|
|
|
|
_PROBE_ENV_VAR = "AUX_SEAM_PROBE_AUTH"
|
|
_PROBE_KEY = "probe-sentinel"
|
|
|
|
|
|
class _FakeNativeClient:
|
|
HERMES_SKIP_TRANSPORT_WRAP = True
|
|
HERMES_SKIP_ASYNC_WRAP = True
|
|
|
|
def __init__(self, **kwargs):
|
|
self.kwargs = kwargs
|
|
|
|
|
|
class _NativeProfile(ProviderProfile):
|
|
def create_client(self, **kwargs):
|
|
return _FakeNativeClient(**kwargs)
|
|
|
|
|
|
class _PassThroughProfile(ProviderProfile):
|
|
"""Ordinary provider: ``create_client`` returns None so the standard client is built."""
|
|
|
|
def create_client(self, **kwargs):
|
|
return None
|
|
|
|
|
|
class _ExplodingProfile(ProviderProfile):
|
|
def create_client(self, **kwargs):
|
|
raise RuntimeError("plugin is broken")
|
|
|
|
|
|
def _probe_profile(cls, name: str) -> ProviderProfile:
|
|
return cls(
|
|
name=name,
|
|
auth_type="api_key",
|
|
env_vars=(_PROBE_ENV_VAR,),
|
|
base_url=f"https://{name}.invalid",
|
|
default_aux_model="probe-model",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def registered(monkeypatch):
|
|
"""Register provider profiles for one test on copies of both registries.
|
|
|
|
Mirrors the import-time synthesis ``hermes_cli.auth`` performs for plugin ``api_key``
|
|
profiles, which is what routes them into ``_resolve_api_key_branch`` in the first place.
|
|
"""
|
|
import hermes_cli.auth as _auth
|
|
from hermes_cli.auth_plugin_providers import register_plugin_provider
|
|
from agent import secret_scope as _secret_scope
|
|
|
|
_providers._discover_providers()
|
|
monkeypatch.setattr(_providers, "_REGISTRY", dict(_providers._REGISTRY))
|
|
monkeypatch.setattr(_providers, "_ALIASES", dict(_providers._ALIASES))
|
|
monkeypatch.setattr(_providers, "_PROVIDER_LIST_CACHE", None)
|
|
monkeypatch.setattr(_auth, "PROVIDER_REGISTRY", dict(_auth.PROVIDER_REGISTRY))
|
|
scope_token = _secret_scope.set_secret_scope({_PROBE_ENV_VAR: _PROBE_KEY})
|
|
|
|
def _register(profile: ProviderProfile) -> None:
|
|
_providers.register_provider(profile)
|
|
register_plugin_provider(profile)
|
|
|
|
yield _register
|
|
_secret_scope.reset_secret_scope(scope_token)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"extra",
|
|
[{"task": "title_generation"}, {"async_mode": True}],
|
|
ids=["sync", "async-skips-AsyncOpenAI-rewrap"],
|
|
)
|
|
def test_native_profile_supplies_the_auxiliary_client(registered, extra):
|
|
from agent.auxiliary_client import resolve_provider_client
|
|
|
|
registered(_probe_profile(_NativeProfile, "aux-seam-native"))
|
|
client, model = resolve_provider_client("aux-seam-native", "probe-model", **extra)
|
|
|
|
assert isinstance(client, _FakeNativeClient)
|
|
assert model == "probe-model"
|
|
# The hook receives the same mapping the branch would have passed to openai.OpenAI.
|
|
assert client.kwargs == {"api_key": _PROBE_KEY, "base_url": "https://aux-seam-native.invalid"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"profile_cls", [_PassThroughProfile, _ExplodingProfile], ids=["returns-None", "raises"]
|
|
)
|
|
def test_profile_without_a_client_falls_back_to_the_standard_client(registered, profile_cls):
|
|
from openai import OpenAI
|
|
|
|
from agent.auxiliary_client import resolve_provider_client
|
|
|
|
registered(_probe_profile(profile_cls, "aux-seam-fallback"))
|
|
client, model = resolve_provider_client("aux-seam-fallback", "probe-model")
|
|
|
|
# A raising plugin can only fail to provide a client, never break auxiliary resolution.
|
|
assert isinstance(client, OpenAI)
|
|
assert model == "probe-model"
|