diff --git a/litellm/main.py b/litellm/main.py index 769eac79488..0fc8a27bd38 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8976,7 +8976,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..81c8ef2295a 100644 --- a/tests/local_testing/test_stream_chunk_builder.py +++ b/tests/local_testing/test_stream_chunk_builder.py @@ -895,3 +895,26 @@ 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). + """ + from litellm.main import _simple_text_part + + # Direct unit test exercising empty-sequence guards in _simple_text_part + assert _simple_text_part([]) is None + assert _simple_text_part([{"delta": {}}]) == "" + + 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" + +