mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
Merge 1676c3496b into c2c2a623c0
This commit is contained in:
commit
f9da960af9
3 changed files with 202 additions and 2 deletions
|
|
@ -882,8 +882,7 @@ def _gemini_convert_messages_with_history(
|
|||
_message_content = assistant_msg.get("content", None)
|
||||
reasoning_content = assistant_msg.get("reasoning_content", None)
|
||||
thinking_blocks = assistant_msg.get("thinking_blocks")
|
||||
if reasoning_content is not None:
|
||||
assistant_content.append(PartType(thought=True, text=reasoning_content))
|
||||
emitted_thinking = False
|
||||
if thinking_blocks is not None:
|
||||
for block in thinking_blocks:
|
||||
if block["type"] == "thinking":
|
||||
|
|
@ -904,6 +903,9 @@ def _gemini_convert_messages_with_history(
|
|||
text=block_thinking_str,
|
||||
)
|
||||
)
|
||||
emitted_thinking = True
|
||||
if not emitted_thinking and reasoning_content is not None:
|
||||
assistant_content.append(PartType(thought=True, text=reasoning_content))
|
||||
if _message_content is not None and isinstance(_message_content, list):
|
||||
_parts = []
|
||||
for element in _message_content:
|
||||
|
|
|
|||
|
|
@ -2490,3 +2490,109 @@ def test_has_tool_with_name_anthropic_shape_without_type_field():
|
|||
def test_has_tool_with_name_not_a_list():
|
||||
assert not has_tool_with_name(None, "my_tool")
|
||||
assert not has_tool_with_name("not a list", "my_tool")
|
||||
|
||||
|
||||
def test_gemini_no_duplicate_reasoning_parts_when_both_fields_present():
|
||||
"""
|
||||
Regression (#37973): when an assistant message has both reasoning_content and
|
||||
thinking_blocks, only one reasoning part must be emitted. Signed thinking_blocks
|
||||
take priority; reasoning_content is a fallback used only when thinking_blocks is absent.
|
||||
"""
|
||||
reasoning_text = "Two plus two is four."
|
||||
signature = "test-signature-abc123"
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "What is 2+2?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "4",
|
||||
"reasoning_content": reasoning_text,
|
||||
"thinking_blocks": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": reasoning_text,
|
||||
"signature": signature,
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Multiply by 3."},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
assistant_turn = contents[1]
|
||||
assert assistant_turn["role"] == "model"
|
||||
|
||||
parts = assistant_turn["parts"]
|
||||
|
||||
# Only the signed block should appear — no extra plain thought part
|
||||
signed_parts = [p for p in parts if p.get("thoughtSignature") is not None]
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
|
||||
assert len(signed_parts) == 1, (
|
||||
f"Expected 1 signed reasoning part, got {len(signed_parts)}: {signed_parts}"
|
||||
)
|
||||
assert len(plain_thought_parts) == 0, (
|
||||
f"reasoning_content must not be emitted when thinking_blocks is present; "
|
||||
f"got {len(plain_thought_parts)} extra plain thought part(s)"
|
||||
)
|
||||
assert signed_parts[0]["thoughtSignature"] == signature
|
||||
|
||||
|
||||
def test_gemini_reasoning_content_fallback_when_no_thinking_blocks():
|
||||
"""
|
||||
When an assistant message has reasoning_content but no thinking_blocks,
|
||||
a plain thought part should still be emitted (the fallback path still works).
|
||||
"""
|
||||
reasoning_text = "I reasoned about this carefully."
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hi there.",
|
||||
"reasoning_content": reasoning_text,
|
||||
},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
assistant_turn = contents[1]
|
||||
assert assistant_turn["role"] == "model"
|
||||
|
||||
parts = assistant_turn["parts"]
|
||||
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
assert len(plain_thought_parts) == 1, (
|
||||
f"Expected 1 plain thought part from reasoning_content fallback, got {len(plain_thought_parts)}"
|
||||
)
|
||||
assert plain_thought_parts[0]["text"] == reasoning_text
|
||||
|
||||
|
||||
def test_gemini_reasoning_content_fallback_when_thinking_blocks_empty():
|
||||
"""
|
||||
Greptile review: when thinking_blocks is non-null but empty (or has no
|
||||
usable signed blocks), reasoning_content must still be emitted as fallback.
|
||||
"""
|
||||
reasoning_text = "I reasoned carefully."
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hi.",
|
||||
"reasoning_content": reasoning_text,
|
||||
"thinking_blocks": [], # non-null but empty — no usable blocks
|
||||
},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
assistant_turn = contents[1]
|
||||
parts = assistant_turn["parts"]
|
||||
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
assert len(plain_thought_parts) == 1, (
|
||||
f"reasoning_content fallback must fire when thinking_blocks is empty; "
|
||||
f"got {len(plain_thought_parts)} plain thought part(s)"
|
||||
)
|
||||
assert plain_thought_parts[0]["text"] == reasoning_text
|
||||
|
|
@ -2781,3 +2781,95 @@ def test_gemini_server_side_tool_signature_not_duplicated_on_text():
|
|||
assert "thoughtSignature" not in text_part
|
||||
tool_call_part = next(p for p in parts if "toolCall" in p)
|
||||
assert tool_call_part["thoughtSignature"] == "server_side_signature"
|
||||
|
||||
|
||||
def test_gemini_no_duplicate_reasoning_parts_when_both_fields_present():
|
||||
"""
|
||||
Regression (#37973): when an assistant message has both reasoning_content and
|
||||
thinking_blocks, only one reasoning part must be emitted. Signed thinking_blocks
|
||||
take priority; reasoning_content is a fallback used only when thinking_blocks is absent.
|
||||
"""
|
||||
reasoning_text = "Two plus two is four."
|
||||
signature = "test-signature-abc123"
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "What is 2+2?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "4",
|
||||
"reasoning_content": reasoning_text,
|
||||
"thinking_blocks": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": reasoning_text,
|
||||
"signature": signature,
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Multiply by 3."},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
parts = contents[1]["parts"]
|
||||
|
||||
signed_parts = [p for p in parts if p.get("thoughtSignature") is not None]
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
|
||||
assert len(signed_parts) == 1, (
|
||||
f"Expected 1 signed reasoning part, got {len(signed_parts)}: {signed_parts}"
|
||||
)
|
||||
assert len(plain_thought_parts) == 0, (
|
||||
f"reasoning_content must not be emitted when thinking_blocks is present; "
|
||||
f"got {len(plain_thought_parts)} extra plain thought part(s)"
|
||||
)
|
||||
assert signed_parts[0]["thoughtSignature"] == signature
|
||||
|
||||
|
||||
def test_gemini_reasoning_content_fallback_when_no_thinking_blocks():
|
||||
"""
|
||||
When an assistant message has reasoning_content but no thinking_blocks,
|
||||
a plain thought part should be emitted as fallback.
|
||||
"""
|
||||
reasoning_text = "I reasoned about this carefully."
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hi there.",
|
||||
"reasoning_content": reasoning_text,
|
||||
},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
parts = contents[1]["parts"]
|
||||
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
assert len(plain_thought_parts) == 1
|
||||
assert plain_thought_parts[0]["text"] == reasoning_text
|
||||
|
||||
|
||||
def test_gemini_reasoning_content_fallback_when_thinking_blocks_empty():
|
||||
"""
|
||||
When thinking_blocks is non-null but empty, reasoning_content must still be emitted.
|
||||
"""
|
||||
reasoning_text = "I reasoned carefully."
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Hi.",
|
||||
"reasoning_content": reasoning_text,
|
||||
"thinking_blocks": [],
|
||||
},
|
||||
{"role": "user", "content": "How are you?"},
|
||||
]
|
||||
|
||||
contents = _gemini_convert_messages_with_history(messages=messages)
|
||||
parts = contents[1]["parts"]
|
||||
|
||||
plain_thought_parts = [p for p in parts if p.get("thought") is True]
|
||||
assert len(plain_thought_parts) == 1
|
||||
assert plain_thought_parts[0]["text"] == reasoning_text
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue