Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions litellm/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@
"store": None,
"metadata": None,
"context_management": None,
"include_server_side_tool_invocations": None,
}

openai_compatible_endpoints: List = [
Expand Down
1 change: 1 addition & 0 deletions litellm/llms/gemini/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def get_supported_openai_params(self, model: str) -> List[str]:
"parallel_tool_calls",
"web_search_options",
"service_tier",
"include_server_side_tool_invocations",
]
if supports_reasoning(model, custom_llm_provider="gemini"):
supported_params.append("reasoning_effort")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,8 @@ def map_openai_params( # noqa: PLR0915
model: str,
drop_params: bool,
) -> Dict:
if non_default_params.get("include_server_side_tool_invocations") is True:
optional_params["include_server_side_tool_invocations"] = True
for param, value in non_default_params.items():
if param == "temperature":
if VertexGeminiConfig._is_gemini_3_or_newer(model):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3499,7 +3499,12 @@ def test_video_metadata_supported_for_all_gemini_models():
}
]

for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro", "gemini-3-pro-preview"]:
for model in [
"gemini-1.5-pro",
"gemini-2.5-flash",
"gemini-2.5-pro",
"gemini-3-pro-preview",
]:
contents = _gemini_convert_messages_with_history(messages=messages, model=model)

file_part = None
Expand All @@ -3509,19 +3514,25 @@ def test_video_metadata_supported_for_all_gemini_models():
break

assert file_part is not None, f"{model}: file part should exist"
assert "video_metadata" in file_part, f"{model}: video_metadata should be present"
assert (
"video_metadata" in file_part
), f"{model}: video_metadata should be present"
assert file_part["video_metadata"]["fps"] == 5, f"{model}: fps should be 5"

# Per-part media_resolution is Gemini 3+ only; 2.x uses generation_config global
for model in ["gemini-3-pro-preview"]:
contents = _gemini_convert_messages_with_history(messages=messages, model=model)
file_part = next(p for p in contents[0]["parts"] if "file_data" in p)
assert "media_resolution" in file_part, f"{model}: media_resolution should be present"
assert (
"media_resolution" in file_part
), f"{model}: media_resolution should be present"

for model in ["gemini-1.5-pro", "gemini-2.5-flash", "gemini-2.5-pro"]:
contents = _gemini_convert_messages_with_history(messages=messages, model=model)
file_part = next(p for p in contents[0]["parts"] if "file_data" in p)
assert "media_resolution" not in file_part, f"{model}: per-part media_resolution should not be set"
assert (
"media_resolution" not in file_part
), f"{model}: per-part media_resolution should not be set"


def test_chunk_parser_handles_prompt_feedback_block():
Expand Down Expand Up @@ -4154,8 +4165,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_in_prompt():

# DOCUMENT tokens should be included in text_tokens: 8 (TEXT) + 774 (DOCUMENT) = 782
assert result.prompt_tokens_details is not None
assert result.prompt_tokens_details.text_tokens == 782, \
"DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)"
assert (
result.prompt_tokens_details.text_tokens == 782
), "DOCUMENT modality tokens should be added to text_tokens (8 TEXT + 774 DOCUMENT = 782)"

# Verify completion token details
assert result.completion_tokens_details is not None
Expand Down Expand Up @@ -4190,8 +4202,9 @@ def test_vertex_ai_usage_metadata_with_document_tokens_cached():

# DOCUMENT cached tokens map to cached_text_tokens, so:
# text_tokens = (8 TEXT + 774 DOCUMENT) - 400 cached = 382
assert result.prompt_tokens_details.text_tokens == 382, \
"text_tokens should be (8 + 774) - 400 cached = 382"
assert (
result.prompt_tokens_details.text_tokens == 382
), "text_tokens should be (8 + 774) - 400 cached = 382"
assert result.prompt_tokens_details.cached_tokens == 400


Expand Down Expand Up @@ -4290,3 +4303,91 @@ def test_transform_response_does_not_leak_body_on_parse_failure():
msg = str(exc_info.value)
assert "secret content" not in msg
assert "Error converting to valid response block" in msg


class TestServerSideToolInvocationsSearchToolPreservation:
"""
Regression tests for https://github.com/BerriAI/litellm/issues/27479

When include_server_side_tool_invocations=True is passed alongside
mixed function + search tools, the search tool must NOT be dropped.
"""

def test_search_tool_preserved_with_server_side_invocations(self):
"""map_openai_params must preserve googleSearch when the flag is set,
even though tools iterates before the flag handler."""
v = VertexGeminiConfig()
result = v.map_openai_params(
non_default_params={
"tools": [
{"google_search": {}},
{
"type": "function",
"function": {
"name": "send_message",
"description": "Send a message",
"parameters": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
},
},
],
"include_server_side_tool_invocations": True,
},
optional_params={},
model="gemini-3.1-pro-preview",
drop_params=False,
)
tools = result["tools"]
has_google_search = any("googleSearch" in t for t in tools)
has_func_declarations = any("function_declarations" in t for t in tools)
assert has_google_search, "googleSearch should be preserved"
assert has_func_declarations, "function declarations should be preserved"

def test_search_tool_dropped_without_server_side_invocations(self):
"""Without include_server_side_tool_invocations, mixed tools should
still drop search tools (existing behavior)."""
v = VertexGeminiConfig()
result = v.map_openai_params(
non_default_params={
"tools": [
{"google_search": {}},
{
"type": "function",
"function": {
"name": "send_message",
"description": "Send a message",
"parameters": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
},
},
],
},
optional_params={},
model="gemini-3.1-pro-preview",
drop_params=False,
)
tools = result["tools"]
has_google_search = any("googleSearch" in t for t in tools)
assert not has_google_search, "googleSearch should be dropped"

def test_google_ai_studio_supports_server_side_invocations(self):
"""GoogleAIStudioGeminiConfig must list the flag in supported params."""
cfg = GoogleAIStudioGeminiConfig()
params = cfg.get_supported_openai_params(model="gemini-3.1-pro-preview")
assert "include_server_side_tool_invocations" in params

def test_flag_in_default_chat_completion_params(self):
"""include_server_side_tool_invocations must be in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
so get_non_default_params does not strip it."""
from litellm.constants import DEFAULT_CHAT_COMPLETION_PARAM_VALUES

assert (
"include_server_side_tool_invocations"
in DEFAULT_CHAT_COMPLETION_PARAM_VALUES
)
Loading