From 50c938393584556a5c80c1d8f6e089238a56e56b Mon Sep 17 00:00:00 2001 From: Chris Nestrud Date: Thu, 1 Jan 2026 13:19:01 -0600 Subject: [PATCH 1/3] fix: suppress empty response warnings when finish reason is present Co-authored-by: aider-ce (synthetic/hf:MiniMaxAI/MiniMax-M2) --- aider/coders/base_coder.py | 29 ++++- tests/basic/test_coder.py | 210 +++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+), 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 9c21669a56b..42bd0ea3537 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -3064,6 +3064,22 @@ def show_send_output(self, completion): self.io.tool_error(content_err) raise Exception("No data found in LLM response!") + # Check if we have a valid response with a finish reason + has_valid_finish_reason = False + if completion and hasattr(completion, "choices") and completion.choices: + choice = completion.choices[0] + if hasattr(choice, "finish_reason") and choice.finish_reason: + has_valid_finish_reason = True + + # Only warn if truly empty (no content, no tool calls, no valid finish_reason) + if ( + not self.partial_response_content + and len(self.partial_response_tool_calls) == 0 + and not has_valid_finish_reason + ): + self.io.tool_warning("Empty response received from LLM. Check your provider account?") + return + show_resp = self.render_incremental_response(True) if self.partial_response_reasoning_content: @@ -3200,7 +3216,18 @@ async def show_send_output_stream(self, completion): # The Part Doing the Heavy Lifting Now self.consolidate_chunks() - if not received_content and len(self.partial_response_tool_calls) == 0: + # Check if we have a valid response with a finish reason + has_valid_finish_reason = False + if completion and hasattr(completion, "choices") and completion.choices: + choice = completion.choices[0] + if hasattr(choice, "finish_reason") and choice.finish_reason: + has_valid_finish_reason = True + + if ( + not received_content + and len(self.partial_response_tool_calls) == 0 + and not has_valid_finish_reason + ): self.io.tool_warning("Empty response received from LLM. Check your provider account?") def consolidate_chunks(self): diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index 42373ac1dac..e9b99d67ca0 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -1952,5 +1952,215 @@ async def test_execute_tool_calls_blob_content(self, mock_call_openai_tool): ) self.assertEqual(result[0]["content"], expected_content) + async def test_show_send_output_stream_valid_empty_with_finish_reason(self): + """Test that valid empty responses with finish_reason don't trigger warnings in streaming mode.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a valid response with finish_reason but no content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "stop" + mock_response.choices[0].delta = MagicMock() + del mock_response.choices[0].delta.content # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the streaming output method + list(coder.show_send_output_stream(mock_response)) + + # Verify that tool_warning was NOT called (valid empty response) + io.tool_warning.assert_not_called() + + async def test_show_send_output_stream_truly_empty_response(self): + """Test that truly empty responses trigger warnings in streaming mode.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a truly empty response (no finish_reason, no content, no tool calls) + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = None # No finish reason + mock_response.choices[0].delta = MagicMock() + del mock_response.choices[0].delta.content # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the streaming output method + list(coder.show_send_output_stream(mock_response)) + + # Verify that tool_warning WAS called (truly empty response) + io.tool_warning.assert_called_once_with( + "Empty response received from LLM. Check your provider account?" + ) + + async def test_show_send_output_stream_response_with_content(self): + """Test that responses with content don't trigger warnings in streaming mode.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a response with content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "stop" + mock_response.choices[0].delta = MagicMock() + mock_response.choices[0].delta.content = "Some response content" + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the streaming output method + list(coder.show_send_output_stream(mock_response)) + + # Verify that tool_warning was NOT called (has content) + io.tool_warning.assert_not_called() + + async def test_show_send_output_stream_response_with_tool_calls(self): + """Test that responses with tool calls don't trigger warnings in streaming mode.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a response with tool calls but no content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "tool_calls" + mock_response.choices[0].delta = MagicMock() + del mock_response.choices[0].delta.content # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Mock partial_response_tool_calls to simulate tool calls + coder.partial_response_tool_calls = [{"id": "test_tool"}] + + # Call the streaming output method + list(coder.show_send_output_stream(mock_response)) + + # Verify that tool_warning was NOT called (has tool calls) + io.tool_warning.assert_not_called() + + async def test_show_send_output_valid_empty_with_finish_reason(self): + """Test that valid empty responses with finish_reason don't trigger warnings.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a valid response with finish_reason but no content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "stop" + mock_response.choices[0].message = MagicMock() + mock_response.choices[0].message.content = None # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the non-streaming output method + coder.show_send_output(mock_response) + + # Verify that tool_warning was NOT called (valid empty response) + io.tool_warning.assert_not_called() + + async def test_show_send_output_truly_empty_response(self): + """Test that truly empty responses trigger warnings.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a truly empty response (no finish_reason, no content, no tool calls) + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = None # No finish reason + mock_response.choices[0].message = MagicMock() + mock_response.choices[0].message.content = None # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the non-streaming output method + coder.show_send_output(mock_response) + + # Verify that tool_warning WAS called (truly empty response) + io.tool_warning.assert_called_once_with( + "Empty response received from LLM. Check your provider account?" + ) + + async def test_show_send_output_response_with_content(self): + """Test that responses with content don't trigger warnings.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a response with content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "stop" + mock_response.choices[0].message = MagicMock() + mock_response.choices[0].message.content = "Some response content" + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Call the non-streaming output method + coder.show_send_output(mock_response) + + # Verify that tool_warning was NOT called (has content) + io.tool_warning.assert_not_called() + + async def test_show_send_output_response_with_tool_calls(self): + """Test that responses with tool calls don't trigger warnings.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = await Coder.create(self.GPT35, "diff", io=io) + + # Mock a response with tool calls but no content + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].finish_reason = "tool_calls" + mock_response.choices[0].message = MagicMock() + mock_response.choices[0].message.content = None # No content + + # Mock the consolidate_chunks method to return our response + coder.consolidate_chunks = MagicMock(return_value=mock_response) + + # Mock tool_warning to check if it's called + io.tool_warning = MagicMock() + + # Mock partial_response_tool_calls to simulate tool calls + coder.partial_response_tool_calls = [{"id": "test_tool"}] + + # Call the non-streaming output method + coder.show_send_output(mock_response) + + # Verify that tool_warning was NOT called (has tool calls) + io.tool_warning.assert_not_called() + # Remove the unittest.main() since we're using pytest From 95c9324c6334315e03ad0af22ead49b0fc027e2e Mon Sep 17 00:00:00 2001 From: Chris Nestrud Date: Fri, 2 Jan 2026 15:45:30 -0600 Subject: [PATCH 2/3] refactor: return response metadata from consolidate_chunks --- cecli/coders/base_coder.py | 82 +++++++++++++++++++++----------------- tests/basic/test_coder.py | 51 +++++++++++++++++++----- 2 files changed, 87 insertions(+), 46 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index 732c1f23a79..c3111ee9281 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -16,6 +16,7 @@ import traceback import weakref from collections import defaultdict +from dataclasses import dataclass from datetime import datetime from unittest.mock import Mock @@ -82,6 +83,16 @@ class FinishReasonLength(Exception): pass +@dataclass +class ConsolidatedResponseMetadata: + """LiteLLM response metadata extracted during chunk consolidation.""" + + choice_has_finish_reason: bool = False + choice_has_content: bool = False + choice_has_tool_calls: bool = False + choice_content_text: str = "" + + def wrap_fence(name): return f"<{name}>", f"" @@ -2397,7 +2408,7 @@ async def send_message(self, inp): # Process any tools using MCP servers try: if self.partial_response_tool_calls: - tool_call_response, a, b = self.consolidate_chunks() + tool_call_response, a, b, _ = self.consolidate_chunks() if await self.process_tool_calls(tool_call_response): self.num_tool_calls += 1 self.reflected_message = True @@ -3149,7 +3160,7 @@ def show_send_output(self, completion): self.partial_response_chunks.append(completion) - response, func_err, content_err = self.consolidate_chunks() + response, func_err, content_err, response_meta = self.consolidate_chunks() resp_hash = dict( function_call=str(self.partial_response_function_call), @@ -3163,32 +3174,26 @@ def show_send_output(self, completion): self.io.tool_error(content_err) raise Exception("No data found in LLM response!") - response_choice = None - if response and hasattr(response, "choices") and response.choices: - response_choice = response.choices[0] - completion_choice = None if completion and hasattr(completion, "choices") and completion.choices: completion_choice = completion.choices[0] # Check if we have a valid response with a finish reason - has_valid_finish_reason = False - for choice in (completion_choice, response_choice): - if not has_valid_finish_reason and self._choice_has_finish_reason(choice): - has_valid_finish_reason = True - - response_has_content = bool(self.partial_response_content) - if not response_has_content: - response_has_content = self._choice_has_content(response_choice) + has_valid_finish_reason = response_meta.choice_has_finish_reason + if not has_valid_finish_reason and self._choice_has_finish_reason(completion_choice): + has_valid_finish_reason = True - response_has_tool_calls = len(self.partial_response_tool_calls) > 0 - if not response_has_tool_calls: - response_has_tool_calls = self._choice_has_tool_calls(response_choice) + response_has_content = ( + bool(self.partial_response_content) or response_meta.choice_has_content + ) + response_has_tool_calls = ( + len(self.partial_response_tool_calls) > 0 or response_meta.choice_has_tool_calls + ) if response_has_content and not self.partial_response_content: - fallback_content = self._choice_content_text( - response_choice - ) or self._choice_content_text(completion_choice) + fallback_content = response_meta.choice_content_text or self._choice_content_text( + completion_choice + ) if fallback_content: self.partial_response_content = fallback_content @@ -3332,29 +3337,23 @@ async def _show_send_output_stream_async(self, completion): yield text # The Part Doing the Heavy Lifting Now - response, _, _ = self.consolidate_chunks() - - response_choice = None - if response and hasattr(response, "choices") and response.choices: - response_choice = response.choices[0] + response, _, _, response_meta = self.consolidate_chunks() completion_choice = None if completion and hasattr(completion, "choices") and completion.choices: completion_choice = completion.choices[0] # Check if we have a valid response with a finish reason - has_valid_finish_reason = False - for choice in (completion_choice, response_choice): - if not has_valid_finish_reason and self._choice_has_finish_reason(choice): - has_valid_finish_reason = True - - response_has_content = bool(self.partial_response_content) - if not response_has_content: - response_has_content = self._choice_has_content(response_choice) + has_valid_finish_reason = response_meta.choice_has_finish_reason + if not has_valid_finish_reason and self._choice_has_finish_reason(completion_choice): + has_valid_finish_reason = True - response_has_tool_calls = len(self.partial_response_tool_calls) > 0 - if not response_has_tool_calls: - response_has_tool_calls = self._choice_has_tool_calls(response_choice) + response_has_content = ( + bool(self.partial_response_content) or response_meta.choice_has_content + ) + response_has_tool_calls = ( + len(self.partial_response_tool_calls) > 0 or response_meta.choice_has_tool_calls + ) if not response_has_content and not response_has_tool_calls and not has_valid_finish_reason: self.io.tool_warning("Empty response received from LLM. Check your provider account?") @@ -3395,6 +3394,7 @@ def consolidate_chunks(self): ) func_err = None content_err = None + response_meta = ConsolidatedResponseMetadata() # Collect provider-specific fields from chunks to preserve them # We need to track both by ID (primary) and index (fallback) since @@ -3497,7 +3497,15 @@ def consolidate_chunks(self): except AttributeError as e: content_err = e - return response, func_err, content_err + response_choice = None + if response and hasattr(response, "choices") and response.choices: + response_choice = response.choices[0] + response_meta.choice_has_finish_reason = self._choice_has_finish_reason(response_choice) + response_meta.choice_content_text = self._choice_content_text(response_choice) or "" + response_meta.choice_has_content = bool(response_meta.choice_content_text) + response_meta.choice_has_tool_calls = self._choice_has_tool_calls(response_choice) + + return response, func_err, content_err, response_meta def stream_wrapper(self, content, final): if not hasattr(self, "_streaming_buffer_length"): diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index af41600d6ac..9bfa38da426 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -11,7 +11,11 @@ from litellm.types.utils import Choices, ModelResponse from cecli.coders import Coder -from cecli.coders.base_coder import FinishReasonLength, UnknownEditFormat +from cecli.coders.base_coder import ( + ConsolidatedResponseMetadata, + FinishReasonLength, + UnknownEditFormat, +) from cecli.commands import SwitchCoder from cecli.dump import dump # noqa: F401 from cecli.io import InputOutput @@ -56,6 +60,19 @@ def _make_model_response(*, finish_reason=None, content=None): response.choices[0].message.content = content return response + @staticmethod + def _make_consolidated_tuple(response): + choice = None + if response and getattr(response, "choices", None): + choice = response.choices[0] + meta = ConsolidatedResponseMetadata( + choice_has_finish_reason=Coder._choice_has_finish_reason(choice), + choice_has_content=Coder._choice_has_content(choice), + choice_has_tool_calls=Coder._choice_has_tool_calls(choice), + choice_content_text=Coder._choice_content_text(choice) if choice else "", + ) + return (response, None, None, meta) + @staticmethod def _reset_partial_response_state(coder): coder.partial_response_content = "" @@ -2015,7 +2032,9 @@ async def test_show_send_output_stream_valid_empty_with_finish_reason(self): mock_response.__aiter__.return_value = iter([mock_response]) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2043,7 +2062,9 @@ async def test_show_send_output_stream_truly_empty_response(self): mock_response.__aiter__.return_value = iter([mock_response]) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2073,7 +2094,9 @@ async def test_show_send_output_stream_response_with_content(self): mock_response.__aiter__.return_value = iter([mock_response]) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2101,7 +2124,9 @@ async def test_show_send_output_stream_response_with_tool_calls(self): mock_response.__aiter__.return_value = iter([mock_response]) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2126,7 +2151,9 @@ async def test_show_send_output_valid_empty_with_finish_reason(self): mock_response = self._make_model_response(finish_reason="stop", content=None) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2148,7 +2175,9 @@ async def test_show_send_output_truly_empty_response(self): mock_response = self._make_model_response(finish_reason=None, content=None) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2174,7 +2203,9 @@ async def test_show_send_output_response_with_content(self): ) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() @@ -2196,7 +2227,9 @@ async def test_show_send_output_response_with_tool_calls(self): mock_response = self._make_model_response(finish_reason="tool_calls", content=None) # Mock the consolidate_chunks method to return our response - coder.consolidate_chunks = MagicMock(return_value=(mock_response, None, None)) + coder.consolidate_chunks = MagicMock( + return_value=self._make_consolidated_tuple(mock_response) + ) # Mock tool_warning to check if it's called io.tool_warning = MagicMock() From 6d1b10cc7467296d92916a63e663e9029c4b561c Mon Sep 17 00:00:00 2001 From: Chris Nestrud Date: Fri, 2 Jan 2026 21:55:54 -0600 Subject: [PATCH 3/3] refactor: centralize empty response warnings Move the empty-response detection out of the streaming code paths and into consolidate_chunks metadata so both synchronous and streaming sends share the same finish_reason/content/tool-call bookkeeping. Also drop the sync-only show_send_output_stream wrapper so we no longer change runtime behavior for tests. --- cecli/coders/base_coder.py | 88 +++++++++++++++----------------------- 1 file changed, 35 insertions(+), 53 deletions(-) diff --git a/cecli/coders/base_coder.py b/cecli/coders/base_coder.py index c3111ee9281..d53c438e7b7 100755 --- a/cecli/coders/base_coder.py +++ b/cecli/coders/base_coder.py @@ -28,7 +28,7 @@ Locale = None from json.decoder import JSONDecodeError from pathlib import Path -from typing import List +from typing import List, Optional import httpx from litellm import experimental_mcp_client @@ -3146,6 +3146,28 @@ def _choice_has_tool_calls(cls, choice): tool_calls = cls._safe_getattr(delta, "tool_calls") return bool(tool_calls) + def _warn_if_truly_empty_response( + self, + response_meta: ConsolidatedResponseMetadata, + response_has_content: Optional[bool] = None, + response_has_tool_calls: Optional[bool] = None, + ) -> bool: + if response_has_content is None: + response_has_content = ( + bool(self.partial_response_content) or response_meta.choice_has_content + ) + if response_has_tool_calls is None: + response_has_tool_calls = ( + bool(self.partial_response_tool_calls) or response_meta.choice_has_tool_calls + ) + + has_valid_finish_reason = response_meta.choice_has_finish_reason + + if not response_has_content and not response_has_tool_calls and not has_valid_finish_reason: + self.io.tool_warning("Empty response received from LLM. Check your provider account?") + return True + return False + def show_send_output(self, completion): if self.verbose: print(completion) @@ -3178,11 +3200,6 @@ def show_send_output(self, completion): if completion and hasattr(completion, "choices") and completion.choices: completion_choice = completion.choices[0] - # Check if we have a valid response with a finish reason - has_valid_finish_reason = response_meta.choice_has_finish_reason - if not has_valid_finish_reason and self._choice_has_finish_reason(completion_choice): - has_valid_finish_reason = True - response_has_content = ( bool(self.partial_response_content) or response_meta.choice_has_content ) @@ -3197,9 +3214,11 @@ def show_send_output(self, completion): if fallback_content: self.partial_response_content = fallback_content - # Only warn if truly empty (no content, no tool calls, no valid finish_reason) - if not response_has_content and not response_has_tool_calls and not has_valid_finish_reason: - self.io.tool_warning("Empty response received from LLM. Check your provider account?") + if self._warn_if_truly_empty_response( + response_meta, + response_has_content=response_has_content, + response_has_tool_calls=response_has_tool_calls, + ): return show_resp = self.render_incremental_response(True) @@ -3223,11 +3242,8 @@ def show_send_output(self, completion): ): raise FinishReasonLength() - async def _show_send_output_stream_async(self, completion): - """ - Internal async helper that contains the original streaming logic. - This is an async generator that yields chunks from a streaming completion. - """ + async def show_send_output_stream(self, completion): + """Yield streaming chunks from the LLM completion.""" async for chunk in completion: if self.args.debug: with open(".cecli/logs/chunks.log", "a") as f: @@ -3339,15 +3355,6 @@ async def _show_send_output_stream_async(self, completion): # The Part Doing the Heavy Lifting Now response, _, _, response_meta = self.consolidate_chunks() - completion_choice = None - if completion and hasattr(completion, "choices") and completion.choices: - completion_choice = completion.choices[0] - - # Check if we have a valid response with a finish reason - has_valid_finish_reason = response_meta.choice_has_finish_reason - if not has_valid_finish_reason and self._choice_has_finish_reason(completion_choice): - has_valid_finish_reason = True - response_has_content = ( bool(self.partial_response_content) or response_meta.choice_has_content ) @@ -3355,36 +3362,11 @@ async def _show_send_output_stream_async(self, completion): len(self.partial_response_tool_calls) > 0 or response_meta.choice_has_tool_calls ) - if not response_has_content and not response_has_tool_calls and not has_valid_finish_reason: - self.io.tool_warning("Empty response received from LLM. Check your provider account?") - - def show_send_output_stream(self, completion): - """ - Synchronous wrapper around _show_send_output_stream_async. - - * When **no event loop** is running (the normal test scenario) we - execute the async generator to completion with ``asyncio.run`` and - yield the collected items one‑by‑one. - - * When an **event loop is already running** (e.g. the method is called - from async code) we simply return the async generator itself so the - caller can ``async for`` over it. - """ - - def _run_sync(): - async def _collect_all(): - return [chunk async for chunk in self._show_send_output_stream_async(completion)] - - collected_chunks = asyncio.run(_collect_all()) - for chunk in collected_chunks: - yield chunk - - try: - asyncio.get_running_loop() - except RuntimeError: - return _run_sync() - else: - return self._show_send_output_stream_async(completion) + self._warn_if_truly_empty_response( + response_meta, + response_has_content=response_has_content, + response_has_tool_calls=response_has_tool_calls, + ) def consolidate_chunks(self): response = (