Update tests and transformation logic to standardize functionCall casing. Add test for parallel tool calls inheriting thought signature.

This commit is contained in:
Julian von der Goltz 2026-02-13 14:57:38 +01:00
parent cff3e0b75e
commit f752005669
4 changed files with 87 additions and 10 deletions

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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"