mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(vertex_ai): only fall back to a placeholder thought signature on the first parallel function call
Gemini returns a thoughtSignature on the first function call of a parallel batch and leaves the siblings bare. When replaying that assistant turn, litellm gave every unsigned call the skip_thought_signature_validator placeholder, so a three-call turn went back with three signatures where Gemini had produced one. Keep the placeholder for the first call only and forward the siblings with whatever signature they actually carry, which is usually none.
This commit is contained in:
parent
da7a10ebbd
commit
aa832d81e9
2 changed files with 126 additions and 12 deletions
|
|
@ -1200,13 +1200,14 @@ def _encode_tool_call_id_with_signature(tool_call_id: str, thought_signature: st
|
|||
return tool_call_id
|
||||
|
||||
|
||||
def _get_thought_signature_from_tool(tool: dict, model: str | None = None) -> str | None:
|
||||
def _get_thought_signature_from_tool(tool: dict) -> str | None:
|
||||
"""Extract thought signature from tool call's provider_specific_fields.
|
||||
|
||||
If not provided try to extract thought signature from tool call id
|
||||
|
||||
Checks both tool.provider_specific_fields and tool.function.provider_specific_fields.
|
||||
If no signature is found and model is gemini-3, returns a dummy signature.
|
||||
Returns None when the tool call carries no signature; callers decide whether a
|
||||
placeholder signature is needed.
|
||||
"""
|
||||
# First check tool's provider_specific_fields
|
||||
provider_fields: Final = tool.get("provider_specific_fields") or {}
|
||||
|
|
@ -1236,13 +1237,6 @@ def _get_thought_signature_from_tool(tool: dict, model: str | None = None) -> st
|
|||
if len(parts) == 2:
|
||||
_, signature = parts
|
||||
return signature
|
||||
# If no signature found and model is gemini-3, return dummy signature
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
VertexGeminiConfig,
|
||||
)
|
||||
|
||||
if model and VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
return _get_dummy_thought_signature()
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -1312,8 +1306,10 @@ def convert_to_gemini_tool_call_invoke(
|
|||
VertexGeminiConfig,
|
||||
)
|
||||
|
||||
needs_dummy_signature: Final = model is not None and VertexGeminiConfig._is_gemini_3_or_newer(model)
|
||||
|
||||
if tool_calls is not None:
|
||||
for idx, tool in enumerate(tool_calls):
|
||||
for tool in tool_calls:
|
||||
if "function" in tool:
|
||||
gemini_function_call: VertexFunctionCall | None = _gemini_tool_call_invoke_helper(
|
||||
function_call_params=tool["function"],
|
||||
|
|
@ -1321,7 +1317,10 @@ def convert_to_gemini_tool_call_invoke(
|
|||
)
|
||||
if gemini_function_call is not None:
|
||||
part_dict: VertexPartType = {"function_call": gemini_function_call}
|
||||
thought_signature = _get_thought_signature_from_tool(dict(tool), model=model)
|
||||
thought_signature = _get_thought_signature_from_tool(dict(tool))
|
||||
is_first_function_call = len(_parts_list) == 0
|
||||
if not thought_signature and is_first_function_call and needs_dummy_signature:
|
||||
thought_signature = _get_dummy_thought_signature()
|
||||
if thought_signature:
|
||||
part_dict["thoughtSignature"] = thought_signature
|
||||
|
||||
|
|
@ -1344,7 +1343,7 @@ def convert_to_gemini_tool_call_invoke(
|
|||
thought_signature = provider_fields.get("thought_signature")
|
||||
|
||||
# If no signature found and model is gemini-3, use dummy signature
|
||||
if not thought_signature and model and VertexGeminiConfig._is_gemini_3_or_newer(model):
|
||||
if not thought_signature and needs_dummy_signature:
|
||||
thought_signature = _get_dummy_thought_signature()
|
||||
|
||||
if thought_signature:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import base64
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_result,
|
||||
)
|
||||
|
|
@ -784,6 +786,119 @@ def test_dummy_signature_with_function_call_mode():
|
|||
assert gemini_parts[0]["thoughtSignature"] == expected_dummy
|
||||
|
||||
|
||||
def _parallel_tool_calls(*signatures):
|
||||
return [
|
||||
{
|
||||
"id": f"call_{idx}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": f"tool_{idx}",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
**(
|
||||
{"provider_specific_fields": {"thought_signature": signature}}
|
||||
if signature is not None
|
||||
else {}
|
||||
),
|
||||
},
|
||||
"index": idx,
|
||||
}
|
||||
for idx, signature in enumerate(signatures)
|
||||
]
|
||||
|
||||
|
||||
REAL_THOUGHT_SIGNATURE = "Co4CAdHtim/rWgXbz2Ghp4tShzLeMASrPw6JJyYIC3cbVyZnKzU3uv8/wVzyS2sKRPL2m8QQHHXbNQhEEz500G7n"
|
||||
|
||||
|
||||
def test_dummy_signature_only_on_first_parallel_tool_call():
|
||||
"""Gemini only returns a thought signature on the first of N parallel function calls.
|
||||
|
||||
The sibling calls carry no signature, so replaying them must not fabricate one.
|
||||
"""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": _parallel_tool_calls(None, None, None),
|
||||
},
|
||||
model="gemini-3-pro-preview",
|
||||
)
|
||||
|
||||
expected_dummy = base64.b64encode(b"skip_thought_signature_validator").decode(
|
||||
"utf-8"
|
||||
)
|
||||
assert len(gemini_parts) == 3
|
||||
assert gemini_parts[0]["thoughtSignature"] == expected_dummy
|
||||
assert "thoughtSignature" not in gemini_parts[1]
|
||||
assert "thoughtSignature" not in gemini_parts[2]
|
||||
|
||||
|
||||
def test_real_signature_on_first_parallel_tool_call_leaves_siblings_empty():
|
||||
"""The real signature from Gemini rides on the first call; siblings stay signature-free."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": _parallel_tool_calls(REAL_THOUGHT_SIGNATURE, None, None),
|
||||
},
|
||||
model="gemini-3-pro-preview",
|
||||
)
|
||||
|
||||
assert len(gemini_parts) == 3
|
||||
assert gemini_parts[0]["thoughtSignature"] == REAL_THOUGHT_SIGNATURE
|
||||
assert "thoughtSignature" not in gemini_parts[1]
|
||||
assert "thoughtSignature" not in gemini_parts[2]
|
||||
|
||||
|
||||
def test_real_signature_on_later_parallel_tool_call_is_preserved():
|
||||
"""A signature attached to a non-first call is still forwarded as-is."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": _parallel_tool_calls(None, REAL_THOUGHT_SIGNATURE),
|
||||
},
|
||||
model="gemini-3-pro-preview",
|
||||
)
|
||||
|
||||
expected_dummy = base64.b64encode(b"skip_thought_signature_validator").decode(
|
||||
"utf-8"
|
||||
)
|
||||
assert len(gemini_parts) == 2
|
||||
assert gemini_parts[0]["thoughtSignature"] == expected_dummy
|
||||
assert gemini_parts[1]["thoughtSignature"] == REAL_THOUGHT_SIGNATURE
|
||||
|
||||
|
||||
def test_no_signatures_on_parallel_tool_calls_for_gemini_2_5():
|
||||
"""Non-gemini-3 models never get a placeholder signature, on any call."""
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
convert_to_gemini_tool_call_invoke,
|
||||
)
|
||||
|
||||
gemini_parts = convert_to_gemini_tool_call_invoke(
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": _parallel_tool_calls(None, None),
|
||||
},
|
||||
model="gemini-2.5-flash",
|
||||
)
|
||||
|
||||
assert len(gemini_parts) == 2
|
||||
assert all("thoughtSignature" not in part for part in gemini_parts)
|
||||
|
||||
|
||||
# Tests for media_resolution (detail parameter) handling - Issue #17084
|
||||
class TestMediaResolution:
|
||||
"""Tests for media_resolution handling in Gemini 2.x models"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue