From 9678c723b094d10a99ab02e1301404a1095effaa Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Wed, 18 Feb 2026 16:47:05 +0530 Subject: [PATCH] Add reasoning' field to 'reasoning_content' field in delta --- .../llms/openai/chat/gpt_transformation.py | 24 +++++- .../chat/test_openai_gpt_transformation.py | 77 ++++++++++++++++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py index 5b9840d95b0..59f52e2b81c 100644 --- a/litellm/llms/openai/chat/gpt_transformation.py +++ b/litellm/llms/openai/chat/gpt_transformation.py @@ -770,14 +770,36 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig): class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator): + def _map_reasoning_to_reasoning_content(self, choices: list) -> list: + """ + Map 'reasoning' field to 'reasoning_content' field in delta. + + Some OpenAI-compatible providers (e.g., GLM-5, hosted_vllm) return + delta.reasoning, but LiteLLM expects delta.reasoning_content. + + Args: + choices: List of choice objects from the streaming chunk + + Returns: + List of choices with reasoning field mapped to reasoning_content + """ + for choice in choices: + delta = choice.get("delta", {}) + if "reasoning" in delta: + delta["reasoning_content"] = delta.pop("reasoning") + return choices + def chunk_parser(self, chunk: dict) -> ModelResponseStream: try: + choices = chunk.get("choices", []) + choices = self._map_reasoning_to_reasoning_content(choices) + kwargs = { "id": chunk["id"], "object": "chat.completion.chunk", "created": chunk.get("created"), "model": chunk.get("model"), - "choices": chunk.get("choices", []), + "choices": choices, } if "usage" in chunk and chunk["usage"] is not None: kwargs["usage"] = chunk["usage"] diff --git a/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py b/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py index e6ab199168d..37959f74086 100644 --- a/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py +++ b/tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py @@ -10,8 +10,8 @@ import pytest sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.openai.chat.gpt_transformation import ( - OpenAIGPTConfig, OpenAIChatCompletionStreamingHandler, + OpenAIGPTConfig, ) @@ -204,6 +204,81 @@ class TestOpenAIChatCompletionStreamingHandler: assert result.choices[0].delta.content == "Hello" assert not hasattr(result, "usage") or result.usage is None + def test_chunk_parser_maps_reasoning_to_reasoning_content(self): + """ + Test that chunk_parser maps 'reasoning' field to 'reasoning_content'. + + Some OpenAI-compatible providers (e.g., GLM-5, hosted_vllm) return + delta.reasoning, but LiteLLM expects delta.reasoning_content. + + Regression test for: Streaming responses with delta.reasoning field + coming back empty when using openai/ or hosted_vllm/ providers. + """ + handler = OpenAIChatCompletionStreamingHandler( + streaming_response=None, sync_stream=True + ) + + # Simulate a chunk with reasoning field (as returned by GLM-5) + chunk = { + "id": "chatcmpl-8e3d624de9b12528", + "object": "chat.completion.chunk", + "created": 1771411455, + "model": "glm-5", + "choices": [ + { + "index": 0, + "delta": { + "reasoning": "The capital of France", + "role": None, + }, + "finish_reason": None, + } + ], + } + + # Parse the chunk + parsed_chunk = handler.chunk_parser(chunk) + + # Verify that reasoning was mapped to reasoning_content + assert parsed_chunk.choices[0].delta.reasoning_content == "The capital of France" + # Verify that the original 'reasoning' field was removed + assert not hasattr(parsed_chunk.choices[0].delta, "reasoning") + + def test_chunk_parser_reasoning_field_not_present(self): + """ + Test that chunks without reasoning field still work correctly. + """ + handler = OpenAIChatCompletionStreamingHandler( + streaming_response=None, sync_stream=True + ) + + # Simulate a chunk without reasoning field + chunk = { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 1769511767, + "model": "gpt-4o", + "choices": [ + { + "delta": { + "content": "Regular content", + "role": "assistant", + }, + "finish_reason": None, + "index": 0, + } + ], + } + + # Parse the chunk + parsed_chunk = handler.chunk_parser(chunk) + + # Verify that content is present + assert parsed_chunk.choices[0].delta.content == "Regular content" + assert parsed_chunk.choices[0].delta.role == "assistant" + # Verify that reasoning_content is not set (it should be deleted by Delta.__init__) + assert not hasattr(parsed_chunk.choices[0].delta, "reasoning_content") + class TestPromptCacheKeyIntegration: """Tests for prompt_cache_key support"""