NousResearch/hermes-agent

xAI Responses API tool schema pattern/format handling

Cluster 27197. Data as of

Nothing open on GitHub. Fix merged in #27575 on 17 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #27575. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #27575. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items appear related to xAI Grok's Responses API returning HTTP 400 or streaming fallback errors, and two pull requests propose stripping pattern/format from Responses-format tool schemas. Titles alone don't show whether the changes are merged or whether the errors are fully explained by that cause.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #27575 fix(xai): strip pattern/format from Responses-format tool schemas (closes #27197, #27219)

    merged to main

    files: agent/chat_completion_helpers.py, tests/tools/test_schema_sanitizer.py, tools/schema_sanitizer.py

    Show the diff
    • agent/chat_completion_helpers.py
      @@ -286,6 +286,21 @@ def build_api_kwargs(agent, api_messages: list) -> dict:
               )
               is_xai_responses = agent.provider in {"xai", "xai-oauth"} or agent._base_url_hostname == "api.x.ai"
               _msgs_for_codex = agent._prepare_messages_for_non_vision_model(api_messages)
      +
      +        # xAI's /responses endpoint rejects ``pattern`` and ``format`` keywords
      +        # in tool schemas (HTTP 400 "Invalid arguments passed to the model").
      +        # Most commonly hit when MCP-derived tools carry JSON Schema validation
      +        # keywords through. Strip them before building kwargs. See #27197.
      +        if is_xai_responses:
      +            try:
      +                from tools.schema_sanitizer import strip_pattern_and_format
      +                tools_for_api, _ = strip_pattern_and_format(tools_for_api)
      +            except Exception as exc:
      +                logger.warning(
      +                    "%s⚠️ Failed to sanitize tool schemas for xAI: %s",
      +                    getattr(agent, "log_prefix", ""), exc,
      +                )
      +
               return _ct.build_kwargs(
                   model=agent.model,
                   messages=_msgs_for_codex,
    • tests/tools/test_schema_sanitizer.py
      @@ -304,6 +304,30 @@ def test_strip_none_returns_zero():
           assert stripped == 0
       
       
      +
      +def test_strip_responses_format_strips_format_keyword():
      +    """Responses-format:  keyword should be stripped."""
      +    from tools.schema_sanitizer import strip_pattern_and_format
      +
      +    tools = [
      +        {
      +            "name": "get_event",
      +            "parameters": {
      +                "type": "object",
      +                "properties": {
      +                    "ts": {"type": "string", "format": "date-time"},
      +                }
      +            },
      +            "type": "function"
      +        }
      +    ]
      +
      +    result, stripped = strip_pattern_and_format(tools)
      +    assert stripped == 1, f"Expected 1 format stripped, got {stripped}"
      +    assert "format" not in result[0]["parameters"]["properties"]["ts"], "format should be stripped"
      +    assert result[0]["parameters"]["properties"]["ts"]["type"] == "string", "type should be preserved"
      +
      +
       def test_top_level_allof_stripped_for_codex_backend_compat():
           """OpenAI Codex backend rejects top-level allOf/oneOf/anyOf/enum/not."""
           tools = [_tool("memory", {
      @@ -360,3 +384,110 @@ def test_nested_allof_preserved():
           nested = out[0]["function"]["parameters"]["properties"]["config"]
           assert "allOf" in nested
           assert nested["allOf"] == [{"required": ["mode"]}]
      +
      +
      +def test_strip_responses_format_tools():
      +    """strip_pattern_and_format should handle Responses-format tools (no function wrapper)."""
      +    from tools.schema_sanitizer import strip_pattern_and_format
      +
      +    # Responses-format: {"name": "...", "parameters": {...}, "type": "function"}
      +    tools = [
      +        {
      +            "name": "mcp_firecrawl_search",
      +            "parameters": {
      +                "type": "object",
      +                "properties": {
      +                    "query": {"type": "string"},
      +                    "includeDomains": {
      +                        "type": "array",
      +                        "items": {
      +                            "type": "string",
      +                            "pattern": "^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$"
      +                        }
      +                    }
      +                }
      +            },
      +            "type": "function"
      +        }
      +    ]
      +
      +    result, stripped = strip_pattern_and_format(tools)
      +    assert stripped == 1, f"Expected 1 pattern stripped, got {stripped}"
      +    
      +    # Verify pattern keyword was removed from includeDomains
      +    domains = result[0]["parameters"]["properties"]["includeDomains"]["items"]
      +    assert "pattern" not in domains, f"pattern should be stripped: {domains}"
      +    assert domains["type"] == "string", "type should be preserved"
      +
      +
      +def test_strip_responses_idempotent():
      +    """Second call on already-stripped Responses-format tools should return 0."""
      +    from tools.schema_sanitizer import strip_pattern_and_format
      +
      +    tools = [
      +        {
      +            "name": "search_files",
      +            "parameters": {
      +                "type": "object",
      +                "properties": {
      +                    "pattern": {"type": "string"}  # This is a property named pattern, NOT schema keyword
      +                }
      +            }
      +        }
      +    ]
      +
      +    # Pass 1 - property named 'pattern' should NOT be stripped
      +    result, first = strip_pattern_and_format(tools)
      +    assert first == 0, f"Expected 0 stripped (property pattern preserved), got {first}"
      +    assert "pattern" in result[0]["parameters"]["properties"], "property named pattern should survive"
      +    
      +    # Pass 2 - idempotent
      +    _, second = strip_pattern_and_format(tools)
      +    assert second == 0, f"Expected 0 on second pass, got {second}"
      +
      +
      +def test_strip_responses_mixed_formats():
      +    """Mixed list of OpenAI-format and Responses-format tools should both be sanitized."""
      +    from tools.schema_sanitizer import strip_pattern_and_format
      +
      +    tools = [
      +        # OpenAI-format: {"function": {"parameters": {...}}}
      +        {
      +            "type": "function",
      +            "function": {
      +                "name": "search",
      +                "parameters": {
      +                    "type": "object",
      +                    "properties": {
      +                        "query": {"type": "string", "pattern": "^[a-z]+$"}
      +                    }
      +                }
      +            }
      +        },
      +        # Responses-format: {"name": "...", "parameters": {...}}
      +        {
      +            "name": "get_time",
      +            "parameters": {
      +                "type": "object",
      +                "properties": {
      +                    "tz": {"type": "string", "format": "date-time"}
      +                }
      +            },
      +            "type": "function"
      +        }
      +    ]
      +
      +    result, stripped = strip_pattern_and_format(tools)
      +    assert stripped == 2, f"Expected 2 stripped (1 pattern + 1 format), got {stripped}"
      +
      +    # OpenAI-format tool: pattern stripped from parameters
      +    openai_params = result[0]["function"]["parameters"]["properties"]["query"]
      +    assert "pattern" not in openai_params, f"pattern should be stripped: {openai_params}"
      +
      +    # Responses-format tool: format stripped
      +    resp_params = result[1]["parameters"]["properties"]["tz"]
      +    assert "format" not in resp_params, f"format should be stripped: {resp_params}"
      +
      +    # Verify structure preserved
      +    assert result[0]["function"]["parameters"]["type"] == "object"
      +    assert result[1]["parameters"]["type"] == "object"
    • tools/schema_sanitizer.py
      @@ -355,11 +355,23 @@ def _walk(node: Any) -> None:
                       _walk(item)
       
           for tool in tools:
      -        fn = tool.get("function") if isinstance(tool, dict) else None
      +        if not isinstance(tool, dict):
      +            continue
      +        
      +        # OpenAI-format: {"function": {"parameters": {...}}}
      +        fn = tool.get("function")
               if isinstance(fn, dict):
                   params = fn.get("parameters")
                   if isinstance(params, dict):
                       _walk(params)
      +                continue
      +        
      +        # Responses-format: {"name": "...", "parameters": {...}}
      +        # (used by codex_responses API mode — xAI, OpenAI Codex, etc.)
      +        params = tool.get("parameters")
      +        if isinstance(params, dict):
      +            _walk(params)
      +            continue
       
           if stripped:
               logger.info(

1 PR closed without merging

From the close comments: done elsewhere 1

  • #27219 fix(schema_sanitizer): strip pattern/format from Responses-format tools for xAI compatibility · done elsewhere → #27575
Closed issues · 1
  • Closed#27197[Bug]: xAI Grok (xai-oauth) returns HTTP 400 or streaming fallback errors on Responses API
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#27575
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
17 May 2026

For your agent

The same facts, typed.