mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
0af33fbe70
commit
379273a1e0
4 changed files with 113 additions and 8 deletions
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue