edac5f6378
Follow-up to #106866 from the post-merge audits (@ehz0ah, @victor-kyriazakos): - _recoverable_pool_provider: the "endpoint mismatch, not a dead key" shield now (a) compares the full origin via base_url_origin() (scheme+host+port — a different port or an HTTPS->HTTP downgrade is a different trust boundary) and (b) applies only when the rejected client carried the SESSION's key, so an independently owned auxiliary pool keeps rotating at its own configured origin. - _resolve_named_custom_branch: explicit/per-task base_url and api_key compose OVER the providers.<name> entry field-by-field instead of the entry silently replacing them (URL-only, key-only, both, neither). - _acreate_with_progress: plain-create fallback only for a rejected stream NEGOTIATION (mirrors the sync wrapper); a mid-stream failure propagates to the classified recovery ladder instead of re-sending the whole prompt non-streaming. Tests (all red on main): foreign origin x {host, port, scheme} shields the session key; same origin and independent aux pool still rotate; named-provider override matrix through resolve_provider_client on a real temp HERMES_HOME; async content-then-error is not retried non-streaming.
61 lines
3.2 KiB
Python
61 lines
3.2 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
|
|
|
|
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 = aux._expand_direct_api_alias("openai", None)
|
|
assert provider == "custom"
|
|
assert base == "https://llm-proxy.corp.example/v1"
|
|
monkeypatch.delenv("OPENAI_BASE_URL")
|
|
assert aux._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")
|