# NousResearch/hermes-agent · xAI Responses API tool schema pattern/format handling

## Open now

- 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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #27575. Verify it addresses your case.
- **Recommended:** read PR #27575 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-27575`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member 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.

## Closed / duplicate issues

- #27197: \[Bug\]: xAI Grok (xai-oauth) returns HTTP 400 or streaming fallback errors on Responses API

## Prior & competing PRs

- **#27575: fix(xai): strip pattern/format from Responses-format tool schemas (closes #27197, #27219)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/chat_completion_helpers.py`, `tests/tools/test_schema_sanitizer.py`, `tools/schema_sanitizer.py`
  - fix hunks:
  - `agent/chat_completion_helpers.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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", {
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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(
    ```
- **#27219: fix(schema_sanitizer): strip pattern/format from Responses-format tools for xAI compatibility**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#27575`

## Fix status

- Merged to main in PR #27575 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-17`.
