mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(proxy): end failed responses streams with [DONE]
Emit data: [DONE] after event: response.failed, and after a late failure when a terminal event already went out, so OpenAI SDK clients see the same stream end as a completed response. Restore the lazy OpenAPI snapshot to its Python 3.12 rendering, which is what CI regenerates.
This commit is contained in:
parent
df1b3c849b
commit
eb96d885ce
4 changed files with 9 additions and 4 deletions
|
|
@ -19394,7 +19394,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"description": "\nUnified rate-limit error.\n\nEvery rate-limit condition surfaced by litellm \u2014 whether it originated from\nan upstream LLM provider, a vendor batch endpoint, or one of litellm's own\nproxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\nmax-iterations, etc.) \u2014 is raised as an instance of this class.\n\nThe :attr:`category` attribute lets callers distinguish the source. See\n:class:`RateLimitErrorCategory` for the available values.\n"
|
||||
"description": "\n Unified rate-limit error.\n\n Every rate-limit condition surfaced by litellm \u2014 whether it originated from\n an upstream LLM provider, a vendor batch endpoint, or one of litellm's own\n proxy-side limiters (parallel-requests, dynamic-rate, batch-rate, budget,\n max-iterations, etc.) \u2014 is raised as an instance of this class.\n\n The :attr:`category` attribute lets callers distinguish the source. See\n :class:`RateLimitErrorCategory` for the available values.\n "
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
|
|
|
|||
|
|
@ -8865,6 +8865,7 @@ def _format_streaming_sse_chunk(chunk: str | bytes) -> str | bytes:
|
|||
|
||||
|
||||
_SSE_FRAME_DELIMITERS: Final = ("\r\n\r\n", "\n\n", "\r\r")
|
||||
_OPENAI_STREAM_DONE_FRAME: Final = "data: [DONE]\n\n"
|
||||
_MAX_RAW_SSE_BUFFER_CHARS: Final = 8 * 1024 * 1024
|
||||
|
||||
|
||||
|
|
@ -9272,8 +9273,7 @@ async def async_data_generator(
|
|||
yield error_message
|
||||
# OpenAI-compatible streams terminate with data: [DONE]; Google GenAI (?alt=sse) does not.
|
||||
if not request_data.get("_litellm_skip_openai_stream_done"):
|
||||
done_message: Final = "[DONE]"
|
||||
yield f"data: {done_message}\n\n"
|
||||
yield _OPENAI_STREAM_DONE_FRAME
|
||||
except (asyncio.CancelledError, GeneratorExit):
|
||||
# Client disconnected mid-stream. CancelledError / GeneratorExit are
|
||||
# BaseException, so they bypass the success/failure logging callbacks
|
||||
|
|
@ -9303,6 +9303,8 @@ async def async_data_generator(
|
|||
error_frame: Final = error_state.format_failure(e)
|
||||
if error_frame is not None:
|
||||
yield error_frame
|
||||
if not request_data.get("_litellm_skip_openai_stream_done"):
|
||||
yield _OPENAI_STREAM_DONE_FRAME
|
||||
return
|
||||
if isinstance(e, HTTPException):
|
||||
raise e
|
||||
|
|
|
|||
|
|
@ -995,6 +995,8 @@ async def test_responses_stream_keeps_tool_deltas_and_only_emits_a_valid_termina
|
|||
for frame in event_frames
|
||||
)
|
||||
|
||||
assert decoded[-1] == "data: [DONE]\n\n"
|
||||
assert len(decoded) == len(event_frames) + 1
|
||||
assert payloads[0]["response"]["id"] == "resp_visible"
|
||||
assert payloads[1] == tool_delta.model_dump()
|
||||
assert len(payloads) == 3
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ async def test_streaming_upstream_errors_keep_the_client_protocol(
|
|||
assert result.status_code == 200, result.text
|
||||
assert message in result.text
|
||||
if path == "/v1/responses":
|
||||
assert frames[-1].startswith("event: response.failed\n"), result.text
|
||||
assert frames[-1] == "data: [DONE]", result.text
|
||||
assert frames[-2].startswith("event: response.failed\n"), result.text
|
||||
if partial:
|
||||
assert [event["type"] for event in events] == [
|
||||
"response.created", "response.output_item.added",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue