fix(gemini): preserve search tools when include_server_side_tool_invocations is set

Three independent code paths prevented the include_server_side_tool_invocations
flag from reaching the search tool conflict resolver, causing googleSearch to be
silently dropped when mixed with function tools:

1. Flag missing from DEFAULT_CHAT_COMPLETION_PARAM_VALUES -- get_non_default_params
   stripped it before any provider code ran.
2. GoogleAIStudioGeminiConfig.get_supported_openai_params did not list it --
   with drop_params=True the flag was silently popped.
3. map_openai_params iterated tools before the flag handler, so
   _resolve_search_tool_conflict never saw the flag in optional_params.

Fixes #27479
This commit is contained in:
Jonathan Wrede 2026-05-10 18:26:01 +00:00
parent 0af33fbe70
commit 379273a1e0
4 changed files with 113 additions and 8 deletions

View file

@ -744,6 +744,7 @@ DEFAULT_CHAT_COMPLETION_PARAM_VALUES = {
"store": None,
"metadata": None,
"context_management": None,
"include_server_side_tool_invocations": None,
}
openai_compatible_endpoints: List = [

View file

@ -92,6 +92,7 @@ class GoogleAIStudioGeminiConfig(VertexGeminiConfig):
"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")

View file

@ -1077,6 +1077,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
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):

View file

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