From c60b7b0a330dbaa226d3d06a8eb975f34f4c4a89 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 5 May 2026 04:49:42 +0000 Subject: [PATCH] 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 --- .../llms/chatgpt/responses/transformation.py | 6 ++- .../test_chatgpt_responses_transformation.py | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index c2060ceaad7..27c49ea658d 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -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() diff --git a/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index 22f65da21d3..90a1c24bada 100644 --- a/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/test_litellm/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -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", [