From 26f601f968157ccfbd47c5bb04b8d8dcfafcbfe5 Mon Sep 17 00:00:00 2001 From: MHammett Date: Sun, 16 Aug 2026 20:21:43 -0500 Subject: [PATCH] fix(xai): stop rejecting `instructions` on the Responses API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `XAIResponsesAPIConfig` removed `instructions` from its supported params on the premise that xAI does not accept it. xAI does accept it — it is in their API reference, and the live API obeys it and echoes it back, including alongside a server-side `web_search` tool. The exclusion was harmless until `web_search_options` began routing xAI completions through the Responses API bridge, which hoists a system message into `instructions`. Since then, any xAI web-search request carrying a system message fails: litellm.UnsupportedParamsError: LlmProviders.XAI does not support parameters: {'instructions': 'Answer briefly.'}, for model=grok-4.3 `drop_params=True` is not a workaround: `instructions` is the caller's system prompt, so the call succeeds having silently discarded it. `map_openai_params` also popped `instructions`, but `_check_valid_arg` raises first, so that branch was unreachable. Both go. The regression test injects a mocked client via `client=` rather than monkeypatching `HTTPHandler.post`, per the testing guidance in CLAUDE.md. Fixes #37127 --- litellm/llms/xai/responses/transformation.py | 29 +----- .../test_xai_responses_transformation.py | 88 +++++++++++++++++-- .../xai/xai_responses/test_transformation.py | 17 ++-- 3 files changed, 97 insertions(+), 37 deletions(-) diff --git a/litellm/llms/xai/responses/transformation.py b/litellm/llms/xai/responses/transformation.py index 646d6798783..687aefde939 100644 --- a/litellm/llms/xai/responses/transformation.py +++ b/litellm/llms/xai/responses/transformation.py @@ -46,7 +46,6 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): Inherits from OpenAIResponsesAPIConfig since XAI's Responses API is largely compatible with OpenAI's, with a few differences: - - Does not support the 'instructions' parameter - Requires code_interpreter tools to have 'container' field removed - Recommends store=false when sending images @@ -57,20 +56,6 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): def custom_llm_provider(self) -> LlmProviders: return LlmProviders.XAI - def get_supported_openai_params(self, model: str) -> list: - """ - Get supported parameters for XAI Responses API. - - XAI supports most OpenAI Responses API params except 'instructions'. - """ - supported_params: Final = super().get_supported_openai_params(model) - - # Remove 'instructions' as it's not supported by XAI - if "instructions" in supported_params: - supported_params.remove("instructions") - - return supported_params - def _transform_web_search_tool(self, tool: Mapping[str, object]) -> Mapping[str, object]: """ Transform web_search tool to XAI format. @@ -160,19 +145,13 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig): Map parameters for XAI Responses API. Handles XAI-specific transformations: - 1. Drops 'instructions' parameter (not supported) - 2. Transforms code_interpreter tools to remove 'container' field - 3. Transforms web_search tools to XAI format (removes search_context_size, adds filters) - 4. Transforms x_search tools to XAI format - 5. Sets store=false when images are detected (recommended by XAI) + 1. Transforms code_interpreter tools to remove 'container' field + 2. Transforms web_search tools to XAI format (removes search_context_size, adds filters) + 3. Transforms x_search tools to XAI format + 4. Sets store=false when images are detected (recommended by XAI) """ params: Final = dict(response_api_optional_params) - # Drop instructions parameter (not supported by XAI) - if "instructions" in params: - verbose_logger.debug("XAI Responses API does not support 'instructions' parameter. Dropping it.") - params.pop("instructions") - if "metadata" in params: verbose_logger.debug("XAI Responses API does not support 'metadata' parameter. Dropping it.") params.pop("metadata") diff --git a/tests/test_litellm/llms/xai/responses/test_xai_responses_transformation.py b/tests/test_litellm/llms/xai/responses/test_xai_responses_transformation.py index 4cff5c76b9e..4693c9b1fa0 100644 --- a/tests/test_litellm/llms/xai/responses/test_xai_responses_transformation.py +++ b/tests/test_litellm/llms/xai/responses/test_xai_responses_transformation.py @@ -7,12 +7,14 @@ transformations for the Responses API. Source: litellm/llms/xai/responses/transformation.py """ +from typing import Final from unittest.mock import MagicMock, Mock import httpx import pytest import litellm +from litellm.llms.custom_httpx.http_handler import HTTPHandler from litellm.llms.xai.cost_calculator import cost_per_token from litellm.llms.xai.responses.transformation import XAIResponsesAPIConfig from litellm.responses.utils import ResponseAPILoggingUtils @@ -53,23 +55,26 @@ class TestXAIResponsesAPITransformation: assert result["tools"][0]["type"] == "code_interpreter" assert "container" not in result["tools"][0], "Container field should be removed" - def test_instructions_parameter_dropped(self): - """Test that instructions parameter is dropped for XAI""" + def test_instructions_parameter_preserved(self): + """XAI accepts `instructions`, so it must survive param mapping. + + Dropping it silently discards the caller's system prompt. + """ config = XAIResponsesAPIConfig() params = ResponsesAPIOptionalRequestParams(instructions="You are a helpful assistant.", temperature=0.7) result = config.map_openai_params(response_api_optional_params=params, model="grok-4-fast", drop_params=False) - assert "instructions" not in result, "Instructions should be dropped" + assert result.get("instructions") == "You are a helpful assistant.", "Instructions should be preserved" assert result.get("temperature") == 0.7, "Other params should be preserved" - def test_supported_params_excludes_instructions(self): - """Test that get_supported_openai_params excludes instructions""" + def test_supported_params_includes_instructions(self): + """Test that get_supported_openai_params includes instructions""" config = XAIResponsesAPIConfig() supported = config.get_supported_openai_params("grok-4-fast") - assert "instructions" not in supported, "instructions should not be supported" + assert "instructions" in supported, "instructions should be supported" assert "tools" in supported, "tools should be supported" assert "temperature" in supported, "temperature should be supported" assert "model" in supported, "model should be supported" @@ -492,3 +497,74 @@ class TestXAIResponsesReportedCost: ) assert usage.cost is None + + +class TestXAICompletionBridgeSystemPrompt: + """`web_search_options` bridges xAI completions to the Responses API. + + The bridge hoists a system message into `instructions`, so excluding + `instructions` from XAI's supported params made every web-search request + carrying a system prompt fail — and made `drop_params=True` silently + discard the system prompt instead. + """ + + MESSAGES = [ + {"role": "system", "content": "Answer briefly."}, + {"role": "user", "content": "Newest litellm version on PyPI?"}, + ] + + @staticmethod + def _mock_response() -> MagicMock: + mock_resp: Final = MagicMock() + mock_resp.status_code = 200 + mock_resp.headers = {} + mock_resp.text = "raw" + mock_resp.json.return_value = { + "id": "resp_1", + "object": "response", + "created_at": 1754900000, + "model": "grok-4.3", + "status": "completed", + "parallel_tool_calls": True, + "tool_choice": "auto", + "tools": [], + "top_p": 1.0, + "output": [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "1.97.0", "annotations": []}], + } + ], + "usage": { + "input_tokens": 100, + "output_tokens": 20, + "total_tokens": 120, + "input_tokens_details": {"cached_tokens": 0}, + "output_tokens_details": {"reasoning_tokens": 0}, + }, + } + return mock_resp + + def test_system_prompt_survives_web_search_bridge(self): + """A system message + web_search_options must reach XAI as `instructions`.""" + client: Final = HTTPHandler() + client.post = MagicMock(return_value=self._mock_response()) + + litellm.completion( + model="xai/grok-4.3", + messages=self.MESSAGES, + web_search_options={"search_context_size": "medium"}, + api_key="fake-key", + client=client, + ) + + client.post.assert_called_once() + request_body: Final = client.post.call_args.kwargs["json"] + + assert request_body["instructions"] == "Answer briefly.", "System prompt must not be dropped" + assert [tool["type"] for tool in request_body["tools"]] == ["web_search"] + # The system message is carried by `instructions`, not duplicated into input. + assert [item["role"] for item in request_body["input"]] == ["user"] diff --git a/tests/test_litellm/llms/xai/xai_responses/test_transformation.py b/tests/test_litellm/llms/xai/xai_responses/test_transformation.py index c783918ca06..9f6bcbab2e4 100644 --- a/tests/test_litellm/llms/xai/xai_responses/test_transformation.py +++ b/tests/test_litellm/llms/xai/xai_responses/test_transformation.py @@ -53,8 +53,11 @@ class TestXAIResponsesAPITransformation: "container" not in result["tools"][0] ), "Container field should be removed" - def test_instructions_parameter_dropped(self): - """Test that instructions parameter is dropped for XAI""" + def test_instructions_parameter_preserved(self): + """XAI accepts `instructions`, so it must survive param mapping. + + Dropping it silently discards the caller's system prompt. + """ config = XAIResponsesAPIConfig() params = ResponsesAPIOptionalRequestParams( @@ -65,15 +68,17 @@ class TestXAIResponsesAPITransformation: response_api_optional_params=params, model="grok-4-fast", drop_params=False ) - assert "instructions" not in result, "Instructions should be dropped" + assert ( + result.get("instructions") == "You are a helpful assistant." + ), "Instructions should be preserved" assert result.get("temperature") == 0.7, "Other params should be preserved" - def test_supported_params_excludes_instructions(self): - """Test that get_supported_openai_params excludes instructions""" + def test_supported_params_includes_instructions(self): + """Test that get_supported_openai_params includes instructions""" config = XAIResponsesAPIConfig() supported = config.get_supported_openai_params("grok-4-fast") - assert "instructions" not in supported, "instructions should not be supported" + assert "instructions" in supported, "instructions should be supported" assert "tools" in supported, "tools should be supported" assert "temperature" in supported, "temperature should be supported" assert "model" in supported, "model should be supported"