fix(streaming): exempt baseten/vllm EOF and emit mock finish_reason

Providers that never record stream finish_reason keep successful EOF.
Mock streams now send a terminal stop chunk so openai-compatible
exhaustion still works. Drop helper/test docstrings per CLAUDE.md.
This commit is contained in:
lei_lei 2026-09-09 03:05:14 +00:00
parent a97ea2c5d0
commit 8c8abcd94d
3 changed files with 221 additions and 437 deletions

View file

@ -1859,18 +1859,15 @@ class CustomStreamWrapper:
if self.logging_obj._is_sync_litellm_request(litellm_params):
self.logging_obj.success_handler(processed_chunk, None, None, cache_hit)
_PROVIDERS_WITHOUT_STREAM_FINISH_REASON = frozenset({"baseten", "vllm"})
def _has_provider_finish_reason(self) -> bool:
"""True when the provider (or an intermittent mid-stream reason) supplied a terminal finish_reason."""
return self.received_finish_reason is not None or self.intermittent_finish_reason is not None
def _should_require_provider_finish_reason(self) -> bool:
return self.custom_llm_provider not in self._PROVIDERS_WITHOUT_STREAM_FINISH_REASON
def _raise_incomplete_stream_without_finish_reason(self) -> "NoReturn":
"""
OpenAI/Azure-compatible chat streams that end without any provider finish_reason
must not be labeled as a successful completion (finish_reason="stop"). Raise a
MidStreamFallbackError so callers can retry/fallback, while retaining partial
content via generated_content / failure usage recovery.
"""
message = (
"Stream ended without a finish_reason from the provider. "
"Partial content was received but the response was not successfully completed."
@ -2092,7 +2089,7 @@ class CustomStreamWrapper:
self._restore_consumer_correlation_context()
raise # Re-raise StopIteration
else:
if not self._has_provider_finish_reason():
if self._should_require_provider_finish_reason() and not self._has_provider_finish_reason():
self._raise_incomplete_stream_without_finish_reason()
self.sent_last_chunk = True
processed_chunk: Final = self.finish_reason_handler()
@ -2356,7 +2353,7 @@ class CustomStreamWrapper:
self._restore_consumer_correlation_context()
raise StopAsyncIteration # Re-raise StopIteration
else:
if not self._has_provider_finish_reason():
if self._should_require_provider_finish_reason() and not self._has_provider_finish_reason():
self._raise_incomplete_stream_without_finish_reason()
self.sent_last_chunk = True
processed_chunk: Final = self.finish_reason_handler()

View file

@ -6975,6 +6975,7 @@ def mock_completion_streaming_obj(model_response, mock_response, model, n: int |
completion_obj = Delta(role="assistant", content=mock_response[i : i + 3])
if n is None:
model_response.choices[0].delta = completion_obj
model_response.choices[0].finish_reason = None
else:
_all_choices = []
for j in range(n):
@ -6985,6 +6986,19 @@ def mock_completion_streaming_obj(model_response, mock_response, model, n: int |
_all_choices.append(_streaming_choice)
model_response.choices = _all_choices
yield model_response
if n is None:
model_response.choices[0].delta = Delta(role="assistant", content=None)
model_response.choices[0].finish_reason = "stop"
else:
model_response.choices = [
litellm.utils.StreamingChoices(
index=j,
delta=litellm.utils.Delta(role="assistant", content=None),
finish_reason="stop",
)
for j in range(n)
]
yield model_response
async def async_mock_completion_streaming_obj(
@ -7002,6 +7016,7 @@ async def async_mock_completion_streaming_obj(
completion_obj = Delta(role="assistant", content=mock_response[i : i + 3])
if n is None:
model_response.choices[0].delta = completion_obj
model_response.choices[0].finish_reason = None
else:
_all_choices = []
for j in range(n):
@ -7012,6 +7027,19 @@ async def async_mock_completion_streaming_obj(
_all_choices.append(_streaming_choice)
model_response.choices = _all_choices
yield model_response
if n is None:
model_response.choices[0].delta = Delta(role="assistant", content=None)
model_response.choices[0].finish_reason = "stop"
else:
model_response.choices = [
litellm.utils.StreamingChoices(
index=j,
delta=litellm.utils.Delta(role="assistant", content=None),
finish_reason="stop",
)
for j in range(n)
]
yield model_response
########## Reading Config File ############################