mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(xai): stop rejecting instructions on the Responses API
`XAIResponsesAPIConfig` removed `instructions` from its supported params
on the premise that xAI does not accept it. xAI does accept it — it is in
their API reference, and the live API obeys it and echoes it back, including
alongside a server-side `web_search` tool.
The exclusion was harmless until `web_search_options` began routing xAI
completions through the Responses API bridge, which hoists a system message
into `instructions`. Since then, any xAI web-search request carrying a system
message fails:
litellm.UnsupportedParamsError: LlmProviders.XAI does not support
parameters: {'instructions': 'Answer briefly.'}, for model=grok-4.3
`drop_params=True` is not a workaround: `instructions` is the caller's system
prompt, so the call succeeds having silently discarded it.
`map_openai_params` also popped `instructions`, but `_check_valid_arg` raises
first, so that branch was unreachable. Both go.
The regression test injects a mocked client via `client=` rather than
monkeypatching `HTTPHandler.post`, per the testing guidance in CLAUDE.md.
Fixes #37127
This commit is contained in:
parent
168a0055a2
commit
26f601f968
3 changed files with 97 additions and 37 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue