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
- 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
Open PRs0
Fixes on record
-
#27575 fix(xai): strip pattern/format from Responses-format tool schemas (closes #27197, #27219)
files:
agent/chat_completion_helpers.py,tests/tools/test_schema_sanitizer.py,tools/schema_sanitizer.pyShow 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.00means 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 LabAt a glance
- Merged PRs
- #27575
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 17 May 2026