From b0ba3b2bc443a58dd9e6bba133a2c1a7273f91cf Mon Sep 17 00:00:00 2001 From: xuan <118178092+yuee3@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:03:04 +0800 Subject: [PATCH 1/3] fix(streaming): move sync max-duration check inside try so Timeout flows through the failure pipeline --- litellm/litellm_core_utils/streaming_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 5542249d2c7..86b6bb854b3 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -2068,8 +2068,8 @@ class CustomStreamWrapper: cache_hit = False if self.custom_llm_provider is not None and self.custom_llm_provider == "cached_response": cache_hit = True - self._check_max_streaming_duration() try: + self._check_max_streaming_duration() if self.completion_stream is None: self.fetch_sync_stream() From 5d2706fe2a13ba83889ff3e5a12c83c10f55b3ab Mon Sep 17 00:00:00 2001 From: xuan <118178092+yuee3@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:03:41 +0800 Subject: [PATCH 2/3] test(streaming): assert sync max-duration Timeout dispatches failure handler and closes stream --- .../test_max_streaming_duration.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/litellm_core_utils/test_max_streaming_duration.py b/tests/unit/litellm_core_utils/test_max_streaming_duration.py index 234bc83e3c3..e645d410419 100644 --- a/tests/unit/litellm_core_utils/test_max_streaming_duration.py +++ b/tests/unit/litellm_core_utils/test_max_streaming_duration.py @@ -65,6 +65,21 @@ class TestCustomStreamWrapperMaxDuration: with pytest.raises(litellm.Timeout): wrapper.__next__() + def test_sync_next_timeout_dispatches_failure_handler_and_closes(self): + """The sync Timeout must flow through the failure pipeline (parity with __anext__).""" + wrapper = _make_custom_stream_wrapper() + wrapper.logging_obj.failure_handler = MagicMock() + wrapper._stream_created_time = time.time() - 20 + with patch("litellm.constants.LITELLM_MAX_STREAMING_DURATION_SECONDS", 10.0): + with pytest.raises(litellm.Timeout): + wrapper.__next__() + assert wrapper._closed is True + + # failure_handler is dispatched on a thread; give it a moment + deadline = time.time() + 2.0 + while time.time() < deadline and wrapper.logging_obj.failure_handler.call_count == 0: + time.sleep(0.01) + wrapper.logging_obj.failure_handler.assert_called_once() @pytest.mark.asyncio async def test_should_raise_on_async_anext_when_exceeded(self): """__anext__ checks the limit before iterating and schedules failure logging.""" From 7e82c048e4183df947ad1842e4eca997cded6caa Mon Sep 17 00:00:00 2001 From: xuan <118178092+yuee3@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:04:08 +0800 Subject: [PATCH 3/3] style: add missing blank line before next test --- tests/unit/litellm_core_utils/test_max_streaming_duration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/litellm_core_utils/test_max_streaming_duration.py b/tests/unit/litellm_core_utils/test_max_streaming_duration.py index e645d410419..ddaf5868bec 100644 --- a/tests/unit/litellm_core_utils/test_max_streaming_duration.py +++ b/tests/unit/litellm_core_utils/test_max_streaming_duration.py @@ -80,6 +80,7 @@ class TestCustomStreamWrapperMaxDuration: while time.time() < deadline and wrapper.logging_obj.failure_handler.call_count == 0: time.sleep(0.01) wrapper.logging_obj.failure_handler.assert_called_once() + @pytest.mark.asyncio async def test_should_raise_on_async_anext_when_exceeded(self): """__anext__ checks the limit before iterating and schedules failure logging."""