fix(gateway): make reasoning session-scoped by default
This commit is contained in:
@@ -33,6 +33,7 @@ def _make_runner():
|
||||
runner._ephemeral_system_prompt = ""
|
||||
runner._prefill_messages = []
|
||||
runner._reasoning_config = None
|
||||
runner._session_reasoning_overrides = {}
|
||||
runner._show_reasoning = False
|
||||
runner._provider_routing = {}
|
||||
runner._fallback_model = None
|
||||
@@ -76,6 +77,10 @@ class TestReasoningCommand:
|
||||
source = inspect.getsource(gateway_run.GatewayRunner._handle_message)
|
||||
assert '"reasoning"' in source
|
||||
|
||||
def test_parse_reasoning_command_args_accepts_ascii_and_smart_global_flags(self):
|
||||
assert gateway_run.GatewayRunner._parse_reasoning_command_args("high --global") == ("high", True)
|
||||
assert gateway_run.GatewayRunner._parse_reasoning_command_args("—global xhigh") == ("xhigh", True)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_command_reloads_current_state_from_config(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
@@ -111,13 +116,90 @@ class TestReasoningCommand:
|
||||
runner = _make_runner()
|
||||
runner._reasoning_config = {"enabled": True, "effort": "medium"}
|
||||
|
||||
result = await runner._handle_reasoning_command(_make_event("/reasoning low"))
|
||||
result = await runner._handle_reasoning_command(_make_event("/reasoning low --global"))
|
||||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["agent"]["reasoning_effort"] == "low"
|
||||
assert runner._reasoning_config == {"enabled": True, "effort": "low"}
|
||||
assert "takes effect on next message" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_reasoning_command_defaults_to_session_only(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text("agent:\n reasoning_effort: medium\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
event = _make_event("/reasoning high")
|
||||
session_key = runner._session_key_for_source(event.source)
|
||||
|
||||
result = await runner._handle_reasoning_command(event)
|
||||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["agent"]["reasoning_effort"] == "medium"
|
||||
assert runner._session_reasoning_overrides[session_key] == {"enabled": True, "effort": "high"}
|
||||
assert runner._reasoning_config == {"enabled": True, "effort": "high"}
|
||||
assert "session only" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_global_clears_existing_session_override(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text("agent:\n reasoning_effort: medium\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
event = _make_event("/reasoning low --global")
|
||||
session_key = runner._session_key_for_source(event.source)
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "xhigh"}
|
||||
|
||||
result = await runner._handle_reasoning_command(event)
|
||||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["agent"]["reasoning_effort"] == "low"
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
assert "saved to config" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reasoning_reset_clears_session_override_without_config_write(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text("agent:\n reasoning_effort: medium\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
event = _make_event("/reasoning reset")
|
||||
session_key = runner._session_key_for_source(event.source)
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "xhigh"}
|
||||
|
||||
result = await runner._handle_reasoning_command(event)
|
||||
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["agent"]["reasoning_effort"] == "medium"
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
assert "cleared" in result
|
||||
|
||||
def test_resolve_session_reasoning_prefers_session_override(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
(hermes_home / "config.yaml").write_text("agent:\n reasoning_effort: low\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
source = _make_event("/reasoning").source
|
||||
session_key = runner._session_key_for_source(source)
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "xhigh"}
|
||||
|
||||
assert runner._resolve_session_reasoning_config(source=source) == {"enabled": True, "effort": "xhigh"}
|
||||
|
||||
def test_run_agent_reloads_reasoning_config_per_message(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
@@ -167,6 +249,56 @@ class TestReasoningCommand:
|
||||
assert _CapturingAgent.last_init is not None
|
||||
assert _CapturingAgent.last_init["reasoning_config"] == {"enabled": True, "effort": "low"}
|
||||
|
||||
def test_run_agent_prefers_session_reasoning_override(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
(hermes_home / "config.yaml").write_text("agent:\n reasoning_effort: low\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
monkeypatch.setattr(gateway_run, "_env_path", hermes_home / ".env")
|
||||
monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
gateway_run,
|
||||
"_resolve_runtime_agent_kwargs",
|
||||
lambda: {
|
||||
"provider": "openrouter",
|
||||
"api_mode": "chat_completions",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key": "***",
|
||||
},
|
||||
)
|
||||
fake_run_agent = types.ModuleType("run_agent")
|
||||
fake_run_agent.AIAgent = _CapturingAgent
|
||||
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
|
||||
|
||||
_CapturingAgent.last_init = None
|
||||
runner = _make_runner()
|
||||
session_key = "agent:main:local:dm"
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
|
||||
|
||||
source = SessionSource(
|
||||
platform=Platform.LOCAL,
|
||||
chat_id="cli",
|
||||
chat_name="CLI",
|
||||
chat_type="dm",
|
||||
user_id="user-1",
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
runner._run_agent(
|
||||
message="ping",
|
||||
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["reasoning_config"] == {"enabled": True, "effort": "high"}
|
||||
|
||||
def test_run_agent_includes_enabled_mcp_servers_in_gateway_toolsets(self, tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
|
||||
@@ -54,6 +54,7 @@ def _make_runner():
|
||||
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 = {}
|
||||
@@ -102,6 +103,7 @@ def test_run_agent_prefers_session_override_over_global_runtime(monkeypatch):
|
||||
)
|
||||
session_key = "agent:main:local:dm"
|
||||
runner._session_model_overrides[session_key] = _codex_override()
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
|
||||
|
||||
result = asyncio.run(
|
||||
runner._run_agent(
|
||||
@@ -121,6 +123,7 @@ def test_run_agent_prefers_session_override_over_global_runtime(monkeypatch):
|
||||
assert _CapturingAgent.last_init["api_mode"] == "codex_responses"
|
||||
assert _CapturingAgent.last_init["base_url"] == "https://chatgpt.com/backend-api/codex"
|
||||
assert _CapturingAgent.last_init["api_key"] == "***"
|
||||
assert _CapturingAgent.last_init["reasoning_config"] == {"enabled": True, "effort": "high"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -149,6 +152,7 @@ async def test_background_task_prefers_session_override_over_global_runtime(monk
|
||||
)
|
||||
session_key = runner._session_key_for_source(source)
|
||||
runner._session_model_overrides[session_key] = _codex_override()
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
|
||||
|
||||
await runner._run_background_task("say hello", source, "bg_test")
|
||||
|
||||
@@ -158,3 +162,4 @@ async def test_background_task_prefers_session_override_over_global_runtime(monk
|
||||
assert _CapturingAgent.last_init["api_mode"] == "codex_responses"
|
||||
assert _CapturingAgent.last_init["base_url"] == "https://chatgpt.com/backend-api/codex"
|
||||
assert _CapturingAgent.last_init["api_key"] == "***"
|
||||
assert _CapturingAgent.last_init["reasoning_config"] == {"enabled": True, "effort": "high"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests that /new (and its /reset alias) clears the session-scoped model override."""
|
||||
"""Tests that /new (and its /reset alias) clears session-scoped overrides."""
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
@@ -37,6 +37,7 @@ def _make_runner():
|
||||
runner._voice_mode = {}
|
||||
runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False)
|
||||
runner._session_model_overrides = {}
|
||||
runner._session_reasoning_overrides = {}
|
||||
runner._pending_model_notes = {}
|
||||
runner._background_tasks = set()
|
||||
|
||||
@@ -75,14 +76,16 @@ async def test_new_command_clears_session_model_override():
|
||||
runner._session_model_overrides[session_key] = {
|
||||
"model": "gpt-4o",
|
||||
"provider": "openai",
|
||||
"api_key": "sk-test",
|
||||
"api_key": "***",
|
||||
"base_url": "",
|
||||
"api_mode": "openai",
|
||||
}
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
|
||||
|
||||
await runner._handle_reset_command(_make_event("/new"))
|
||||
|
||||
assert session_key not in runner._session_model_overrides
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -92,10 +95,12 @@ async def test_new_command_no_override_is_noop():
|
||||
session_key = build_session_key(_make_source())
|
||||
|
||||
assert session_key not in runner._session_model_overrides
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
|
||||
await runner._handle_reset_command(_make_event("/new"))
|
||||
|
||||
assert session_key not in runner._session_model_overrides
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -115,12 +120,16 @@ async def test_new_command_only_clears_own_session():
|
||||
runner._session_model_overrides[other_key] = {
|
||||
"model": "claude-sonnet-4-6",
|
||||
"provider": "anthropic",
|
||||
"api_key": "sk-ant-test",
|
||||
"api_key": "***",
|
||||
"base_url": "",
|
||||
"api_mode": "anthropic",
|
||||
}
|
||||
runner._session_reasoning_overrides[session_key] = {"enabled": True, "effort": "high"}
|
||||
runner._session_reasoning_overrides[other_key] = {"enabled": True, "effort": "low"}
|
||||
|
||||
await runner._handle_reset_command(_make_event("/new"))
|
||||
|
||||
assert session_key not in runner._session_model_overrides
|
||||
assert other_key in runner._session_model_overrides
|
||||
assert session_key not in runner._session_reasoning_overrides
|
||||
assert other_key in runner._session_reasoning_overrides
|
||||
|
||||
Reference in New Issue
Block a user