From 1656e7ea642769286a200b3e5395b87c66c54a04 Mon Sep 17 00:00:00 2001 From: PriyanJindal Date: Tue, 1 Sep 2026 15:22:35 -0700 Subject: [PATCH] fix(responses): propagate chat bridge stream errors --- .../transformation.py | 38 +++++++++++ ...responses_transformation_transformation.py | 67 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 7368de1e968..0ac2849f1ef 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -1368,6 +1368,21 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ) ] ) + elif event_type in (ResponsesAPIStreamEvents.RESPONSE_FAILED, ResponsesAPIStreamEvents.ERROR): + from litellm.responses.streaming_iterator import _error_event_fields, _status_code_for_error_fields + + error_payload: Final = ( + parsed_chunk.get("response", {}).get("error") + if event_type == ResponsesAPIStreamEvents.RESPONSE_FAILED + else parsed_chunk.get("error") + ) + error_message, error_type, error_code = _error_event_fields(error_payload) + raise litellm.APIError( + status_code=_status_code_for_error_fields(error_type, error_code), + message=error_message, + llm_provider="openai", + model="responses", + ) elif event_type == "response.output_item.added": # New output item added output_item = parsed_chunk.get("item", {}) @@ -1498,6 +1513,29 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ) else: raise ValueError(f"Chat provider: Invalid text delta {parsed_chunk}") + elif event_type == ResponsesAPIStreamEvents.REFUSAL_DELTA: + refusal: Final = parsed_chunk.get("delta") + if isinstance(refusal, str): + return ModelResponseStream( + choices=[ + StreamingChoices( + index=0, + delta=Delta(refusal=refusal), + finish_reason=None, + ) + ] + ) + raise ValueError(f"Chat provider: Invalid refusal delta {parsed_chunk}") + elif event_type == ResponsesAPIStreamEvents.REFUSAL_DONE: + return ModelResponseStream( + choices=[ + StreamingChoices( + index=0, + delta=Delta(), + finish_reason=None, + ) + ] + ) elif event_type == "response.reasoning_summary_text.delta": content_part = parsed_chunk.get("delta", None) if content_part: diff --git a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index 21b60d7a216..2115517aa0b 100644 --- a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -3796,6 +3796,73 @@ def test_response_incomplete_stream_event_without_details_defaults_to_length(): assert result.choices[0].finish_reason == "length" +@pytest.mark.parametrize( + ("event", "expected_status"), + [ + ( + { + "type": "response.failed", + "response": { + "error": { + "type": "server_error", + "code": "server_error", + "message": "generation failed", + } + }, + }, + 500, + ), + ( + { + "type": "error", + "error": { + "type": "invalid_request_error", + "code": "invalid_prompt", + "message": "prompt was rejected", + }, + }, + 400, + ), + ], +) +def test_response_stream_errors_raise_litellm_api_errors(event, expected_status): + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + OpenAiResponsesToChatCompletionStreamIterator, + ) + + iterator = OpenAiResponsesToChatCompletionStreamIterator( + streaming_response=iter([f"data: {json.dumps(event)}"]), sync_stream=True + ) + + with pytest.raises(litellm.APIError) as exc_info: + next(iterator) + + assert exc_info.value.status_code == expected_status + assert event.get("error", event.get("response", {}).get("error"))["message"] in str(exc_info.value) + + +def test_response_refusal_stream_events_preserve_refusal_delta_without_duplication(): + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + OpenAiResponsesToChatCompletionStreamIterator, + ) + + iterator = OpenAiResponsesToChatCompletionStreamIterator( + streaming_response=iter( + [ + 'data: {"type":"response.refusal.delta","delta":"I cannot help with that."}', + 'data: {"type":"response.refusal.done","refusal":"I cannot help with that."}', + ] + ), + sync_stream=True, + ) + + delta = next(iterator) + done = next(iterator) + + assert delta.choices[0].delta.get("refusal") == "I cannot help with that." + assert done.choices[0].delta.get("refusal") is None + + def test_assistant_message_with_tool_calls_keeps_its_content(): """Regression for https://github.com/BerriAI/litellm/issues/24985.