mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
Merge pull request #38268 from BerriAI/litellm_fix_xai_web_search_nested_filters
fix(xai): honor nested web_search filters on the xAI Responses API
This commit is contained in:
commit
1b8daf20e0
2 changed files with 71 additions and 12 deletions
|
|
@ -3,6 +3,7 @@ from types import MappingProxyType
|
|||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
|
|
@ -32,6 +33,8 @@ if TYPE_CHECKING:
|
|||
else:
|
||||
LiteLLMLoggingObj = Any
|
||||
|
||||
_STR_MAPPING_ADAPTER: Final = TypeAdapter(Mapping[str, object])
|
||||
|
||||
|
||||
def _usage_restated_from_xai_ticks(usage: ResponseAPIUsage | None) -> ResponseAPIUsage | None:
|
||||
reported_cost: Final = xai_reported_cost_in_usd(getattr(usage, "cost_in_usd_ticks", None))
|
||||
|
|
@ -81,30 +84,25 @@ class XAIResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
- enable_image_understanding
|
||||
|
||||
XAI does NOT support search_context_size (OpenAI-specific).
|
||||
|
||||
Domains may come nested under 'filters' (the OpenAI/XAI documented shape) or flat on the tool.
|
||||
"""
|
||||
xai_tool: Final[dict[str, object]] = {"type": "web_search"}
|
||||
|
||||
# Remove search_context_size if present (not supported by XAI)
|
||||
if "search_context_size" in tool:
|
||||
verbose_logger.info(
|
||||
"XAI does not support 'search_context_size' parameter. Removing it from web_search tool."
|
||||
)
|
||||
|
||||
# Handle filters (XAI-specific structure)
|
||||
filters: Final = {}
|
||||
if "allowed_domains" in tool:
|
||||
allowed_domains: Final = tool["allowed_domains"]
|
||||
filters["allowed_domains"] = allowed_domains
|
||||
nested_filters: Final = tool.get("filters")
|
||||
domains: Final = (
|
||||
_STR_MAPPING_ADAPTER.validate_python(nested_filters) if isinstance(nested_filters, Mapping) else tool
|
||||
)
|
||||
filters: Final = {key: domains[key] for key in ("allowed_domains", "excluded_domains") if key in domains}
|
||||
|
||||
if "excluded_domains" in tool:
|
||||
excluded_domains: Final = tool["excluded_domains"]
|
||||
filters["excluded_domains"] = excluded_domains
|
||||
|
||||
# Add filters if any were specified
|
||||
if filters:
|
||||
xai_tool["filters"] = filters
|
||||
|
||||
# Handle enable_image_understanding (top-level in XAI format)
|
||||
if "enable_image_understanding" in tool:
|
||||
xai_tool["enable_image_understanding"] = tool["enable_image_understanding"]
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,67 @@ class TestXAIResponsesAPITransformation:
|
|||
assert tool["filters"]["allowed_domains"] == ["wikipedia.org", "x.ai"]
|
||||
assert tool["enable_image_understanding"] is True
|
||||
|
||||
def test_web_search_nested_filters_preserved(self):
|
||||
"""The documented nested 'filters' shape must reach xAI instead of being dropped"""
|
||||
config = XAIResponsesAPIConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search",
|
||||
"filters": {"allowed_domains": ["grokipedia.com"], "excluded_domains": ["example.com"]},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="grok-4-1-fast",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
tool = result["tools"][0]
|
||||
assert tool["filters"]["allowed_domains"] == ["grokipedia.com"]
|
||||
assert tool["filters"]["excluded_domains"] == ["example.com"]
|
||||
|
||||
def test_web_search_nested_filters_win_over_flat(self):
|
||||
"""Nested filters take precedence when both shapes are sent"""
|
||||
config = XAIResponsesAPIConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
tools=[
|
||||
{
|
||||
"type": "web_search",
|
||||
"allowed_domains": ["flat.com"],
|
||||
"filters": {"allowed_domains": ["nested.com"]},
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="grok-4-1-fast",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["tools"][0]["filters"] == {"allowed_domains": ["nested.com"]}
|
||||
|
||||
def test_web_search_empty_nested_filters_win_over_flat(self):
|
||||
"""An explicit empty 'filters' object means unrestricted search, even when stale flat fields are present"""
|
||||
config = XAIResponsesAPIConfig()
|
||||
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
tools=[{"type": "web_search", "allowed_domains": ["flat.com"], "filters": {}}]
|
||||
)
|
||||
|
||||
result = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="grok-4-1-fast",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["tools"][0] == {"type": "web_search"}
|
||||
|
||||
def test_web_search_search_context_size_removed(self):
|
||||
"""Test that search_context_size is removed from web_search tools"""
|
||||
config = XAIResponsesAPIConfig()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue