mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(chatgpt/responses): strip whitespace before parsing SSE chunks
_parse_sse_json_chunk in ChatGPTResponsesAPIConfig passed the raw chunk
directly to _strip_sse_data_from_chunk, which only matches the 'data:'
prefix at position 0. Chunks with leading whitespace (e.g. ' data: {...}')
were returned unchanged and silently failed JSON parsing, dropping the
contained event.
Mirror the existing fix in LiteLLMResponsesTransformationHandler._parse_raw_sse_chunk
by calling chunk.strip() before stripping the SSE prefix.
Adds a regression test using whitespace-padded data: lines and verifies
that the response.output_item.done payload is recovered into the final
ResponsesAPIResponse output.
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
84df63c76a
commit
c60b7b0a33
2 changed files with 47 additions and 1 deletions
|
|
@ -189,7 +189,11 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
return completed_response, error_message
|
||||
|
||||
def _parse_sse_json_chunk(self, chunk: str) -> Optional[Any]:
|
||||
stripped_chunk = CustomStreamWrapper._strip_sse_data_from_chunk(chunk)
|
||||
# Strip outer whitespace before removing the SSE `data:` prefix.
|
||||
# `_strip_sse_data_from_chunk` only matches the prefix at position 0,
|
||||
# so chunks with leading whitespace (e.g. ` data: {...}`) would
|
||||
# otherwise be returned unchanged and fail JSON parsing silently.
|
||||
stripped_chunk = CustomStreamWrapper._strip_sse_data_from_chunk(chunk.strip())
|
||||
if not stripped_chunk:
|
||||
return None
|
||||
stripped_chunk = stripped_chunk.strip()
|
||||
|
|
|
|||
|
|
@ -248,6 +248,48 @@ class TestChatGPTResponsesAPITransformation:
|
|||
|
||||
assert parsed.output_text == "Hello from stream!"
|
||||
|
||||
def test_chatgpt_non_stream_sse_recovers_whitespace_padded_chunks(self):
|
||||
"""Chunks with leading whitespace before `data:` must still parse.
|
||||
|
||||
`_strip_sse_data_from_chunk` only matches the prefix at position 0,
|
||||
so without an outer `.strip()` such chunks would fail JSON parsing
|
||||
and silently drop the contained event.
|
||||
"""
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
response_payload = {
|
||||
"id": "resp_test",
|
||||
"object": "response",
|
||||
"created_at": 1700000000,
|
||||
"status": "completed",
|
||||
"model": "gpt-5.4",
|
||||
"output": [],
|
||||
}
|
||||
streamed_output_item = {
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": [{"type": "output_text", "text": "Recovered from padded"}],
|
||||
}
|
||||
sse_body = "\n".join(
|
||||
[
|
||||
f" data: {json.dumps({'type': 'response.output_item.done', 'output_index': 0, 'item': streamed_output_item})} ",
|
||||
f"\tdata: {json.dumps({'type': 'response.completed', 'response': response_payload})}",
|
||||
"data: [DONE]",
|
||||
"",
|
||||
]
|
||||
)
|
||||
raw_response = httpx.Response(
|
||||
200, headers={"content-type": "text/event-stream"}, text=sse_body
|
||||
)
|
||||
logging_obj = MagicMock()
|
||||
|
||||
parsed = config.transform_response_api_response(
|
||||
model="chatgpt/gpt-5.4",
|
||||
raw_response=raw_response,
|
||||
logging_obj=logging_obj,
|
||||
)
|
||||
|
||||
assert parsed.output_text == "Recovered from padded"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_chunk",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue