From 5befa15abab1f38f7541ffbaf6ef6d72c00dbf7c Mon Sep 17 00:00:00 2001 From: YAMAGUCHI Seiji Date: Fri, 10 Jul 2026 08:02:49 +0900 Subject: [PATCH] feat: configure X Search reasoning effort --- hermes_cli/config.py | 3 ++ tests/tools/test_x_search_tool.py | 44 +++++++++++++++++++ tools/x_search_tool.py | 24 ++++++++++ website/docs/user-guide/features/x-search.md | 9 ++++ .../current/user-guide/features/x-search.md | 7 +++ 5 files changed, 87 insertions(+) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3e535ce678ba..8fb9ad63172b 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3303,6 +3303,9 @@ DEFAULT_CONFIG = { # recommended default; any Grok model with x_search tool # access works. "model": "grok-4.5", + # Optional reasoning effort sent to xAI Responses API models that + # support it. Leave null to preserve the selected model's default. + "reasoning_effort": None, # Request timeout in seconds (minimum 30). x_search can take # 60-120s for complex queries — the default is generous. "timeout_seconds": 180, diff --git a/tests/tools/test_x_search_tool.py b/tests/tools/test_x_search_tool.py index 8aeb07ef3d50..b17751d166e3 100644 --- a/tests/tools/test_x_search_tool.py +++ b/tests/tools/test_x_search_tool.py @@ -72,6 +72,7 @@ def test_x_search_posts_responses_request(monkeypatch): assert captured["headers"]["User-Agent"] == f"Hermes-Agent/{__version__}" assert captured["json"]["model"] == "grok-4.5" assert captured["json"]["store"] is False + assert "reasoning" not in captured["json"] assert tool_def["type"] == "x_search" assert tool_def["allowed_x_handles"] == ["xai", "grok"] assert tool_def["from_date"] == "2026-04-01" @@ -424,6 +425,49 @@ def test_x_search_honors_config_model_and_timeout(monkeypatch, tmp_path): assert captured["timeout"] == 45 +def test_x_search_honors_config_reasoning_effort(monkeypatch): + """Configured reasoning effort reaches the xAI Responses request.""" + from tools.x_search_tool import x_search_tool + + monkeypatch.setenv("XAI_API_KEY", "xai-test-key") + monkeypatch.setattr( + "tools.x_search_tool._load_x_search_config", + lambda: {"reasoning_effort": "low", "retries": 0}, + ) + captured = {} + + def _fake_post(url, headers=None, json=None, timeout=None): + assert json is not None + captured["reasoning"] = json.get("reasoning") + return _FakeResponse({"output_text": "Reasoning configured."}) + + monkeypatch.setattr("requests.post", _fake_post) + + result = json.loads(x_search_tool(query="anything")) + + assert result["success"] is True + assert captured["reasoning"] == {"effort": "low"} + + +def test_x_search_rejects_invalid_config_reasoning_effort(monkeypatch): + """A typo must fail closed instead of silently using xAI's default effort.""" + from tools.x_search_tool import x_search_tool + + monkeypatch.setenv("XAI_API_KEY", "xai-test-key") + monkeypatch.setattr( + "tools.x_search_tool._load_x_search_config", + lambda: {"reasoning_effort": "minimal"}, + ) + _no_post_allowed(monkeypatch) + + result = json.loads(x_search_tool(query="anything")) + + assert result["error"] == ( + "x_search.reasoning_effort must be one of: low, medium, high, xhigh " + "(got 'minimal')" + ) + + def test_x_search_registered_in_registry_with_check_fn(): """The tool is registered under the x_search toolset with the gating check_fn.""" import tools.x_search_tool # noqa: F401 — ensures registration runs diff --git a/tools/x_search_tool.py b/tools/x_search_tool.py index 8ed35ff2ee1e..87de533dd4ac 100644 --- a/tools/x_search_tool.py +++ b/tools/x_search_tool.py @@ -59,6 +59,7 @@ DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1" DEFAULT_X_SEARCH_MODEL = "grok-4.5" DEFAULT_X_SEARCH_TIMEOUT_SECONDS = 180 DEFAULT_X_SEARCH_RETRIES = 2 +X_SEARCH_REASONING_EFFORTS = ("low", "medium", "high", "xhigh") MAX_HANDLES = 10 @@ -80,6 +81,22 @@ def _get_x_search_model() -> str: return (str(cfg.get("model") or "").strip() or DEFAULT_X_SEARCH_MODEL) +def _get_x_search_reasoning_effort() -> Optional[str]: + cfg = _load_x_search_config() + raw_value = cfg.get("reasoning_effort") + if raw_value is None or not str(raw_value).strip(): + return None + + effort = str(raw_value).strip().lower() + if effort not in X_SEARCH_REASONING_EFFORTS: + allowed = ", ".join(X_SEARCH_REASONING_EFFORTS) + raise ValueError( + f"x_search.reasoning_effort must be one of: {allowed} " + f"(got {raw_value!r})" + ) + return effort + + def _get_x_search_timeout_seconds() -> int: cfg = _load_x_search_config() raw_value = cfg.get("timeout_seconds", DEFAULT_X_SEARCH_TIMEOUT_SECONDS) @@ -299,6 +316,11 @@ def x_search_tool( except ValueError as exc: return tool_error(str(exc)) + try: + reasoning_effort = _get_x_search_reasoning_effort() + except ValueError as exc: + return tool_error(str(exc)) + tool_def: Dict[str, Any] = {"type": "x_search"} if allowed: tool_def["allowed_x_handles"] = allowed @@ -324,6 +346,8 @@ def x_search_tool( "tools": [tool_def], "store": False, } + if reasoning_effort: + payload["reasoning"] = {"effort": reasoning_effort} timeout_seconds = _get_x_search_timeout_seconds() max_retries = _get_x_search_retries() diff --git a/website/docs/user-guide/features/x-search.md b/website/docs/user-guide/features/x-search.md index c1512823c387..105e96a77ae1 100644 --- a/website/docs/user-guide/features/x-search.md +++ b/website/docs/user-guide/features/x-search.md @@ -54,6 +54,11 @@ x_search: # with x_search tool access works. model: grok-4.5 + # Optional reasoning effort: low, medium, high, or xhigh. When omitted, + # the selected model's default applies. xhigh is supported only by + # models that document it, such as grok-4.20-multi-agent. + reasoning_effort: low + # Request timeout in seconds. x_search can take 60–120s for # complex queries — the default is generous. Minimum: 30. timeout_seconds: 180 @@ -63,6 +68,10 @@ x_search: retries: 2 ``` +`reasoning_effort` is sent to the xAI Responses API as +`reasoning: {effort: ...}`. Leave it unset for models that do not support +configurable reasoning. Invalid values fail before an API request is made. + ## Tool parameters The agent calls `x_search` with these arguments: diff --git a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/x-search.md b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/x-search.md index 65de81dc71de..ef6bed81f162 100644 --- a/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/x-search.md +++ b/website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/features/x-search.md @@ -50,6 +50,10 @@ x_search: # x_search 工具访问权限的 Grok 模型均可使用。 model: grok-4.5 + # 可选推理强度:low、medium、high 或 xhigh。省略时使用所选模型的默认值。 + # xhigh 仅适用于明确支持它的模型,例如 grok-4.20-multi-agent。 + reasoning_effort: low + # 请求超时时间(秒)。复杂查询的 x_search 可能需要 60–120 秒, # 默认值较为宽松。最小值:30。 timeout_seconds: 180 @@ -59,6 +63,9 @@ x_search: retries: 2 ``` +`reasoning_effort` 会以 `reasoning: {effort: ...}` 的形式发送到 xAI +Responses API。不支持可配置推理的模型应留空。无效值会在发起 API 请求前失败。 + ## 工具参数 agent 调用 `x_search` 时使用以下参数: