From e1a2bfb63abcb878446540bc1180c07e3496fb34 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Thu, 7 Aug 2025 09:49:33 -0600 Subject: [PATCH] Fix Ollama GPT-OSS streaming with 'thinking' field - Handle chunks containing 'thinking' field with empty 'response' - Treat these as intermediate chunks that don't contain user content - Add comprehensive tests for chunk parsing scenarios - Resolves APIConnectionError for GPT-OSS model streaming Fixes #13340 --- .../llms/ollama/completion/transformation.py | 9 +++ .../test_ollama_completion_transformation.py | 77 ++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py index aa1da616d89..cec27b02d6d 100644 --- a/litellm/llms/ollama/completion/transformation.py +++ b/litellm/llms/ollama/completion/transformation.py @@ -459,6 +459,15 @@ class OllamaTextCompletionResponseIterator(BaseModelResponseIterator): finish_reason="stop", usage=None, ) + elif "thinking" in chunk and not chunk["response"]: + # Handle GPT-OSS models that include 'thinking' field with empty response + # These are intermediate chunks that don't contain user-facing content + return GenericStreamingChunk( + text="", + is_finished=is_finished, + finish_reason=None, + usage=None, + ) else: raise Exception(f"Unable to parse ollama chunk - {chunk}") except Exception as e: diff --git a/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py b/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py index f0b5c00d017..241558cf3e2 100644 --- a/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py +++ b/tests/test_litellm/llms/ollama/test_ollama_completion_transformation.py @@ -10,7 +10,10 @@ sys.path.insert( 0, os.path.abspath("../../../../..") ) # Adds the parent directory to the system path -from litellm.llms.ollama.completion.transformation import OllamaConfig +from litellm.llms.ollama.completion.transformation import ( + OllamaConfig, + OllamaTextCompletionResponseIterator, +) from litellm.types.utils import Message, ModelResponse @@ -155,3 +158,75 @@ class TestOllamaConfig: assert result.choices[0]["message"].content == expected_content assert result.choices[0]["finish_reason"] == "stop" # No usage assertions here as we don't need to test them in every case + + +class TestOllamaTextCompletionResponseIterator: + def test_chunk_parser_with_thinking_field(self): + """Test that chunks with 'thinking' field and empty 'response' are handled correctly.""" + iterator = OllamaTextCompletionResponseIterator( + streaming_response=iter([]), sync_stream=True, json_mode=False + ) + + # Test chunk with thinking field - this is the problematic case from the issue + chunk_with_thinking = { + "model": "gpt-oss:20b", + "created_at": "2025-08-06T14:34:31.5276077Z", + "response": "", + "thinking": "User", + "done": False, + } + + result = iterator.chunk_parser(chunk_with_thinking) + + # Should return empty text and not be finished + assert result["text"] == "" + assert result["is_finished"] is False + assert result["finish_reason"] is None + assert result["usage"] is None + + def test_chunk_parser_normal_response(self): + """Test that normal response chunks still work.""" + iterator = OllamaTextCompletionResponseIterator( + streaming_response=iter([]), sync_stream=True, json_mode=False + ) + + # Test normal chunk with response + normal_chunk = { + "model": "llama2", + "created_at": "2025-08-06T14:34:31.5276077Z", + "response": "Hello world", + "done": False, + } + + result = iterator.chunk_parser(normal_chunk) + + assert result["text"] == "Hello world" + assert result["is_finished"] is False + assert result["finish_reason"] == "stop" + assert result["usage"] is None + + def test_chunk_parser_done_chunk(self): + """Test that done chunks work correctly.""" + iterator = OllamaTextCompletionResponseIterator( + streaming_response=iter([]), sync_stream=True, json_mode=False + ) + + # Test done chunk + done_chunk = { + "model": "llama2", + "created_at": "2025-08-06T14:34:31.5276077Z", + "response": "", + "done": True, + "prompt_eval_count": 10, + "eval_count": 5, + } + + result = iterator.chunk_parser(done_chunk) + + assert result["text"] == "" + assert result["is_finished"] is True + assert result["finish_reason"] == "stop" + assert result["usage"] is not None + assert result["usage"]["prompt_tokens"] == 10 + assert result["usage"]["completion_tokens"] == 5 + assert result["usage"]["total_tokens"] == 15