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:
Chesars 2026-03-10 12:48:30 -03:00
parent 95ecf9f46d
commit 0680a97409
2 changed files with 115 additions and 3 deletions

View file

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

View file

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