Files
hermes-agent/tests/gateway/test_custom_provider_request_overrides.py
T
teknium1 5aa1a50c57 fix(config): good-config backup only for the active home; fixture-based effective-config contract
- load_user_config_effective wrote backups/config/*.good.* into ANY home it
  read, so doctor and the TUI cwd lookup created backup dirs inside other
  profiles. The copy is now taken only when the path is the active home's
  config (the only home load_config ever backed up).
- The effective-config test re-composed the implementation's own primitives
  (a mirror); it now pins a literal expected dict for a fixture of user file +
  managed overlay + env, and fail_closed asserts yaml.YAMLError.
- Four repointed gateway tests carried duplicate _load_gateway_config setattr
  lines (one silently overriding the other); deduped to the intended dict.
2026-09-13 05:09:06 -07:00

304 lines
9.4 KiB
Python

"""Regression tests for gateway preservation of provider-derived request_overrides.
Named custom providers can return request_overrides (for example
``extra_body.text.verbosity`` for OpenAI Responses). The gateway must preserve
those overrides on the runtime path and merge fast-mode overrides on top rather
than replacing them with an empty dict.
"""
from __future__ import annotations
import asyncio
import sys
import threading
import types
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import gateway.run as gateway_run
from gateway.config import Platform
from gateway.session import SessionSource
class _CapturingAgent:
last_init = None
def __init__(self, *args, **kwargs):
type(self).last_init = dict(kwargs)
self.tools = []
self.request_overrides = dict(kwargs.get("request_overrides") or {})
def run_conversation(self, user_message: str, conversation_history=None, task_id=None):
return {
"final_response": "ok",
"messages": [],
"api_calls": 1,
}
def _install_fake_agent(monkeypatch):
fake_run_agent = types.ModuleType("run_agent")
fake_run_agent.AIAgent = _CapturingAgent
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
def _make_runner():
runner = object.__new__(gateway_run.GatewayRunner)
runner.adapters = {}
runner.session_store = None
runner.config = None
runner._voice_mode = {}
runner._ephemeral_system_prompt = ""
runner._prefill_messages = []
runner._reasoning_config = None
runner._show_reasoning = False
runner._provider_routing = {}
runner._fallback_model = None
runner._service_tier = None
runner._running_agents = {}
runner._running_agents_ts = {}
runner._background_tasks = set()
runner._session_db = None
runner._session_model_overrides = {}
runner._session_reasoning_overrides = {}
runner._pending_model_notes = {}
runner._pending_approvals = {}
runner._agent_cache = {}
runner._agent_cache_lock = threading.Lock()
runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
runner.hooks = MagicMock()
runner.hooks.emit = AsyncMock()
runner.hooks.loaded_hooks = []
return runner
def _make_source() -> SessionSource:
return SessionSource(
platform=Platform.FEISHU,
chat_id="ou_test",
chat_type="dm",
user_id="user-1",
user_name="tester",
)
def test_resolve_runtime_agent_kwargs_preserves_request_overrides(monkeypatch):
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
lambda: {
"api_key": "***",
"base_url": "https://example.test/v1",
"provider": "custom",
"api_mode": "codex_responses",
"command": None,
"args": [],
"credential_pool": None,
"request_overrides": {
"extra_body": {"text": {"verbosity": "low"}},
},
},
)
result = gateway_run._resolve_runtime_agent_kwargs()
assert result["request_overrides"] == {
"extra_body": {"text": {"verbosity": "low"}},
}
def test_turn_route_preserves_provider_request_overrides_without_fast_mode():
runner = _make_runner()
runner._service_tier = None
runtime_kwargs = {
"api_key": "***",
"base_url": "https://example.test/v1",
"provider": "custom",
"api_mode": "codex_responses",
"command": None,
"args": [],
"credential_pool": None,
"request_overrides": {
"extra_body": {"text": {"verbosity": "low"}},
},
}
route = gateway_run.GatewayRunner._resolve_turn_agent_config(
runner,
"hi",
"gpt-5.4",
runtime_kwargs,
)
assert route["request_overrides"] == {
"extra_body": {"text": {"verbosity": "low"}},
}
def test_turn_route_merges_fast_mode_with_provider_request_overrides():
runner = _make_runner()
runner._service_tier = "priority"
runtime_kwargs = {
"api_key": "***",
"base_url": "https://example.test/v1",
"provider": "custom",
"api_mode": "codex_responses",
"command": None,
"args": [],
"credential_pool": None,
"request_overrides": {
"extra_body": {"text": {"verbosity": "low"}},
},
}
with patch(
"hermes_cli.models.resolve_fast_mode_overrides",
return_value={"service_tier": "priority"},
):
route = gateway_run.GatewayRunner._resolve_turn_agent_config(
runner,
"hi",
"gpt-5.4",
runtime_kwargs,
)
assert route["request_overrides"] == {
"extra_body": {"text": {"verbosity": "low"}},
"service_tier": "priority",
}
@pytest.mark.asyncio
async def test_run_agent_preserves_provider_request_overrides_on_gateway_path(monkeypatch):
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
monkeypatch.setattr(
gateway_run,
"_resolve_runtime_agent_kwargs",
lambda: {
"provider": "custom",
"api_mode": "codex_responses",
"base_url": "https://example.test/v1",
"api_key": "***",
"request_overrides": {
"extra_body": {"text": {"verbosity": "low"}},
},
},
)
_install_fake_agent(monkeypatch)
import hermes_cli.tools_config as tools_config
monkeypatch.setattr(tools_config, "_get_platform_tools", lambda user_config, platform_key: {"core"})
runner = _make_runner()
source = _make_source()
session_key = "agent:main:feishu:dm:ou_test"
runner.session_store = SimpleNamespace(
get_or_create_session=lambda _source: SimpleNamespace(session_id="session-1"),
load_transcript=lambda _session_id: [],
)
_CapturingAgent.last_init = None
result = await runner._run_agent(
message="hi",
context_prompt="",
history=[],
source=source,
session_id="session-1",
session_key=session_key,
)
assert result["final_response"] == "ok"
assert _CapturingAgent.last_init is not None
assert _CapturingAgent.last_init["request_overrides"] == {
"extra_body": {"text": {"verbosity": "low"}},
}
@pytest.mark.asyncio
async def test_reused_agent_turn_merges_request_overrides_not_overwrite(monkeypatch):
"""Merge-not-overwrite regression (salvaged from PR #52432).
A cached/reused gateway agent must keep its init-time request_overrides
(custom-provider extra_body) across turns: a /fast turn layers
service_tier ON TOP, and the following normal turn drops only the stale
fast-mode key while the provider extra_body survives.
"""
monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
monkeypatch.setattr(
gateway_run,
"_resolve_runtime_agent_kwargs",
lambda: {
"provider": "custom",
"api_mode": "codex_responses",
"base_url": "https://example.test/v1",
"api_key": "***",
"request_overrides": {
"extra_body": {"text": {"verbosity": "low"}},
},
},
)
_install_fake_agent(monkeypatch)
import hermes_cli.tools_config as tools_config
monkeypatch.setattr(tools_config, "_get_platform_tools", lambda user_config, platform_key: {"core"})
runner = _make_runner()
source = _make_source()
session_key = "agent:main:feishu:dm:ou_test"
runner.session_store = SimpleNamespace(
get_or_create_session=lambda _source: SimpleNamespace(session_id="session-1"),
load_transcript=lambda _session_id: [],
)
seen_agents = []
orig_init = _CapturingAgent.__init__
def _tracking_init(self, *args, **kwargs):
orig_init(self, *args, **kwargs)
seen_agents.append(self)
monkeypatch.setattr(_CapturingAgent, "__init__", _tracking_init)
async def run_turn():
return await runner._run_agent(
message="hi",
context_prompt="",
history=[],
source=source,
session_id="session-1",
session_key=session_key,
)
# Turn 1: /fast active — provider extra_body AND service_tier both present.
# The turn path re-resolves the tier per session, so stub the resolver.
tier_box = {"tier": "priority"}
runner._resolve_session_service_tier = lambda *a, **k: tier_box["tier"]
with patch(
"hermes_cli.models.resolve_fast_mode_overrides",
return_value={"service_tier": "priority"},
):
result = await run_turn()
assert result["final_response"] == "ok"
assert len(seen_agents) == 1
agent = seen_agents[0]
assert agent.request_overrides == {
"extra_body": {"text": {"verbosity": "low"}},
"service_tier": "priority",
}
# Turn 2: back to normal — the SAME cached agent must drop only the stale
# fast-mode key; the init-time provider extra_body survives the refresh.
tier_box["tier"] = None
result = await run_turn()
assert result["final_response"] == "ok"
assert len(seen_agents) == 1, "agent should be reused from the gateway cache"
assert agent.request_overrides == {
"extra_body": {"text": {"verbosity": "low"}},
}