fix(aux): add session_search extra_body and concurrency controls
Adds auxiliary.<task>.extra_body config passthrough so reasoning-heavy OpenAI-compatible providers can receive provider-specific request fields (e.g. enable_thinking: false on GLM) on auxiliary calls, and bounds session_search summary fan-out with auxiliary.session_search.max_concurrency (default 3, clamped 1-5) to avoid 429 bursts on small providers. - agent/auxiliary_client.py: extract _get_auxiliary_task_config helper, add _get_task_extra_body, merge config+explicit extra_body with explicit winning - hermes_cli/config.py: extra_body defaults on all aux tasks + session_search.max_concurrency; _config_version 19 -> 20 - tools/session_search_tool.py: semaphore around _summarize_all gather - tests: coverage in test_auxiliary_client, test_session_search, test_aux_config - docs: user-guide/configuration.md + fallback-providers.md Co-authored-by: Teknium <teknium@nousresearch.com>
This commit is contained in:
@@ -946,6 +946,70 @@ class TestStaleBaseUrlWarning:
|
||||
"Expected a warning about stale OPENAI_BASE_URL"
|
||||
assert mod._stale_base_url_warned is True
|
||||
|
||||
|
||||
class TestAuxiliaryTaskExtraBody:
|
||||
def test_sync_call_merges_task_extra_body_from_config(self):
|
||||
client = MagicMock()
|
||||
client.base_url = "https://api.example.com/v1"
|
||||
response = MagicMock()
|
||||
client.chat.completions.create.return_value = response
|
||||
|
||||
config = {
|
||||
"auxiliary": {
|
||||
"session_search": {
|
||||
"extra_body": {
|
||||
"enable_thinking": False,
|
||||
"reasoning": {"effort": "none"},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with patch("hermes_cli.config.load_config", return_value=config), patch(
|
||||
"agent.auxiliary_client._get_cached_client",
|
||||
return_value=(client, "glm-4.5-air"),
|
||||
):
|
||||
result = call_llm(
|
||||
task="session_search",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
extra_body={"metadata": {"source": "test"}},
|
||||
)
|
||||
|
||||
assert result is response
|
||||
kwargs = client.chat.completions.create.call_args.kwargs
|
||||
assert kwargs["extra_body"]["enable_thinking"] is False
|
||||
assert kwargs["extra_body"]["reasoning"] == {"effort": "none"}
|
||||
assert kwargs["extra_body"]["metadata"] == {"source": "test"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_call_explicit_extra_body_overrides_task_config(self):
|
||||
client = MagicMock()
|
||||
client.base_url = "https://api.example.com/v1"
|
||||
response = MagicMock()
|
||||
client.chat.completions.create = AsyncMock(return_value=response)
|
||||
|
||||
config = {
|
||||
"auxiliary": {
|
||||
"session_search": {
|
||||
"extra_body": {"enable_thinking": False}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with patch("hermes_cli.config.load_config", return_value=config), patch(
|
||||
"agent.auxiliary_client._get_cached_client",
|
||||
return_value=(client, "glm-4.5-air"),
|
||||
):
|
||||
result = await async_call_llm(
|
||||
task="session_search",
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
extra_body={"enable_thinking": True},
|
||||
)
|
||||
|
||||
assert result is response
|
||||
kwargs = client.chat.completions.create.call_args.kwargs
|
||||
assert kwargs["extra_body"]["enable_thinking"] is True
|
||||
|
||||
def test_no_warning_when_provider_is_custom(self, monkeypatch, caplog):
|
||||
"""No warning when the provider is 'custom' — OPENAI_BASE_URL is expected."""
|
||||
import agent.auxiliary_client as mod
|
||||
|
||||
@@ -39,6 +39,15 @@ def test_title_generation_present_in_default_config():
|
||||
assert tg["provider"] == "auto"
|
||||
assert tg["model"] == ""
|
||||
assert tg["timeout"] > 0
|
||||
assert tg["extra_body"] == {}
|
||||
|
||||
|
||||
def test_session_search_defaults_include_extra_body_and_concurrency():
|
||||
ss = DEFAULT_CONFIG["auxiliary"]["session_search"]
|
||||
assert ss["provider"] == "auto"
|
||||
assert ss["model"] == ""
|
||||
assert ss["extra_body"] == {}
|
||||
assert ss["max_concurrency"] == 3
|
||||
|
||||
|
||||
def test_aux_tasks_keys_all_exist_in_default_config():
|
||||
|
||||
@@ -459,7 +459,7 @@ class TestCustomProviderCompatibility:
|
||||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 19
|
||||
assert raw["_config_version"] == 20
|
||||
assert raw["providers"]["openai-direct"] == {
|
||||
"api": "https://api.openai.com/v1",
|
||||
"api_key": "test-key",
|
||||
@@ -606,7 +606,7 @@ class TestInterimAssistantMessageConfig:
|
||||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 19
|
||||
assert raw["_config_version"] == 20
|
||||
assert raw["display"]["tool_progress"] == "off"
|
||||
assert raw["display"]["interim_assistant_messages"] is True
|
||||
|
||||
@@ -626,6 +626,6 @@ class TestDiscordChannelPromptsConfig:
|
||||
migrate_config(interactive=False, quiet=True)
|
||||
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert raw["_config_version"] == 19
|
||||
assert raw["_config_version"] == 20
|
||||
assert raw["discord"]["auto_thread"] is True
|
||||
assert raw["discord"]["channel_prompts"] == {}
|
||||
|
||||
@@ -64,4 +64,4 @@ class TestCamofoxConfigDefaults:
|
||||
|
||||
# The current schema version is tracked globally; unrelated default
|
||||
# options may bump it after browser defaults are added.
|
||||
assert DEFAULT_CONFIG["_config_version"] == 19
|
||||
assert DEFAULT_CONFIG["_config_version"] == 20
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for tools/session_search_tool.py — helper functions and search dispatcher."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import pytest
|
||||
@@ -8,6 +9,7 @@ from tools.session_search_tool import (
|
||||
_format_timestamp,
|
||||
_format_conversation,
|
||||
_truncate_around_matches,
|
||||
_get_session_search_max_concurrency,
|
||||
_HIDDEN_SESSION_SOURCES,
|
||||
MAX_SESSION_CHARS,
|
||||
SESSION_SEARCH_SCHEMA,
|
||||
@@ -181,6 +183,63 @@ class TestTruncateAroundMatches:
|
||||
assert result.lower().count("alpha beta") == 2
|
||||
|
||||
|
||||
class TestSessionSearchConcurrency:
|
||||
def test_defaults_to_three(self):
|
||||
assert _get_session_search_max_concurrency() == 3
|
||||
|
||||
def test_reads_and_clamps_configured_value(self, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.load_config",
|
||||
lambda: {"auxiliary": {"session_search": {"max_concurrency": 9}}},
|
||||
)
|
||||
assert _get_session_search_max_concurrency() == 5
|
||||
|
||||
def test_session_search_respects_configured_concurrency_limit(self, monkeypatch):
|
||||
from unittest.mock import MagicMock
|
||||
from tools.session_search_tool import session_search
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.config.load_config",
|
||||
lambda: {"auxiliary": {"session_search": {"max_concurrency": 1}}},
|
||||
)
|
||||
|
||||
max_seen = {"value": 0}
|
||||
active = {"value": 0}
|
||||
|
||||
async def fake_summarize(_text, _query, _meta):
|
||||
active["value"] += 1
|
||||
max_seen["value"] = max(max_seen["value"], active["value"])
|
||||
await asyncio.sleep(0.01)
|
||||
active["value"] -= 1
|
||||
return "summary"
|
||||
|
||||
monkeypatch.setattr("tools.session_search_tool._summarize_session", fake_summarize)
|
||||
monkeypatch.setattr("model_tools._run_async", lambda coro: asyncio.run(coro))
|
||||
|
||||
mock_db = MagicMock()
|
||||
mock_db.search_messages.return_value = [
|
||||
{"session_id": "s1", "source": "cli", "session_started": 1709500000, "model": "test"},
|
||||
{"session_id": "s2", "source": "cli", "session_started": 1709500001, "model": "test"},
|
||||
{"session_id": "s3", "source": "cli", "session_started": 1709500002, "model": "test"},
|
||||
]
|
||||
mock_db.get_session.side_effect = lambda sid: {
|
||||
"id": sid,
|
||||
"parent_session_id": None,
|
||||
"source": "cli",
|
||||
"started_at": 1709500000,
|
||||
}
|
||||
mock_db.get_messages_as_conversation.side_effect = lambda sid: [
|
||||
{"role": "user", "content": f"message from {sid}"},
|
||||
{"role": "assistant", "content": "response"},
|
||||
]
|
||||
|
||||
result = json.loads(session_search(query="message", db=mock_db, limit=3))
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 3
|
||||
assert max_seen["value"] == 1
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# session_search (dispatcher)
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user