Files
aiturk-hermes-ide/tests/gateway/test_custom_provider_request_overrides.py

308 lines
9.8 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, "load_dotenv", lambda *args, **kwargs: None)
monkeypatch.setattr(gateway_run, "_load_gateway_runtime_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, "load_dotenv", lambda *args, **kwargs: None)
monkeypatch.setattr(gateway_run, "_load_gateway_runtime_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"}},
}