diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index 09af8544b28..710215a9a86 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -219,13 +219,19 @@ class XAIChatConfig(OpenAIGPTConfig): litellm_params: dict, headers: dict, ) -> dict: - """ - Handle https://github.com/BerriAI/litellm/issues/9720 + """Handle https://github.com/BerriAI/litellm/issues/9720""" + if "web_search_options" in optional_params: + verbose_logger.warning( + "XAI no longer supports web search on /chat/completions (Live Search is deprecated). " + "Dropping 'web_search_options'. Use the Responses API for XAI web search." + ) - Filter out 'name' from messages - """ - messages = strip_name_from_messages(messages) - return super().transform_request(model, messages, optional_params, litellm_params, headers) + chat_params: Final = { # mutable-ok: base transform_request takes a plain dict of optional params + key: value for key, value in optional_params.items() if key != "web_search_options" + } + return super().transform_request( + model, strip_name_from_messages(messages), chat_params, litellm_params, headers + ) @staticmethod def _fix_choice_finish_reason_for_tool_calls(choice: Choices) -> None: diff --git a/litellm/main.py b/litellm/main.py index 2929790f2bd..9e71d99881b 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1036,10 +1036,6 @@ def responses_api_bridge_check( mode = "responses" model_info["mode"] = mode - if web_search_options is not None and custom_llm_provider == "xai": - model_info["mode"] = "responses" - model = model.replace("responses/", "") - except Exception as e: verbose_logger.debug("Error getting model info: %s", e) @@ -1048,6 +1044,11 @@ def responses_api_bridge_check( mode = "responses" model_info["mode"] = mode + # xAI retired Live Search on /v1/chat/completions (410), so web search only works on /v1/responses + if web_search_options is not None and custom_llm_provider == "xai": + model_info["mode"] = "responses" + model = model.replace("responses/", "") + # OpenAI/Azure GPT-5 chat-completions that need Responses-only fields (e.g. # ``reasoningSummary`` in ``extra_body``) must be bridged; Chat Completions rejects # those keys. diff --git a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py index c67dca11a56..524ca6a02d7 100644 --- a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py +++ b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py @@ -124,6 +124,24 @@ class TestXAIParallelToolCalls: assert result["messages"][0]["role"] == "user" +class TestXAIChatWebSearchOptions: + """XAI answers /chat/completions requests carrying web_search_options with a 410 (Live Search retired)""" + + def test_transform_request_drops_web_search_options(self): + config = XAIChatConfig() + + result = config.transform_request( + model="xai/grok-4.6", + messages=[{"role": "user", "content": "newest litellm version?"}], + optional_params={"web_search_options": {"search_context_size": "medium"}, "temperature": 0.5}, + litellm_params={}, + headers={}, + ) + + assert "web_search_options" not in result + assert result["temperature"] == 0.5 + + class TestXAIUsageNormalization: def test_preserves_reasoning_tokens_in_total_usage(self): usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=200) diff --git a/tests/test_litellm/test_xai_responses_auto_routing.py b/tests/test_litellm/test_xai_responses_auto_routing.py index 5b1944dcb8b..fbf2453d7fb 100644 --- a/tests/test_litellm/test_xai_responses_auto_routing.py +++ b/tests/test_litellm/test_xai_responses_auto_routing.py @@ -204,6 +204,17 @@ class TestXAIResponsesAutoRouting: assert model_info.get("mode") == "responses" assert updated_model == model + def test_responses_api_bridge_check_with_web_search_options_on_unmapped_model(self): + """web search must reach /responses even for a model missing from the cost map, chat returns 410""" + model_info, updated_model = responses_api_bridge_check( + model="grok-not-in-cost-map", + custom_llm_provider="xai", + web_search_options={"search_context_size": "medium"}, + ) + + assert model_info.get("mode") == "responses" + assert updated_model == "grok-not-in-cost-map" + @patch("litellm.completion_extras.responses_api_bridge.completion") def test_completion_with_tools_routes_to_responses_api( self, mock_responses_completion