Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions litellm/litellm_core_utils/streaming_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ def raise_on_model_repetition(self) -> None:
if len(self.chunks) < 2:
return

# Providers like Vertex Gemini (Flash / Flash Lite with web search) emit
# metadata-only / usage-only chunks with no choices. These get stored in
# self.chunks but carry no comparable content, so skip repetition detection.
if not self.chunks[-1].choices or not self.chunks[-2].choices:
return

last_content = self.chunks[-1].choices[0].delta.content

if (
Expand Down
38 changes: 38 additions & 0 deletions tests/test_litellm/litellm_core_utils/test_streaming_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1509,6 +1509,44 @@ def test_raise_on_model_repetition(
wrapper.raise_on_model_repetition()


@pytest.mark.parametrize(
"empty_chunk_index",
[-1, -2],
ids=["last_chunk_empty", "second_to_last_chunk_empty"],
)
def test_raise_on_model_repetition_tolerates_empty_choices(
initialized_custom_stream_wrapper: CustomStreamWrapper,
empty_chunk_index: int,
):
"""
Regression test for https://github.com/BerriAI/litellm/issues/28884

Vertex Gemini Flash / Flash Lite with web search streaming emits
metadata-only and usage-only chunks that carry no choices. These are
appended to self.chunks, and raise_on_model_repetition() previously
accessed choices[0] unconditionally, raising IndexError mid-stream
(surfaced to users as MidStreamFallbackError -> APIConnectionError).
"""
wrapper = initialized_custom_stream_wrapper

chunks = [
_make_chunk("hello world"),
ModelResponseStream(
id="usage-only",
created=1741037890,
model="vertex_ai/gemini-3.1-flash-lite",
choices=[],
usage=Usage(prompt_tokens=10, completion_tokens=0, total_tokens=10),
),
]
if empty_chunk_index == -2:
chunks.append(_make_chunk("hello world again"))

for chunk in chunks:
wrapper.chunks.append(chunk)
wrapper.raise_on_model_repetition()


def test_usage_chunk_after_finish_reason_updates_hidden_params(logging_obj):
"""
Test that provider-reported usage from a post-finish_reason chunk
Expand Down
Loading