Skip to content

feat(xai): xai_web_search tool — live web search via xAI Responses API web_search - #23345

Closed
Julientalbot wants to merge 2 commits into
NousResearch:mainfrom
Julientalbot:feat/xai-web-search
Closed

feat(xai): xai_web_search tool — live web search via xAI Responses API web_search#23345
Julientalbot wants to merge 2 commits into
NousResearch:mainfrom
Julientalbot:feat/xai-web-search

Conversation

@Julientalbot

Copy link
Copy Markdown
Contributor

Summary

Adds xai_web_search — a tool that runs a live web search via xAI's Responses API web_search built-in tool, returning an answer grounded in citations.

This is the web counterpart of x_search (#14541): x_search targets X / Twitter, xai_web_search targets the open web. Together they cover both surfaces of xAI's two-tool search ecosystem (per the xAI API reference: "only functions and web search are supported as tools").

Why

Hermes already has a generic web_search tool (in tools/web_tools.py) backed by various third-party providers (Exa, Firecrawl, Parallel-Web). That's the right default for portability.

But when an xAI API key is available, the xAI-native web_search is the more direct path:

  • Single API call (no third-party intermediary).
  • Citations come back in the same response, anchored with start_index / end_index offsets into the answer text.
  • Pricing and rate limits are governed by xAI directly — predictable for users already paying for xAI.

This tool gives Hermes profiles that have XAI_API_KEY set the option of routing search through xAI without changing the existing web_search default.

Changes (5 files, +743/-0)

tools/xai_web_search_tool.py (new, ~395 LoC)

Implementation symmetric with the rebased tools/x_search_tool.py from #14541 — the two share the same Responses-API-as-search-backend pattern.

web_search_tool(
    query: str,                              # required
    allowed_websites: list[str] | None = None,  # max 10, hostnames; mutually exclusive
    excluded_websites: list[str] | None = None, # max 10, hostnames
    from_date: str = "",                     # ISO YYYY-MM-DD
    to_date: str = "",                       # ISO YYYY-MM-DD
    country: str = "",                       # ISO alpha-2; uppercased
) -> str  # JSON-encoded {success, provider, tool, model, query, answer, citations, inline_citations}

Key design choices:

  • Hostname normalization: https://www.nytimes.com/section/foonytimes.com (strips protocol, www., path) so the user can paste any URL form.
  • Citations on two channels: top-level data.citations (xAI's flat list of sources used) + inline url_citation annotations on output[*].content[*].annotations with start_index / end_index offsets so callers can hyperlink the answer text.
  • Retry policy matches x_search: retry on 5xx and read-timeout / connection errors with exponential backoff capped at 5s; no retry on 4xx auth errors.
  • Configurable via config.yaml:
    web_search:
      model: grok-4.20-reasoning
      timeout_seconds: 240
      retries: 3
  • Reuses tools.xai_http.hermes_xai_user_agent for User-Agent.
  • Self-registers via tools.registry.registry.register, gated on XAI_API_KEY.
  • Registered as xai_web_search because the bare web_search name is already owned by tools/web_tools.py (the generic, provider-neutral search). Profiles can enable both — the agent picks per-call.

tests/tools/test_xai_web_search_tool.py (new, ~290 LoC)

33 unit tests with a requests.post monkeypatch fake (no real network):

  • Requirements / schema: check_web_search_requirements (with / without / blank API key); required query; optional params advertised.
  • _normalize_websites: strips https:// / http:// / www. / paths; drops empty entries; rejects lists > 10; handles None.
  • Argument validation: empty query, missing API key, allowed_websites + excluded_websites mutually exclusive — all surface as success: false JSON.
  • Body construction: default model, /responses endpoint (not /chat/completions), allowed-/excluded-websites threading, date range, country uppercased, minimal tool_def when no options, complete headers (Bearer, JSON, User-Agent).
  • Response parsing: answer extraction; top-level citations passthrough; inline url_citation annotation extraction; legacy output_text fallback; multi-piece text concatenation; non-url_citation annotation types skipped.
  • HTTP errors: 401 surfaces immediately; 500 retries then succeeds when retries available; 500 exhausts retries; 4xx never retries (saves the user from compounding auth errors).

Wiring

Validation

  • pytest tests/tools/test_xai_web_search_tool.py tests/tools/test_registry.py64/64 passing locally on top of current main.

Backward compatibility

  • Pure addition. The existing generic web_search tool in tools/web_tools.py is untouched.
  • Tool is check_fn-gated on XAI_API_KEY — invisible to users without an xAI key.
  • xai_web_search is not in any default toolset roster; users opt in via tools_config or by enabling the toolset in their profile.
  • A profile can enable both web_search and xai_web_search simultaneously; the agent picks per-call based on which is more appropriate for the query.

Scope deliberately not in this PR

  • Default-routing logic that picks xai_web_search over web_search when xAI is available. That's a per-profile policy decision, not a tool-level concern.
  • Streaming (stream=true on the underlying Responses call). Web search is typically a single-shot answer; streaming would mostly mask the citation block until the end.
  • max_search_results as a tool-level argument. Currently controlled via the search_parameters.max_search_results body field if needed; happy to expose if the maintainer wants it.

…s API web_search

xAI's Responses API supports a web_search built-in tool: pass tools:
[{type: "web_search", ...}] and the model searches the live web,
scrapes pages it finds, and grounds its answer with citations.

Per the xAI API reference: "only functions and web search are
supported as tools" — so this is the canonical way to wire xAI-native
web search into Hermes without going through a third-party search
provider.

This is the web counterpart of x_search (NousResearch#14541) which targets
X / Twitter; xai_web_search targets the open web.

Changes:
- tools/xai_web_search_tool.py: self-contained tool implementation
  - web_search_tool(query, allowed_websites, excluded_websites,
    from_date, to_date, country) → JSON-encoded result with
    {success, provider, tool, model, query, answer, citations,
    inline_citations}
  - Uses Responses API tool_def shape symmetric with x_search:
    type=web_search, optional allowed_websites/excluded_websites
    (max 10, mutually exclusive), from_date/to_date (ISO YYYY-MM-DD),
    country (uppercased ISO alpha-2)
  - Citation extraction: top-level data.citations + inline
    url_citation annotations on output[*].content[*].annotations
  - Retry on 5xx and read-timeout/connection errors with exponential
    backoff capped at 5s; no retry on 4xx (auth)
  - Configurable via config.yaml under web_search:
    {model, timeout_seconds, retries}
  - Reuses tools.xai_http.hermes_xai_user_agent
  - Self-registers via tools.registry.registry.register
  - Tool name xai_web_search (the bare web_search name is already
    taken by tools.web_tools)
- tests/tools/test_xai_web_search_tool.py: 33 unit tests
  - check_web_search_requirements (with/without/blank API key)
  - schema (required query, optional params advertised)
  - _normalize_websites (strips protocol/www/path, drops empty,
    rejects >10, handles None)
  - argument validation (empty query, missing API key, allowed +
    excluded mutually exclusive)
  - body construction (default model, /responses endpoint, allowed
    websites, excluded websites, date range, country uppercase,
    minimal tool_def, headers)
  - response parsing (answer, top-level citations, inline citations,
    legacy output_text fallback, multi-piece concat, non-url types
    skipped)
  - HTTP errors (401 surface, 500 retry then succeed, 500 exhaust
    retries, 4xx no retry)
- toolsets.py: add xai_web_search to _HERMES_CORE_TOOLS and new
  xai_web_search toolset
- hermes_cli/tools_config.py: add xai_web_search to
  CONFIGURABLE_TOOLSETS
- tests/tools/test_registry.py: add tools.xai_web_search_tool to
  manual builtin tool set snapshot

Requires XAI_API_KEY in ~/.hermes/.env.
@alt-glitch alt-glitch added type/feature New feature or request comp/tools Tool registry, model_tools, toolsets provider/xai xAI (Grok) tool/web Web search and extraction P3 Low — cosmetic, nice to have labels May 10, 2026
@Julientalbot

Copy link
Copy Markdown
Contributor Author

Closing this older xAI web search PR in favor of #27023, which supersedes it with a fresh main base, the current xAI Responses API web_search shape (filters.allowed_domains / filters.excluded_domains, max 5), refreshed tests, and active CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have provider/xai xAI (Grok) tool/web Web search and extraction type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants