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"