From b9f3736c20f70872f0bb0cf0fa793afc1e027701 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:01:46 +0000 Subject: [PATCH 1/3] fix(xai): stop sending web_search_options to xAI's retired Live Search path Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/xai/chat/transformation.py | 16 +++++++++++++--- litellm/main.py | 9 +++++---- .../llms/xai/test_xai_chat_transformation.py | 18 ++++++++++++++++++ .../test_xai_responses_auto_routing.py | 11 +++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index ae5849812bf..5b07823a36c 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -214,10 +214,20 @@ class XAIChatConfig(OpenAIGPTConfig): """ Handle https://github.com/BerriAI/litellm/issues/9720 - Filter out 'name' from messages + Filter out 'name' from messages, and drop 'web_search_options': xAI retired Live Search on + /v1/chat/completions and now answers those requests with a 410. xAI web search lives on the + Responses API, where completion() bridges it to a native 'web_search' tool """ - messages = strip_name_from_messages(messages) - return super().transform_request(model, messages, optional_params, litellm_params, headers) + 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." + ) + + 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 98f92e50599..2551c884884 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -1028,10 +1028,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) @@ -1040,6 +1036,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 e5e853ec82f..7b64240eb5c 100644 --- a/tests/test_litellm/llms/xai/test_xai_chat_transformation.py +++ b/tests/test_litellm/llms/xai/test_xai_chat_transformation.py @@ -119,6 +119,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 From c7159328abcb0073278d37cb6dcae408ec041077 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:01:52 +0000 Subject: [PATCH 2/3] style: ruff format Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/xai/chat/transformation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index 5b07823a36c..c6462f10cc9 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -227,7 +227,9 @@ class XAIChatConfig(OpenAIGPTConfig): 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) + 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: From 2ec59eebf6aa1f61a10a05e5dbd73c08135f7261 Mon Sep 17 00:00:00 2001 From: yassin Date: Wed, 2 Sep 2026 15:45:58 +0000 Subject: [PATCH 3/3] refactor(xai): trim transform_request docstring that restated the code Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/llms/xai/chat/transformation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/litellm/llms/xai/chat/transformation.py b/litellm/llms/xai/chat/transformation.py index c6462f10cc9..f9140601101 100644 --- a/litellm/llms/xai/chat/transformation.py +++ b/litellm/llms/xai/chat/transformation.py @@ -211,13 +211,7 @@ class XAIChatConfig(OpenAIGPTConfig): litellm_params: dict, headers: dict, ) -> dict: - """ - Handle https://github.com/BerriAI/litellm/issues/9720 - - Filter out 'name' from messages, and drop 'web_search_options': xAI retired Live Search on - /v1/chat/completions and now answers those requests with a 410. xAI web search lives on the - Responses API, where completion() bridges it to a native 'web_search' tool - """ + """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). "