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>
This commit is contained in:
Devin AI 2026-08-25 23:01:46 +00:00
parent e4ff44f623
commit b9f3736c20
4 changed files with 47 additions and 7 deletions

View file

@ -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:

View file

@ -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.

View file

@ -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)

View file

@ -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