From 39961ad57a4260c46d106cb0df13baaf7e20f834 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Mon, 28 Sep 2026 04:23:18 +0530 Subject: [PATCH 1/3] fix(streaming): reject partial generic streaming chunks missing required fields Fixes #43487 --- .../litellm_core_utils/streaming_handler.py | 3 ++- .../test_streaming_handler.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index fa4650aec4f..6e6a0cb3b65 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -55,6 +55,7 @@ FUNCTION_CALL_ATTRIBUTE: Final = "function_call" _SYNC_ITER_EXHAUSTED: Final = object() _GCHUNK_FIELDS: Final[frozenset] = frozenset(GChunk.__annotations__) +_GCHUNK_REQUIRED_FIELDS: Final[frozenset] = frozenset(GChunk.__required_keys__) _USAGE_COST_HEADER_PROVIDERS: Final[frozenset[str]] = frozenset({LlmProviders.OPENROUTER.value}) @@ -2459,7 +2460,7 @@ def generic_chunk_has_all_required_fields(chunk: dict) -> bool: :param chunk: The dictionary to check. :return: True if all required fields are present, False otherwise. """ - return all(key in _GCHUNK_FIELDS for key in chunk) + return all(key in chunk for key in _GCHUNK_REQUIRED_FIELDS) def convert_generic_chunk_to_model_response_stream( diff --git a/tests/unit/litellm_core_utils/test_streaming_handler.py b/tests/unit/litellm_core_utils/test_streaming_handler.py index 62d8b0e203f..3cc64f1a727 100644 --- a/tests/unit/litellm_core_utils/test_streaming_handler.py +++ b/tests/unit/litellm_core_utils/test_streaming_handler.py @@ -17,6 +17,8 @@ from litellm.litellm_core_utils.streaming_handler import ( CustomStreamWrapper, _ProviderChunkEarlyReturn, _ProviderChunkParsed, + convert_generic_chunk_to_model_response_stream, + generic_chunk_has_all_required_fields, ) from litellm.types.utils import ( CompletionTokensDetailsWrapper, @@ -4983,3 +4985,21 @@ async def test_async_stream_without_usage_counts_tokens_off_the_event_loop(): assert chunks[-1].usage.prompt_tokens > 100_000 assert chunks[-1].usage.completion_tokens > 100_000 assert_loop_stayed_free(took, lags) + + +def test_generic_chunk_predicate_rejects_partial_chunk(): + partial_chunk = {"is_finished": False} + assert generic_chunk_has_all_required_fields(partial_chunk) is False + + full_chunk = { + "text": "hello", + "is_finished": True, + "finish_reason": "stop", + "usage": None, + "index": 0, + } + assert generic_chunk_has_all_required_fields(full_chunk) is True + + stream = convert_generic_chunk_to_model_response_stream(full_chunk) + assert stream.choices[0].delta.content == "hello" + assert stream.finish_reason == "stop" From dbe3094391b789a8ec28dc1de92b5c0a34ee0d79 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Tue, 29 Sep 2026 10:07:02 +0530 Subject: [PATCH 2/3] test: update generic-chunk predicate test to required-fields semantics --- .../test_streaming_overhead.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/unit/litellm_core_utils/test_streaming_overhead.py b/tests/unit/litellm_core_utils/test_streaming_overhead.py index 8fb0659ab5a..96011624a38 100644 --- a/tests/unit/litellm_core_utils/test_streaming_overhead.py +++ b/tests/unit/litellm_core_utils/test_streaming_overhead.py @@ -127,24 +127,22 @@ def test_gchunk_fields_is_frozenset(): def test_generic_chunk_has_all_required_fields_uses_module_constant(monkeypatch): - """generic_chunk_has_all_required_fields must use _GCHUNK_FIELDS, not __annotations__. + """generic_chunk_has_all_required_fields must use _GCHUNK_REQUIRED_FIELDS. - The check semantics: every key in `chunk` must be a known GChunk field. - This identifies GChunk-shaped dicts (all keys are valid GChunk fields). + The check semantics: every required GChunk field must be present in `chunk`. + A chunk missing a required field is not a usable generic chunk. """ valid_chunk = _make_generic_chunk("hello") assert generic_chunk_has_all_required_fields(valid_chunk) is True - # A dict with an extra unknown key should return False — the unknown key - # is not a GChunk field, so the chunk is not a pure GChunk. + # Extra unknown keys don't matter, only that all required fields are present. extra_key_chunk = dict(valid_chunk) extra_key_chunk["unknown_extra_key"] = "value" - assert generic_chunk_has_all_required_fields(extra_key_chunk) is False + assert generic_chunk_has_all_required_fields(extra_key_chunk) is True - # A dict with only known GChunk fields but fewer keys still passes because - # all its keys are valid (subset of GChunk fields). + # A dict missing required fields is rejected, even if every key it has is valid. partial_chunk = {"text": "hi", "is_finished": False} - assert generic_chunk_has_all_required_fields(partial_chunk) is True + assert generic_chunk_has_all_required_fields(partial_chunk) is False # --------------------------------------------------------------------------- From e33457d9263373e5ebba9b9cd68cb40b5e8db9f7 Mon Sep 17 00:00:00 2001 From: bunnysayzz Date: Wed, 30 Sep 2026 04:06:34 +0530 Subject: [PATCH 3/3] fix(streaming): keep the upper bound on generic chunk fields --- litellm/litellm_core_utils/streaming_handler.py | 2 +- .../litellm_core_utils/test_streaming_overhead.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py index 6e6a0cb3b65..147f7d5423e 100644 --- a/litellm/litellm_core_utils/streaming_handler.py +++ b/litellm/litellm_core_utils/streaming_handler.py @@ -2460,7 +2460,7 @@ def generic_chunk_has_all_required_fields(chunk: dict) -> bool: :param chunk: The dictionary to check. :return: True if all required fields are present, False otherwise. """ - return all(key in chunk for key in _GCHUNK_REQUIRED_FIELDS) + return _GCHUNK_REQUIRED_FIELDS <= chunk.keys() <= _GCHUNK_FIELDS def convert_generic_chunk_to_model_response_stream( diff --git a/tests/unit/litellm_core_utils/test_streaming_overhead.py b/tests/unit/litellm_core_utils/test_streaming_overhead.py index 96011624a38..2a6af87cf28 100644 --- a/tests/unit/litellm_core_utils/test_streaming_overhead.py +++ b/tests/unit/litellm_core_utils/test_streaming_overhead.py @@ -127,18 +127,20 @@ def test_gchunk_fields_is_frozenset(): def test_generic_chunk_has_all_required_fields_uses_module_constant(monkeypatch): - """generic_chunk_has_all_required_fields must use _GCHUNK_REQUIRED_FIELDS. + """generic_chunk_has_all_required_fields must use the module constants. - The check semantics: every required GChunk field must be present in `chunk`. - A chunk missing a required field is not a usable generic chunk. + The check semantics: every required GChunk field must be present in `chunk` + and every key must be a known GChunk field. Unknown keys and missing + required fields both reject the chunk. """ valid_chunk = _make_generic_chunk("hello") assert generic_chunk_has_all_required_fields(valid_chunk) is True - # Extra unknown keys don't matter, only that all required fields are present. + # A dict with an extra unknown key should return False — the unknown key + # is not a GChunk field, so the chunk is not a pure GChunk. extra_key_chunk = dict(valid_chunk) extra_key_chunk["unknown_extra_key"] = "value" - assert generic_chunk_has_all_required_fields(extra_key_chunk) is True + assert generic_chunk_has_all_required_fields(extra_key_chunk) is False # A dict missing required fields is rejected, even if every key it has is valid. partial_chunk = {"text": "hi", "is_finished": False}