mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
fix: handle list-content messages in thinking block interleaving
When assistant content is already a list containing thinking blocks inline (not str/None), SEQUENTIAL MODE was still prepending all thinking_blocks from provider_specific_fields, causing duplication and breaking Anthropic's position-dependent signature verification. Now detects if the content list already has thinking blocks and skips the extend(thinking_blocks) to preserve the original interleaved order. Addresses the correctness gap identified by Greptile review where list-content messages bypass INTERLEAVED MODE. Fixes: https://github.com/BerriAI/litellm/issues/23047
This commit is contained in:
parent
95ecf9f46d
commit
0680a97409
2 changed files with 115 additions and 3 deletions
|
|
@ -2614,13 +2614,28 @@ def anthropic_messages_pt( # noqa: PLR0915
|
|||
else:
|
||||
# SEQUENTIAL MODE: No server tool calls, or no thinking blocks,
|
||||
# or content is a list. Use the original sequential approach.
|
||||
|
||||
# When content is a list, check if it already contains thinking
|
||||
# blocks inline. If so, skip prepending thinking_blocks to avoid
|
||||
# duplication and preserve the original interleaved order.
|
||||
# Fixes the gap where list-content messages bypass INTERLEAVED
|
||||
# MODE and still get thinking blocks prepended out of order.
|
||||
_content_is_list = "content" in assistant_content_block and isinstance(
|
||||
assistant_content_block["content"], list
|
||||
)
|
||||
_list_has_thinking = False
|
||||
if _content_is_list:
|
||||
for _item in assistant_content_block["content"]:
|
||||
if isinstance(_item, dict) and _item.get("type") == "thinking":
|
||||
_list_has_thinking = True
|
||||
break
|
||||
|
||||
if (
|
||||
thinking_blocks is not None
|
||||
and not _list_has_thinking
|
||||
): # IMPORTANT: ADD THIS FIRST, ELSE ANTHROPIC WILL RAISE AN ERROR
|
||||
assistant_content.extend(thinking_blocks)
|
||||
if "content" in assistant_content_block and isinstance(
|
||||
assistant_content_block["content"], list
|
||||
):
|
||||
if _content_is_list:
|
||||
for m in assistant_content_block["content"]:
|
||||
# handle thinking blocks
|
||||
thinking_block = cast(str, m.get("thinking", ""))
|
||||
|
|
|
|||
|
|
@ -1847,3 +1847,100 @@ def test_anthropic_messages_pt_interleave_more_thinking_than_tool_groups():
|
|||
"thinking", # extra - before text
|
||||
"text",
|
||||
], f"Expected order but got: {types}"
|
||||
|
||||
|
||||
def test_anthropic_messages_pt_list_content_with_thinking_preserves_order():
|
||||
"""
|
||||
Test that when assistant content is already a list containing interleaved
|
||||
thinking blocks and server tool blocks, the thinking_blocks from
|
||||
provider_specific_fields are NOT duplicated/prepended.
|
||||
|
||||
This covers the gap identified by Greptile where list-content messages
|
||||
bypass INTERLEAVED MODE and fall into SEQUENTIAL MODE, which previously
|
||||
would prepend all thinking_blocks again, causing duplication and
|
||||
breaking Anthropic's position-dependent signature verification.
|
||||
|
||||
Fixes: https://github.com/BerriAI/litellm/issues/23047
|
||||
"""
|
||||
messages = [
|
||||
{"role": "user", "content": "Search for AI news"},
|
||||
{
|
||||
"role": "assistant",
|
||||
# Content is already a list with interleaved thinking + server tool blocks
|
||||
"content": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Let me search for AI news.",
|
||||
"signature": "sig_1",
|
||||
},
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"id": "srvtoolu_01SEARCH1",
|
||||
"name": "web_search",
|
||||
"input": {"query": "AI news"},
|
||||
},
|
||||
{
|
||||
"type": "web_search_tool_result",
|
||||
"tool_use_id": "srvtoolu_01SEARCH1",
|
||||
"content": [
|
||||
{
|
||||
"type": "web_search_result",
|
||||
"url": "https://example.com",
|
||||
"title": "AI News",
|
||||
"snippet": "Latest AI news",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Now let me summarize.",
|
||||
"signature": "sig_2",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Here is the AI news summary.",
|
||||
},
|
||||
],
|
||||
# thinking_blocks also present in provider_specific_fields
|
||||
"thinking_blocks": [
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Let me search for AI news.",
|
||||
"signature": "sig_1",
|
||||
},
|
||||
{
|
||||
"type": "thinking",
|
||||
"thinking": "Now let me summarize.",
|
||||
"signature": "sig_2",
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Tell me more"},
|
||||
]
|
||||
|
||||
result = anthropic_messages_pt(
|
||||
messages, model="claude-sonnet-4-5", llm_provider="anthropic"
|
||||
)
|
||||
|
||||
assistant_msg = next(m for m in result if m["role"] == "assistant")
|
||||
content = assistant_msg["content"]
|
||||
types = [c.get("type") for c in content]
|
||||
|
||||
# The list content already has the correct interleaved order.
|
||||
# thinking_blocks should NOT be prepended again (which would cause
|
||||
# duplication and break signature verification).
|
||||
assert types == [
|
||||
"thinking",
|
||||
"server_tool_use",
|
||||
"web_search_tool_result",
|
||||
"thinking",
|
||||
"text",
|
||||
], f"Expected preserved list order without duplicate thinking blocks, but got: {types}"
|
||||
|
||||
# Verify no duplicate thinking blocks
|
||||
thinking_count = sum(1 for t in types if t == "thinking")
|
||||
assert thinking_count == 2, f"Expected 2 thinking blocks, got {thinking_count} (duplication detected)"
|
||||
|
||||
# Verify signatures preserved in correct positions
|
||||
assert content[0]["signature"] == "sig_1"
|
||||
assert content[3]["signature"] == "sig_2"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue