From 35bf93a7acc0815383e9669b6ffa26198c0e4266 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:14:53 -0700 Subject: [PATCH 1/2] fix(completions): stream an empty text instead of None on the finish chunk The shared text completion stream wrapper copied the chat delta's content into the text choice, so the finish chunk (and the trailing usage chunk) carried text None where OpenAI sends an empty string and the OpenAI SDK types the field as str. A client joining choice.text across the stream raised TypeError on that chunk. --- litellm/utils.py | 2 +- .../providers/test_ollama_prompt_tools_wire.py | 2 +- tests/unit/test_utils.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/litellm/utils.py b/litellm/utils.py index 9ded4ff1131..07135168059 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -7395,7 +7395,7 @@ class TextCompletionStreamWrapper: if isinstance(chunk, Choices): # chunk should always be of type StreamingChoices raise Exception delta: Final = chunk["choices"][0]["delta"] - text_choices["text"] = delta["content"] + text_choices["text"] = delta["content"] or "" text_choices["reasoning_content"] = delta.get("reasoning_content") text_choices["index"] = chunk["choices"][0]["index"] text_choices["finish_reason"] = chunk["choices"][0]["finish_reason"] diff --git a/tests/integration/providers/test_ollama_prompt_tools_wire.py b/tests/integration/providers/test_ollama_prompt_tools_wire.py index f3aa18cb366..4162837f348 100644 --- a/tests/integration/providers/test_ollama_prompt_tools_wire.py +++ b/tests/integration/providers/test_ollama_prompt_tools_wire.py @@ -843,7 +843,7 @@ def _thinking_reply(thinking: str) -> Reply: def _completion_texts(chunks: Sequence[Completion]) -> Iterator[str]: for chunk in chunks: - yield from (choice.text or "" for choice in chunk.choices) + yield from (choice.text for choice in chunk.choices) def _raw_stream_frames(gateway: Gateway, path: str, body: dict[str, JsonValue]) -> tuple[str, ...]: diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8a7c9825f0a..7984c0162c3 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -2200,6 +2200,23 @@ def test_reasoning_content_preserved_in_text_completion_wrapper(): assert choice["reasoning_content"] == "Here's my chain of thought..." +def test_text_completion_wrapper_sends_empty_text_on_finish_chunk(): + finish_chunk = ModelResponseStream( + id="test-id", + created=1234567890, + model="test-model", + object="chat.completion.chunk", + choices=[StreamingChoices(finish_reason="stop", index=0, delta=Delta(content=None, role=None))], + ) + wrapper = TextCompletionStreamWrapper(completion_stream=None, model="test-model", stream_options=None) + + choice = wrapper.convert_to_text_completion_object(finish_chunk)["choices"][0] + + assert choice["text"] == "" + assert choice["finish_reason"] == "stop" + assert choice["index"] == 0 + + def test_anthropic_claude_4_invoke_chat_provider_config(): """Test that the Anthropic Claude 4 Invoke chat provider config is correct.""" from litellm.llms.bedrock.chat.invoke_transformations.anthropic_claude3_transformation import ( From af91726ad98a2f169f15356522579a56d5d59af6 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:26:11 -0700 Subject: [PATCH 2/2] test(completions): annotate the finish chunk test's locals with Final --- tests/unit/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7984c0162c3..6c8ba5430c2 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -2201,16 +2201,16 @@ def test_reasoning_content_preserved_in_text_completion_wrapper(): def test_text_completion_wrapper_sends_empty_text_on_finish_chunk(): - finish_chunk = ModelResponseStream( + finish_chunk: Final = ModelResponseStream( id="test-id", created=1234567890, model="test-model", object="chat.completion.chunk", choices=[StreamingChoices(finish_reason="stop", index=0, delta=Delta(content=None, role=None))], ) - wrapper = TextCompletionStreamWrapper(completion_stream=None, model="test-model", stream_options=None) + wrapper: Final = TextCompletionStreamWrapper(completion_stream=None, model="test-model", stream_options=None) - choice = wrapper.convert_to_text_completion_object(finish_chunk)["choices"][0] + choice: Final = wrapper.convert_to_text_completion_object(finish_chunk)["choices"][0] assert choice["text"] == "" assert choice["finish_reason"] == "stop"