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:
mateo-berri 2026-05-05 04:49:42 +00:00
parent 84df63c76a
commit c60b7b0a33
No known key found for this signature in database
2 changed files with 47 additions and 1 deletions

View file

@ -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()

View file

@ -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",
[