This commit is contained in:
Jeremy Schoemaker 2026-08-27 20:40:38 +00:00 committed by GitHub
commit 37b5ed9ecb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 159 additions and 5 deletions

View file

@ -2288,15 +2288,21 @@ def sanitize_messages_for_tool_calling(
def _is_unsignable_thinking_block(block: object) -> bool:
"""A `thinking` block that Anthropic cannot accept on input.
Anthropic verifies the thinking signature cryptographically, so a block whose
signature is null, empty, or missing (e.g. from an open-source reasoning model)
is rejected with a 400 and must be dropped rather than blanked or repaired.
`redacted_thinking` blocks carry no signature and are always kept.
Anthropic verifies the signature cryptographically, so a block with a null,
empty, or missing signature (e.g. from an open-source reasoning model) is
rejected with a 400. It also rejects a `thinking` block whose text is empty
or whitespace-only ("each thinking block must contain thinking"), regardless
of signature, e.g. when a `thinking_blocks` history item from a non-Anthropic
reasoning provider is replayed through this path. `redacted_thinking` blocks
carry no signature and are always kept.
"""
if not isinstance(block, dict) or block.get("type") != "thinking":
return False
signature: Final = block.get("signature")
return not (isinstance(signature, str) and len(signature) > 0)
if not (isinstance(signature, str) and len(signature) > 0):
return True
thinking_text: Final = block.get("thinking")
return not (isinstance(thinking_text, str) and len(thinking_text.strip()) > 0)
def _drop_unsignable_thinking_blocks(

View file

@ -3578,3 +3578,151 @@ async def test_bedrock_converse_pdf_only_user_message_gets_text_block_async():
assert len(result) == 1
assert any("document" in block for block in result[0]["content"])
assert _text_blocks(result[0]) == [BEDROCK_DOCUMENT_PLACEHOLDER_TEXT]
def test_anthropic_messages_pt_drops_empty_but_signed_thinking_block():
"""An empty-but-signed thinking block must still be dropped."""
from litellm.litellm_core_utils.prompt_templates.factory import (
anthropic_messages_pt,
)
messages = [
{"role": "user", "content": "What's 2+2?"},
{
"role": "assistant",
"content": "4",
"thinking_blocks": [
{
"type": "thinking",
"thinking": "",
"signature": "sig_abc123_looks_valid",
}
],
},
]
result = anthropic_messages_pt(
messages=messages,
model="claude-sonnet-4-5-20250929",
llm_provider="anthropic",
)
assistant_msg = result[1]
assert isinstance(assistant_msg["content"], list)
content_types = [block.get("type") for block in assistant_msg["content"]]
assert "thinking" not in content_types, "empty-text thinking block must be dropped even though it has a signature"
def test_anthropic_messages_pt_keeps_non_empty_signed_thinking_block():
"""Regression: a non-empty, signed thinking block passes through unchanged."""
from litellm.litellm_core_utils.prompt_templates.factory import (
anthropic_messages_pt,
)
messages = [
{"role": "user", "content": "What's 2+2?"},
{
"role": "assistant",
"content": "4",
"thinking_blocks": [
{
"type": "thinking",
"thinking": "Let me add these numbers together.",
"signature": "sig_abc123_looks_valid",
}
],
},
]
result = anthropic_messages_pt(
messages=messages,
model="claude-sonnet-4-5-20250929",
llm_provider="anthropic",
)
assistant_msg = result[1]
assert isinstance(assistant_msg["content"], list)
thinking_block = next((b for b in assistant_msg["content"] if b.get("type") == "thinking"), None)
assert thinking_block is not None, "non-empty signed thinking block must be kept"
assert thinking_block["thinking"] == "Let me add these numbers together."
assert thinking_block["signature"] == "sig_abc123_looks_valid"
def test_anthropic_messages_pt_keeps_redacted_thinking_block():
"""Regression: redacted_thinking blocks are unaffected by the emptiness check."""
from litellm.litellm_core_utils.prompt_templates.factory import (
anthropic_messages_pt,
)
messages = [
{"role": "user", "content": "What's 2+2?"},
{
"role": "assistant",
"content": "4",
"thinking_blocks": [
{
"type": "redacted_thinking",
"data": "encrypted_opaque_blob",
}
],
},
]
result = anthropic_messages_pt(
messages=messages,
model="claude-sonnet-4-5-20250929",
llm_provider="anthropic",
)
assistant_msg = result[1]
assert isinstance(assistant_msg["content"], list)
content_types = [block.get("type") for block in assistant_msg["content"]]
assert "redacted_thinking" in content_types, "redacted_thinking blocks must always be kept"
def test_anthropic_messages_pt_drops_unsigned_thinking_block():
"""Regression: an unsigned thinking block is still dropped, regardless of text."""
from litellm.litellm_core_utils.prompt_templates.factory import (
anthropic_messages_pt,
)
messages = [
{"role": "user", "content": "What's 2+2?"},
{
"role": "assistant",
"content": "4",
"thinking_blocks": [
{
"type": "thinking",
"thinking": "Let me add these numbers together.",
"signature": "",
}
],
},
]
result = anthropic_messages_pt(
messages=messages,
model="claude-sonnet-4-5-20250929",
llm_provider="anthropic",
)
assistant_msg = result[1]
assert isinstance(assistant_msg["content"], list)
content_types = [block.get("type") for block in assistant_msg["content"]]
assert "thinking" not in content_types, "unsigned thinking block must still be dropped"
def test_is_unsignable_thinking_block_treats_whitespace_only_as_empty():
"""Whitespace-only `thinking` text is treated as empty and dropped."""
from litellm.litellm_core_utils.prompt_templates.factory import (
_is_unsignable_thinking_block,
)
whitespace_only_block = {
"type": "thinking",
"thinking": " \n\t ",
"signature": "sig_abc123_looks_valid",
}
assert _is_unsignable_thinking_block(whitespace_only_block) is True