From e4f66c01a012c617ccc3a064a940ee2838bddd68 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Mon, 29 Jun 2026 15:14:45 +0530 Subject: [PATCH] fix: strip no-content chunks before stream_chunk_builder, keep usage-bearing ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove chunks where choices==[] AND usage is absent — these cause stream_chunk_builder to IndexError on chunks[-1]["choices"][0] and silently drop the partial billing record. Usage-bearing empty-choices chunks (OpenAI stream_options include_usage final chunk) are explicitly preserved so real provider token counts are not replaced by estimates. --- litellm/proxy/common_request_processing.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index ab16c0e9acc..e97387ff1af 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -2465,6 +2465,22 @@ class ProxyBaseLLMRequestProcessing: return elif not isinstance(first_chunk, str) and not hasattr(first_chunk, "choices"): return + # Remove pure no-content chunks (choices=[] and no usage payload) that can + # cause stream_chunk_builder to IndexError on chunks[-1]["choices"][0]. + # Usage-bearing empty-choices chunks (OpenAI stream_options include_usage) + # are intentionally kept so real token counts are not replaced by estimates. + def _is_discardable_chunk(c: Any) -> bool: + if isinstance(c, dict): + return c.get("choices") == [] and not c.get("usage") + return ( + hasattr(c, "choices") + and getattr(c, "choices", None) == [] + and not getattr(c, "usage", None) + ) + + chunks = [c for c in chunks if not _is_discardable_chunk(c)] + if not chunks: + return # Optimization, not a correctness guard: dispatch_success_handlers is the # authoritative de-dup via has_dispatched_final_stream_success. This just # skips the stream_chunk_builder assembly when completion already logged.