diff --git a/litellm/main.py b/litellm/main.py index 12854db15d0..5b5e43258fd 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8952,7 +8952,11 @@ def _delta_carries_more_than_text(delta: Mapping[str, object]) -> bool: def _simple_text_part(choices: Sequence[object]) -> str | None: + if not choices: + return None deltas: Final = tuple(_stream_choice_delta(choice) for choice in choices) + if not deltas: + return None if any(_delta_carries_more_than_text(delta) for delta in deltas): return None content: Final = deltas[0].get("content") diff --git a/tests/local_testing/test_stream_chunk_builder.py b/tests/local_testing/test_stream_chunk_builder.py index 6d62dd52b89..660586777fb 100644 --- a/tests/local_testing/test_stream_chunk_builder.py +++ b/tests/local_testing/test_stream_chunk_builder.py @@ -895,3 +895,19 @@ def test_grok_bug(load_env): litellm.set_verbose = True _, LLAMA3_3 = load_env execute_completion(LLAMA3_3) + + +def test_stream_chunk_builder_empty_choices_guard(): + """ + Test that stream_chunk_builder does not crash with IndexError when + streaming chunks contain empty choices lists (e.g. trailing usage chunks). + """ + chunks = [ + {"id": "chat-test-1", "model": "gpt-4o", "choices": [{"index": 0, "delta": {"role": "assistant", "content": "Hello"}}]}, + {"id": "chat-test-1", "model": "gpt-4o", "choices": [{"index": 0, "delta": {"content": " world"}}]}, + {"id": "chat-test-1", "model": "gpt-4o", "choices": [], "usage": {"prompt_tokens": 10, "completion_tokens": 2, "total_tokens": 12}}, + ] + res = litellm.stream_chunk_builder(chunks) + assert res is not None + assert res["choices"][0]["message"]["content"] == "Hello world" +