Merge pull request #38254 from BerriAI/litellm_fix_xai_responses_instructions

This commit is contained in:
Yassin Kortam 2026-09-15 19:55:52 -07:00 committed by GitHub
commit d108cdc431
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 71 additions and 38 deletions

View file

@ -49,7 +49,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
@ -60,20 +59,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.
@ -158,19 +143,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")

View file

@ -51,23 +51,23 @@ 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_forwarded(self):
"""xAI supports 'instructions' on /v1/responses, so it must survive param mapping"""
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."
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):
"""A system message bridged to 'instructions' must not be rejected for xAI"""
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"

View file

@ -53,8 +53,8 @@ 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_forwarded(self):
"""xAI supports 'instructions' on /v1/responses, so it must survive param mapping"""
config = XAIResponsesAPIConfig()
params = ResponsesAPIOptionalRequestParams(
@ -65,15 +65,15 @@ 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."
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):
"""A system message bridged to 'instructions' must not be rejected for xAI"""
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"

View file

@ -2,14 +2,30 @@
Test automatic routing to xAI Responses API when tools are present
"""
import json
from collections.abc import Mapping
from typing import Final
from unittest.mock import MagicMock, patch
import httpx
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import HTTPHandler
from litellm.main import responses_api_bridge_check
class _RecordingResponsesHandler:
"""MockTransport handler that serves a canned /responses reply and keeps the body xAI would have received"""
def __init__(self, reply: Mapping[str, object]) -> None:
self.reply: Final = reply
self.request_body: Mapping[str, object] | None = None
def __call__(self, request: httpx.Request) -> httpx.Response:
self.request_body = json.loads(request.content)
return httpx.Response(200, json=dict(self.reply), request=request)
class TestXAIResponsesAutoRouting:
"""Test that xAI requests with tools automatically route to Responses API"""
@ -254,6 +270,44 @@ class TestXAIResponsesAutoRouting:
# Note: This test may need adjustment based on actual mock_response behavior
# The key is that the responses_api_bridge_check logic routes correctly
def test_system_message_survives_web_search_bridge(self):
"""A system message becomes 'instructions' on the bridged /responses call, and xAI accepts it"""
handler: Final = _RecordingResponsesHandler(
reply={
"id": "resp_test",
"object": "response",
"created_at": 0,
"status": "completed",
"model": "grok-4.6",
"output": [
{
"type": "message",
"id": "msg_test",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": "1.0.0", "annotations": []}],
}
],
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
}
)
response: Final = litellm.completion(
model="xai/grok-4.6",
messages=[
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": "newest litellm version?"},
],
web_search_options={"search_context_size": "medium"},
api_key="fake-key",
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handler))),
)
assert response.choices[0].message.content == "1.0.0"
assert handler.request_body is not None
assert handler.request_body["instructions"] == "Answer briefly."
assert handler.request_body["tools"] == [{"type": "web_search"}]
if __name__ == "__main__":
pytest.main([__file__, "-v"])