Files
hermes-agent/tests/agent/test_aux_session_endpoint_affinity.py
teknium1 956a8c843d fix(aux): provider "openai" resolves the same on the runtime and aux-client paths
auxiliary.<task>.provider: openai was expanded to custom + the user's OpenAI
endpoint only by agent/auxiliary_client.py (compression, vision, title
generation). hermes_cli/runtime_provider.py::resolve_runtime_provider — the
path background_review, curator, MoA slots and delegation use — had no such
expansion, so "openai" hit auth.resolve_provider's registry lookup and raised
"Unknown provider 'openai'". The alias table now lives once in
runtime_provider_custom.py (the direct-alias/custom sibling) and both paths
call it; resolve_runtime_provider applies it before the ladder.

_host_gated_env_key_candidates also pairs OPENAI_API_KEY with a base_url that
is exactly OPENAI_BASE_URL: the alias lands on that proxy when no block
base_url is set, and the key was issued for it — the host gate otherwise sent
the "no-key-required" placeholder there while the aux-client path used the key.

Slim redo of #116083 (same direction: shared alias, applied in the runtime
resolver) without the extra key gate and effective_provider threading.

Co-authored-by: Mohamad Kanso <91088196+MohamadKanso@users.noreply.github.com>
2026-09-19 12:30:31 -07:00

62 lines
3.3 KiB
Python

"""Auxiliary routing sticks to the session's configured OpenAI endpoint; a rejection elsewhere is not a dead key.
Proxy users (`OPENAI_BASE_URL` / `providers.openai` pointing at a corporate gateway) saw compression
hop to api.openai.com, 401 with the proxy-issued key, and then have that key quarantined.
"""
from types import SimpleNamespace
import pytest
from agent import auxiliary_client as aux
from hermes_cli.runtime_provider_custom import expand_direct_api_alias
SESSION = {"provider": "openai-api", "model": "gpt-5.4",
"base_url": "https://proxy.example:8443/v1", "api_key": "sk-session"}
def test_openai_alias_prefers_configured_endpoint_over_public_default(monkeypatch):
monkeypatch.setenv("OPENAI_BASE_URL", "https://llm-proxy.corp.example/v1")
provider, base = expand_direct_api_alias("openai", None)
assert provider == "custom"
assert base == "https://llm-proxy.corp.example/v1"
monkeypatch.delenv("OPENAI_BASE_URL")
assert expand_direct_api_alias("openai", None) == ("custom", "https://api.openai.com/v1")
@pytest.mark.parametrize("rejecting_base", [
"https://api.openai.com/v1/", # different host
"https://proxy.example:9443/v1/", # same host, different port
"http://proxy.example:8443/v1/", # same host+port, scheme downgrade
])
def test_session_key_rejected_at_foreign_origin_is_not_rotated(rejecting_base):
client = SimpleNamespace(base_url=rejecting_base, api_key="sk-session")
assert aux._recoverable_pool_provider("openai-api", client, main_runtime=SESSION) is None
def test_rotation_survives_at_session_origin_and_for_independent_aux_pool():
same_origin = SimpleNamespace(base_url="https://proxy.example:8443/v1/", api_key="sk-session")
assert aux._recoverable_pool_provider("openai-api", same_origin, main_runtime=SESSION) == "openai-api"
# A separately owned aux credential at its own configured endpoint must keep rotating.
independent = SimpleNamespace(base_url="https://api.openai.com/v1/", api_key="sk-aux-pool-1")
assert aux._recoverable_pool_provider("openai-api", independent, main_runtime=SESSION) == "openai-api"
def test_named_provider_defaults_compose_under_task_overrides(monkeypatch, tmp_path):
"""URL-only / key-only task overrides win field-by-field; the named entry fills only the blanks."""
monkeypatch.setenv("NAMED_KEY", "named-key")
(tmp_path / "config.yaml").write_text(
"model:\n provider: openai\n default: gpt-5.4\n"
"providers:\n openai:\n api: https://named.example/v1\n key_env: NAMED_KEY\n")
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
def route(**overrides):
aux._client_cache.clear()
client, _ = aux.resolve_provider_client("openai", "gpt-5.4-mini", **overrides)
assert client is not None
return str(client.base_url).rstrip("/"), str(client.api_key)
assert route() == ("https://named.example/v1", "named-key")
assert route(explicit_base_url="https://aux-explicit.example/v1") == ("https://aux-explicit.example/v1", "named-key")
assert route(explicit_api_key="task-key") == ("https://named.example/v1", "task-key")
assert route(explicit_base_url="https://aux-explicit.example/v1", explicit_api_key="task-key") == (
"https://aux-explicit.example/v1", "task-key")