From 0d7217ce585d8b37c4849a75b9c5eb2392d59b34 Mon Sep 17 00:00:00 2001 From: jesco-absolut Date: Fri, 31 Jul 2026 23:20:45 -0400 Subject: [PATCH 1/2] fix(anthropic): preserve reasoning fallback --- .../adapters/transformation.py | 34 ++++++++--------- .../test_streaming_iterator_first_delta.py | 37 +++++++++++++++++++ 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 86c9c1db481..4c6df5002dc 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1407,8 +1407,8 @@ class LiteLLMAnthropicMessagesAdapter: return "tool_use", cast("ContentBlockContentBlockDict", tool_block) elif choice.delta.content is not None and len(choice.delta.content) > 0: return "text", TextBlock(type="text", text="") - elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"): - thinking_blocks = choice.delta.thinking_blocks or [] + elif isinstance(choice, StreamingChoices): + thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or [] if len(thinking_blocks) > 0: thinking_block = thinking_blocks[0] if thinking_block["type"] == "thinking": @@ -1421,13 +1421,8 @@ class LiteLLMAnthropicMessagesAdapter: return "thinking", ChatCompletionThinkingBlock( type="thinking", thinking=thinking, signature=signature ) - # OpenAI-compatible reasoning backends (e.g. vLLM/SGLang reasoning - # parsers) populate ``reasoning_content`` without ``thinking_blocks``. - # ``Delta`` deletes the ``thinking_blocks`` attribute when unset, so the - # branch above is skipped entirely; open a ``thinking`` block here so the - # matching ``thinking_delta`` stream is not emitted into a text block. - elif isinstance(choice, StreamingChoices) and getattr(choice.delta, "reasoning_content", None): - return "thinking", ChatCompletionThinkingBlock(type="thinking", thinking="", signature="") + if getattr(choice.delta, "reasoning_content", None): + return "thinking", ChatCompletionThinkingBlock(type="thinking", thinking="", signature="") return "text", TextBlock(type="text", text="") @@ -1454,8 +1449,10 @@ class LiteLLMAnthropicMessagesAdapter: for tool in choice.delta.tool_calls: if tool.function is not None and tool.function.arguments is not None: partial_json = (partial_json or "") + tool.function.arguments - elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"): - thinking_blocks = choice.delta.thinking_blocks or [] + elif isinstance(choice, StreamingChoices): + thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or [] + choice_reasoning_content = "" + choice_reasoning_signature = "" if len(thinking_blocks) > 0: for thinking_block in thinking_blocks: if thinking_block["type"] == "thinking": @@ -1465,13 +1462,14 @@ class LiteLLMAnthropicMessagesAdapter: assert isinstance(thinking, str) assert isinstance(signature, str) - reasoning_content += thinking - reasoning_signature += signature - # Handle reasoning_content when thinking_blocks is not present - # This handles providers like OpenRouter that return reasoning_content - elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "reasoning_content"): - if choice.delta.reasoning_content is not None: - reasoning_content += choice.delta.reasoning_content + choice_reasoning_content += thinking + choice_reasoning_signature += signature + if choice_reasoning_content or choice_reasoning_signature: + reasoning_content += choice_reasoning_content + reasoning_signature += choice_reasoning_signature + elif hasattr(choice.delta, "reasoning_content"): + if choice.delta.reasoning_content is not None: + reasoning_content += choice.delta.reasoning_content if partial_json is not None: return "input_json_delta", ContentJsonBlockDelta(type="input_json_delta", partial_json=partial_json) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py index 2eb8e077320..5a893fb3b76 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py @@ -576,6 +576,43 @@ def test_reasoning_content_first_stream_opens_thinking_block_at_index_zero_sync( _assert_thinking_first_block_opens_at_index_zero(_drain_sync(wrapper)) +@pytest.mark.parametrize( + "thinking_blocks", + [ + pytest.param([], id="empty"), + pytest.param( + [{"type": "redacted_thinking", "data": "redacted"}], + id="redacted-only", + ), + ], +) +def test_reasoning_content_falls_back_without_usable_thinking_blocks_sync( + thinking_blocks, +): + chunks = [ + _make_chunk( + Delta( + content=None, + reasoning_content="fallback thought", + thinking_blocks=thinking_blocks, + ) + ), + _make_chunk(Delta(content=None), finish_reason="stop"), + ] + wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="claude-x") + events = _drain_sync(wrapper) + + starts = [ + (event["index"], event["content_block"]["type"]) + for event in events + if event.get("type") == "content_block_start" + ] + assert starts == [(0, "thinking")] + assert _thinking_deltas(events) == ["fallback thought"] + assert _text_deltas(events) == [] + _assert_deltas_match_their_block_type(events) + + def _blank_lead_chunks() -> List[MagicMock]: return [ _make_chunk(Delta(content=None)), From e3fb8b1f1f5189cd08819d6edcf2aba8818b8b3a Mon Sep 17 00:00:00 2001 From: jesco-absolut Date: Sat, 1 Aug 2026 16:54:28 -0400 Subject: [PATCH 2/2] fix(anthropic): reduce reasoning fallback complexity --- .../adapters/transformation.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 4c6df5002dc..4c78a6c3876 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1426,6 +1426,28 @@ class LiteLLMAnthropicMessagesAdapter: return "text", TextBlock(type="text", text="") + @staticmethod + def _streaming_reasoning_fields(choice: StreamingChoices) -> tuple[str, str]: + reasoning_content = "" + reasoning_signature = "" + thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or [] + for thinking_block in thinking_blocks: + if thinking_block["type"] == "thinking": + thinking = thinking_block.get("thinking") or "" + signature = thinking_block.get("signature") or "" + + assert isinstance(thinking, str) + assert isinstance(signature, str) + + reasoning_content += thinking + reasoning_signature += signature + + if reasoning_content or reasoning_signature: + return reasoning_content, reasoning_signature + + fallback = getattr(choice.delta, "reasoning_content", None) + return fallback or "", "" + def _translate_streaming_openai_chunk_to_anthropic( self, choices: List[Union[OpenAIStreamingChoice, StreamingChoices]] ) -> Tuple[ @@ -1450,26 +1472,9 @@ class LiteLLMAnthropicMessagesAdapter: if tool.function is not None and tool.function.arguments is not None: partial_json = (partial_json or "") + tool.function.arguments elif isinstance(choice, StreamingChoices): - thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or [] - choice_reasoning_content = "" - choice_reasoning_signature = "" - if len(thinking_blocks) > 0: - for thinking_block in thinking_blocks: - if thinking_block["type"] == "thinking": - thinking = thinking_block.get("thinking") or "" - signature = thinking_block.get("signature") or "" - - assert isinstance(thinking, str) - assert isinstance(signature, str) - - choice_reasoning_content += thinking - choice_reasoning_signature += signature - if choice_reasoning_content or choice_reasoning_signature: - reasoning_content += choice_reasoning_content - reasoning_signature += choice_reasoning_signature - elif hasattr(choice.delta, "reasoning_content"): - if choice.delta.reasoning_content is not None: - reasoning_content += choice.delta.reasoning_content + choice_reasoning_content, choice_reasoning_signature = self._streaming_reasoning_fields(choice) + reasoning_content += choice_reasoning_content + reasoning_signature += choice_reasoning_signature if partial_json is not None: return "input_json_delta", ContentJsonBlockDelta(type="input_json_delta", partial_json=partial_json)