mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
Update tests and transformation logic to standardize functionCall casing. Add test for parallel tool calls inheriting thought signature.
This commit is contained in:
parent
cff3e0b75e
commit
f752005669
4 changed files with 87 additions and 10 deletions
|
|
@ -1394,7 +1394,8 @@ def convert_to_gemini_tool_call_invoke(
|
|||
)
|
||||
if gemini_function_call is not None:
|
||||
part_dict: VertexPartType = {
|
||||
"function_call": gemini_function_call
|
||||
# Gemini request payloads should use camelCase functionCall.
|
||||
"functionCall": gemini_function_call
|
||||
}
|
||||
thought_signature = _get_thought_signature_from_tool(
|
||||
dict(tool), model=model
|
||||
|
|
@ -1415,7 +1416,8 @@ def convert_to_gemini_tool_call_invoke(
|
|||
)
|
||||
if gemini_function_call is not None:
|
||||
part_dict_function: VertexPartType = {
|
||||
"function_call": gemini_function_call
|
||||
# Gemini request payloads should use camelCase functionCall.
|
||||
"functionCall": gemini_function_call
|
||||
}
|
||||
|
||||
# Extract thought signature from function_call's provider_specific_fields
|
||||
|
|
|
|||
|
|
@ -985,8 +985,36 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915
|
|||
gemini_tool_call_parts = convert_to_gemini_tool_call_invoke(
|
||||
assistant_msg, model=model
|
||||
)
|
||||
|
||||
# Gemini validates thought signatures on function-call turns.
|
||||
# If a signature exists for this assistant turn (e.g. from thinking_blocks),
|
||||
# copy it to parallel function calls missing thoughtSignature.
|
||||
turn_thought_signature = None
|
||||
for existing_part in assistant_content:
|
||||
if (
|
||||
isinstance(existing_part, dict)
|
||||
and existing_part.get("thoughtSignature")
|
||||
and (
|
||||
existing_part.get("functionCall") is not None
|
||||
or existing_part.get("function_call") is not None
|
||||
)
|
||||
):
|
||||
turn_thought_signature = existing_part.get(
|
||||
"thoughtSignature"
|
||||
)
|
||||
break
|
||||
|
||||
## check if gemini_tool_call already exists in assistant_content
|
||||
for gemini_tool_call_part in gemini_tool_call_parts:
|
||||
if (
|
||||
turn_thought_signature is not None
|
||||
and isinstance(gemini_tool_call_part, dict)
|
||||
and gemini_tool_call_part.get("thoughtSignature") is None
|
||||
):
|
||||
gemini_tool_call_part["thoughtSignature"] = (
|
||||
turn_thought_signature
|
||||
)
|
||||
|
||||
if not check_if_part_exists_in_parts(
|
||||
assistant_content,
|
||||
gemini_tool_call_part,
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ def test_convert_to_gemini_with_embedded_signature():
|
|||
|
||||
# Verify thought signature is extracted and sent to Gemini
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
assert gemini_parts[0]["thoughtSignature"] == test_signature
|
||||
|
||||
|
|
|
|||
|
|
@ -453,12 +453,12 @@ def test_thought_signature_preservation_in_conversion():
|
|||
|
||||
# Verify thought signature is preserved in first function call part
|
||||
assert len(gemini_parts) == 2
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
assert gemini_parts[0]["thoughtSignature"] == test_signature
|
||||
|
||||
# Verify second function call part does not have thought signature
|
||||
assert "function_call" in gemini_parts[1]
|
||||
assert "functionCall" in gemini_parts[1]
|
||||
assert "thoughtSignature" not in gemini_parts[1]
|
||||
|
||||
|
||||
|
|
@ -522,6 +522,53 @@ def test_thought_signature_sequential_function_calls():
|
|||
assert gemini_parts_step2[0]["thoughtSignature"] == signature_2
|
||||
|
||||
|
||||
def test_parallel_tool_calls_copy_thought_signature_from_thinking_block():
|
||||
"""Test that parallel tool calls inherit turn thought signature from thinking_blocks."""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"thinking_blocks": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": '{"functionCall":{"name":"get_current_temperature","args":{"location":"Paris"}}}',
|
||||
"signature": "sig-turn-123",
|
||||
}
|
||||
],
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "Paris"}',
|
||||
},
|
||||
"index": 0,
|
||||
},
|
||||
{
|
||||
"id": "call_2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_current_temperature",
|
||||
"arguments": '{"location": "London"}',
|
||||
},
|
||||
"index": 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages, model="gemini-2.5-flash")
|
||||
|
||||
assert len(contents) == 1
|
||||
model_parts = contents[0]["parts"]
|
||||
function_parts = [
|
||||
p for p in model_parts if p.get("functionCall") is not None or p.get("function_call") is not None
|
||||
]
|
||||
assert len(function_parts) == 2
|
||||
assert all(p.get("thoughtSignature") == "sig-turn-123" for p in function_parts)
|
||||
|
||||
|
||||
def test_thought_signature_with_function_call_mode():
|
||||
"""Test thought signature extraction in function_call mode (is_function_call=True)"""
|
||||
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
|
||||
|
|
@ -589,7 +636,7 @@ def test_dummy_signature_added_for_gemini_3_conversation_history():
|
|||
|
||||
# Verify dummy signature is added
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
|
||||
# Verify it's the expected dummy signature (base64 encoded "skip_thought_signature_validator")
|
||||
|
|
@ -630,7 +677,7 @@ def test_dummy_signature_not_added_for_gemini_2_5():
|
|||
|
||||
# Verify no dummy signature is added for non-gemini-3 models
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" not in gemini_parts[0]
|
||||
|
||||
|
||||
|
|
@ -669,7 +716,7 @@ def test_dummy_signature_not_added_when_signature_exists():
|
|||
|
||||
# Verify real signature is preserved, not replaced with dummy
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
assert gemini_parts[0]["thoughtSignature"] == real_signature
|
||||
|
||||
|
|
@ -700,7 +747,7 @@ def test_dummy_signature_with_function_call_mode():
|
|||
|
||||
# Verify dummy signature is added
|
||||
assert len(gemini_parts) == 1
|
||||
assert "function_call" in gemini_parts[0]
|
||||
assert "functionCall" in gemini_parts[0]
|
||||
assert "thoughtSignature" in gemini_parts[0]
|
||||
|
||||
# Verify it's the expected dummy signature
|
||||
|
|
@ -1938,7 +1985,7 @@ def test_function_response_has_user_role():
|
|||
|
||||
assert contents[0]["role"] == "user"
|
||||
assert contents[1]["role"] == "model"
|
||||
assert "function_call" in contents[1]["parts"][0]
|
||||
assert "functionCall" in contents[1]["parts"][0]
|
||||
|
||||
# The critical assertion: function response must have role="user"
|
||||
assert contents[2]["role"] == "user"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue