diff --git a/CHANGELOG.md b/CHANGELOG.md index 040d34c8..fe3eccb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +* Streaming from `ChatOpenAICompletions()` (and the providers built on it, such as `ChatOpenRouter()` and `ChatGroq()`) no longer drops a chunk's text when the same chunk carries an empty `reasoning` or `reasoning_content` field, as some OpenAI-compatible servers send. * `ChatOllama()` can now be created without a `model`, so `ChatOllama().list_models()` works. Sending a message without a model still raises an error that lists the available models. (#166) * `ChatGoogle()` now reports `finish_reason == "content_filter"` for Gemini's `IMAGE_SAFETY`, `IMAGE_PROHIBITED_CONTENT`, `IMAGE_RECITATION` and `LANGUAGE` finish reasons, instead of passing the raw Gemini string through, and reports `"tool_use"` (rather than `"success"`) when a turn ends with a tool request, as `ChatOpenAI()` does. (#473) * The error raised for a tool request with invalid JSON arguments from `ChatOpenAI()` and `ChatOpenAICompletions()` (and the providers built on them) now shows the offending arguments, instead of the literal text `{args}`. diff --git a/chatlas/_provider_openai_completions.py b/chatlas/_provider_openai_completions.py index 5051bcfc..334f8a32 100644 --- a/chatlas/_provider_openai_completions.py +++ b/chatlas/_provider_openai_completions.py @@ -320,7 +320,7 @@ def stream_content(self, chunk, completion, turns=()) -> list[Content]: reasoning = getattr(delta, "reasoning", None) or getattr( delta, "reasoning_content", None ) - if reasoning is not None: + if reasoning: return [ContentThinkingDelta(thinking=reasoning)] text = delta.content diff --git a/tests/test_provider_openai_completions.py b/tests/test_provider_openai_completions.py index eb5178ee..8547ea02 100644 --- a/tests/test_provider_openai_completions.py +++ b/tests/test_provider_openai_completions.py @@ -206,6 +206,33 @@ def __init__(self, choices): assert result == [ContentText(text="hello")] +def test_stream_content_empty_reasoning_keeps_text(): + """Some backends send `reasoning_content: ""` alongside the text of a chunk.""" + from openai.types.chat import ChatCompletionChunk + + provider = OpenAICompletionsProvider(model="test") + + def chunk(**delta): + return ChatCompletionChunk.model_validate( + { + "id": "1", + "object": "chat.completion.chunk", + "created": 0, + "model": "test", + "choices": [{"index": 0, "delta": delta, "finish_reason": None}], + } + ) + + result = provider.stream_content(chunk(content="Hello", reasoning_content=""), None) + assert result == [ContentText(text="Hello")] + + result = provider.stream_content(chunk(content="Hello", reasoning=""), None) + assert result == [ContentText(text="Hello")] + + # A chunk with neither reasoning nor text adds nothing. + assert provider.stream_content(chunk(reasoning_content=""), None) == [] + + def test_response_as_turn_extracts_reasoning_content(): from unittest.mock import Mock