From f74c8533dc64a214f92c283744f5135b12a6753c Mon Sep 17 00:00:00 2001 From: Charlie Freeman <243496084+cf3901646@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:47:24 +0800 Subject: [PATCH] fix(streaming): preserve reasoning_details chunks in stream wrapper and proxy serializer (#48401) --- .../litellm_core_utils/streaming_handler.py | 1 + litellm/proxy/proxy_server.py | 2 + .../test_reasoning_details_streaming.py | 75 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 tests/unit/litellm_core_utils/test_reasoning_details_streaming.py diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 2df470edc59..b4d7a52aeb5 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -1006,6 +1006,7 @@ class CustomStreamWrapper: and model_response.choices[0].delta.role is not None ) or (getattr(model_response.choices[0].delta, "reasoning_items", None) is not None) + or bool(getattr(model_response.choices[0].delta, "reasoning_details", None)) ): return True else: diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index df948b69a67..c88a900f9cf 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10093,6 +10093,8 @@ def _fast_serialize_simple_model_response_stream( "thinking_blocks", "provider_specific_fields", "refusal", + "reasoning_items", + "reasoning_details", ) if any(getattr(delta, field, None) is not None for field in unsupported_delta_fields): return None diff --git a/tests/unit/litellm_core_utils/test_reasoning_details_streaming.py b/tests/unit/litellm_core_utils/test_reasoning_details_streaming.py new file mode 100644 index 00000000000..97c6a994bd9 --- /dev/null +++ b/tests/unit/litellm_core_utils/test_reasoning_details_streaming.py @@ -0,0 +1,75 @@ +from unittest.mock import MagicMock +from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper +from litellm.types.utils import ModelResponseStream + + +def test_is_chunk_non_empty_with_reasoning_details(): + """ + Unit test that chunks carrying only delta.reasoning_details (e.g., OpenRouter + Anthropic signatures or encrypted reasoning blobs) are treated as non-empty. + + Regression test for https://github.com/BerriAI/litellm/issues/48401 + """ + wrapper = CustomStreamWrapper( + completion_stream=iter([]), + model="anthropic/claude-sonnet-4.5", + custom_llm_provider="openrouter", + logging_obj=MagicMock(), + ) + + chunk_with_signature = { + "id": "gen-sig-1", + "object": "chat.completion.chunk", + "created": 1741037890, + "model": "anthropic/claude-sonnet-4.5", + "choices": [ + { + "index": 0, + "delta": { + "content": None, + "reasoning_details": [ + { + "type": "reasoning.text", + "signature": "sig-abc", + "index": 0, + "format": "anthropic-claude-v1", + } + ], + }, + "finish_reason": None, + } + ], + } + assert ( + wrapper.is_chunk_non_empty( + completion_obj={}, + model_response=ModelResponseStream(**chunk_with_signature), + response_obj={}, + ) + is True + ) + + chunk_with_empty_reasoning_details = { + "id": "gen-sig-2", + "object": "chat.completion.chunk", + "created": 1741037890, + "model": "anthropic/claude-sonnet-4.5", + "choices": [ + { + "index": 0, + "delta": { + "content": None, + "reasoning_details": [], + }, + "finish_reason": None, + } + ], + } + assert ( + wrapper.is_chunk_non_empty( + completion_obj={}, + model_response=ModelResponseStream(**chunk_with_empty_reasoning_details), + response_obj={}, + ) + is False + )