mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
Merge dfba491fa4 into c2c2a623c0
This commit is contained in:
commit
a80fd6abde
2 changed files with 158 additions and 6 deletions
|
|
@ -2302,18 +2302,24 @@ 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, and
|
||||
so is a block whose signature or data carries another provider's encrypted
|
||||
reasoning. A `redacted_thinking` block Anthropic minted is 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, and so is a block whose signature or data carries
|
||||
another provider's encrypted reasoning. 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 is_encrypted_reasoning_block(block):
|
||||
return True
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -3721,3 +3721,149 @@ def test_convert_to_anthropic_tool_invoke_keeps_paired_server_tool_use():
|
|||
},
|
||||
server_result,
|
||||
]
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue