From e5f7a0b0a5b0240099d815ca0942f4e13f866195 Mon Sep 17 00:00:00 2001 From: Devaj Mody Date: Fri, 5 Dec 2025 18:53:49 -0500 Subject: [PATCH] fix(streaming): add length validation for empty tool_calls in delta (#17523) Fixes #17425 - Add length check for tool_calls in model_response.choices[0].delta - Prevents empty tool call objects from appearing in streaming responses - Add regression tests for empty and valid tool_calls scenarios --- .../litellm_core_utils/streaming_handler.py | 1 + .../test_streaming_handler.py | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index a7f460fab5..4ffb7ace5b 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -737,6 +737,7 @@ class CustomStreamWrapper: or ( "tool_calls" in model_response.choices[0].delta and model_response.choices[0].delta["tool_calls"] is not None + and len(model_response.choices[0].delta["tool_calls"]) > 0 ) or ( "function_call" in model_response.choices[0].delta diff --git a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py index 3456f69eeb..6a528fef8f 100644 --- a/tests/test_litellm/litellm_core_utils/test_streaming_handler.py +++ b/tests/test_litellm/litellm_core_utils/test_streaming_handler.py @@ -1079,3 +1079,86 @@ def test_has_special_delta_attribute( assert not initialized_custom_stream_wrapper._has_special_delta_attribute( delta_with_none, "audio" ) + + +def test_is_chunk_non_empty_with_empty_tool_calls( + initialized_custom_stream_wrapper: CustomStreamWrapper, +): + """ + Test that is_chunk_non_empty returns False when tool_calls is an empty list. + + Regression test for https://github.com/BerriAI/litellm/issues/17425 + Empty tool_calls in delta should not be considered non-empty chunks. + """ + chunk = { + "id": "test-chunk-id", + "object": "chat.completion.chunk", + "created": 1741037890, + "model": "claude-sonnet-4-20250514", + "choices": [ + { + "index": 0, + "delta": { + "content": None, + "tool_calls": [], # Empty tool_calls list + }, + "logprobs": None, + "finish_reason": None, + } + ], + } + # Empty tool_calls should return False + assert ( + initialized_custom_stream_wrapper.is_chunk_non_empty( + completion_obj={}, # completion_obj has no tool_calls + model_response=ModelResponseStream(**chunk), + response_obj={}, + ) + is False + ) + + +def test_is_chunk_non_empty_with_valid_tool_calls( + initialized_custom_stream_wrapper: CustomStreamWrapper, +): + """ + Test that is_chunk_non_empty returns True when tool_calls has valid entries. + + Companion test for https://github.com/BerriAI/litellm/issues/17425 + Non-empty tool_calls in delta should be considered non-empty chunks. + """ + chunk = { + "id": "test-chunk-id", + "object": "chat.completion.chunk", + "created": 1741037890, + "model": "claude-sonnet-4-20250514", + "choices": [ + { + "index": 0, + "delta": { + "content": None, + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"location": "NYC"}', + }, + } + ], + }, + "logprobs": None, + "finish_reason": None, + } + ], + } + # Non-empty tool_calls should return True + assert ( + initialized_custom_stream_wrapper.is_chunk_non_empty( + completion_obj={}, + model_response=ModelResponseStream(**chunk), + response_obj={}, + ) + is True + )