diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 66acd933416..f85f50b7570 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -133,7 +133,9 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): ) completed_response = None + completed_response_payload = None error_message = None + output_text_parts = [] for chunk in body_text.splitlines(): stripped_chunk = CustomStreamWrapper._strip_sse_data_from_chunk(chunk) if not stripped_chunk: @@ -150,10 +152,16 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): if not isinstance(parsed_chunk, dict): continue event_type = parsed_chunk.get("type") + if event_type == ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA: + content_part = parsed_chunk.get("delta", None) + if isinstance(content_part, str) and content_part: + output_text_parts.append(content_part) + continue if event_type == ResponsesAPIStreamEvents.RESPONSE_COMPLETED: response_payload = parsed_chunk.get("response") if isinstance(response_payload, dict): response_payload = dict(response_payload) + completed_response_payload = response_payload if "created_at" in response_payload: response_payload["created_at"] = _safe_convert_created_field( response_payload["created_at"] @@ -178,6 +186,34 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): else: error_message = str(error_obj) + if ( + completed_response_payload is not None + and not completed_response_payload.get("output") + and len(output_text_parts) > 0 + ): + completed_response_payload["output"] = [ + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "".join(output_text_parts), + } + ], + } + ] + if "created_at" in completed_response_payload: + completed_response_payload["created_at"] = _safe_convert_created_field( + completed_response_payload["created_at"] + ) + try: + completed_response = ResponsesAPIResponse(**completed_response_payload) + except Exception: + completed_response = ResponsesAPIResponse.model_construct( + **completed_response_payload + ) + if completed_response is None: raise OpenAIError( message=error_message or raw_response.text, 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 2498946bb5c..70707c95de9 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 @@ -201,3 +201,35 @@ class TestChatGPTResponsesAPITransformation: ) assert parsed.output_text == "Hello!" + + def test_chatgpt_non_stream_sse_response_parsing_empty_completed_output(self): + config = ChatGPTResponsesAPIConfig() + response_payload = { + "id": "resp_test", + "object": "response", + "created_at": 1700000000, + "status": "completed", + "model": "gpt-5.4", + "output": [], + } + sse_body = "\n".join( + [ + f"data: {json.dumps({'type': 'response.output_text.delta', 'delta': 'Hel'})}", + f"data: {json.dumps({'type': 'response.output_text.delta', 'delta': 'lo!'})}", + f"data: {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 == "Hello!"