diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index a7d26cba80a..d95bc32fa6a 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -2302,20 +2302,14 @@ 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 also rejects a `thinking` block whose `thinking` text is empty or - whitespace-only ("each thinking block must contain thinking"), regardless of - signature. This shape reaches us when a caller replays a `thinking_blocks` - history item that originated from a non-Anthropic reasoning provider (e.g. an - OpenAI Responses-API turn with no summary text) through this Anthropic-shaped - request path (`/v1/chat/completions` -> anthropic/vertex_ai's claude models), - which is the same failure the Anthropic Responses-bridge adapter guards - against (see PR #36033) for its own separate content-block path. + 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 diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index a66af63b184..525ae2a680c 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -3722,14 +3722,7 @@ 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(): - """ - Anthropic rejects a `thinking` block whose `thinking` text is empty, even - when it carries a valid-looking signature, with: - 400 messages.N.content.M.thinking: each thinking block must contain thinking - This shape is reachable via cross-provider replay of a `thinking_blocks` - history item (see PR #36033), so `_is_unsignable_thinking_block()` must - also check the thinking text, not just the signature. - """ + """An empty-but-signed thinking block must still be dropped.""" from litellm.litellm_core_utils.prompt_templates.factory import ( anthropic_messages_pt, ) @@ -3762,10 +3755,7 @@ def test_anthropic_messages_pt_drops_empty_but_signed_thinking_block(): def test_anthropic_messages_pt_keeps_non_empty_signed_thinking_block(): - """ - Regression: a real, non-empty, signed thinking block must still pass - through unchanged. - """ + """Regression: a non-empty, signed thinking block passes through unchanged.""" from litellm.litellm_core_utils.prompt_templates.factory import ( anthropic_messages_pt, ) @@ -3800,11 +3790,7 @@ def test_anthropic_messages_pt_keeps_non_empty_signed_thinking_block(): def test_anthropic_messages_pt_keeps_redacted_thinking_block(): - """ - Regression: `redacted_thinking` blocks carry no signature and no plaintext - `thinking` field by design, and must always be kept regardless of the new - emptiness check (which only applies to `type == "thinking"` blocks). - """ + """Regression: redacted_thinking blocks are unaffected by the emptiness check.""" from litellm.litellm_core_utils.prompt_templates.factory import ( anthropic_messages_pt, ) @@ -3836,11 +3822,7 @@ def test_anthropic_messages_pt_keeps_redacted_thinking_block(): def test_anthropic_messages_pt_drops_unsigned_thinking_block(): - """ - Regression (pre-existing behaviour): a thinking block with no signature - (or an empty/null one) must still be dropped, independent of whether the - thinking text is populated. - """ + """Regression: an unsigned thinking block is still dropped, regardless of text.""" from litellm.litellm_core_utils.prompt_templates.factory import ( anthropic_messages_pt, ) @@ -3873,24 +3855,7 @@ def test_anthropic_messages_pt_drops_unsigned_thinking_block(): def test_is_unsignable_thinking_block_treats_whitespace_only_as_empty(): - """ - Edge case: a `thinking` field that is present but whitespace-only (e.g. - a single trailing newline forwarded from another provider's empty - reasoning summary) is functionally empty and Anthropic's API will still - reject it with "each thinking block must contain thinking". We treat it - the same as a fully empty string and drop the block. - - Note the sibling check ~30 lines below this function's definition - (the "don't pass empty text blocks" comment) uses a bare - `len(thinking_block) > 0`, which by itself would treat whitespace-only - text as non-empty. That sibling check is always combined with - `not _is_unsignable_thinking_block(m)` in an `and`, so this function's - stricter whitespace-aware check is still the one that decides whether a - whitespace-only block survives — the two checks don't disagree in - practice, but this function is intentionally the stricter of the two - since it is also called standalone (via `_drop_unsignable_thinking_blocks`) - without that extra `len(...) > 0` guard. - """ + """Whitespace-only `thinking` text is treated as empty and dropped.""" from litellm.litellm_core_utils.prompt_templates.factory import ( _is_unsignable_thinking_block, )