From 881f7742ac81c39da4419d2267c8d8c7b6865270 Mon Sep 17 00:00:00 2001 From: agustin18 Date: Wed, 23 Sep 2026 23:08:46 +0000 Subject: [PATCH 1/4] fix(responses): wrap converted-stream bridge responses into synthetic stream --- .../handler.py | 32 ++++++- litellm/responses/streaming_iterator.py | 46 +++++++--- .../test_handler.py | 92 +++++++++++++++++++ 3 files changed, 157 insertions(+), 13 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index 505b5b09433..8ba1b8b1cb1 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -13,7 +13,11 @@ from litellm.responses.litellm_completion_transformation.streaming_iterator impo from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) -from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator +from litellm.responses.streaming_iterator import ( + BaseResponsesAPIStreamingIterator, + MockResponsesAPIStreamingIterator, +) +from litellm.types.integrations.custom_logger import converted_stream_requested from litellm.types.llms.openai import ( ResponseInputParam, ResponsesAPIOptionalRequestParams, @@ -81,6 +85,19 @@ class LiteLLMCompletionTransformationHandler: ) ) + converted_stream: Final = ( + converted_stream_requested(kwargs) + or converted_stream_requested(litellm_completion_request) + ) + if converted_stream and not kwargs.get("_agentic_loop_depth"): + return MockResponsesAPIStreamingIterator( + model=model, + transformed_response=responses_api_response, + logging_obj=kwargs.get("logging_obj"), + custom_llm_provider=custom_llm_provider, + litellm_metadata=kwargs.get("litellm_metadata", {}), + ) + return responses_api_response elif isinstance(litellm_completion_response, litellm.CustomStreamWrapper): @@ -126,6 +143,19 @@ class LiteLLMCompletionTransformationHandler: ) ) + converted_stream: Final = ( + converted_stream_requested(kwargs) + or converted_stream_requested(litellm_completion_request) + ) + if converted_stream and not kwargs.get("_agentic_loop_depth"): + return MockResponsesAPIStreamingIterator( + model=litellm_completion_request.get("model") or "", + transformed_response=responses_api_response, + logging_obj=kwargs.get("logging_obj"), + custom_llm_provider=litellm_completion_request.get("custom_llm_provider"), + litellm_metadata=kwargs.get("litellm_metadata", {}), + ) + return responses_api_response elif isinstance(litellm_completion_response, litellm.CustomStreamWrapper): diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 9f537d24eaa..cb550f585d1 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -312,9 +312,15 @@ class BaseResponsesAPIStreamingIterator: # set hidden params for response headers (e.g., x-litellm-model-id) # This matches the stream wrapper in litellm/litellm_core_utils/streaming_handler.py + _model_call_details: Final = getattr(self.logging_obj, "model_call_details", None) + _optional_params: Final = ( + _typed_gets_litellm_params(_model_call_details.get)("litellm_params", {}) + if isinstance(_model_call_details, dict) + else {} + ) _api_base: Final = get_api_base( model=model or "", - optional_params=_typed_gets_litellm_params(self.logging_obj.model_call_details.get)("litellm_params", {}), + optional_params=_optional_params, ) self._hidden_params: dict[str, object] = { "model_id": _model_id_from_metadata(litellm_metadata), @@ -512,7 +518,7 @@ class BaseResponsesAPIStreamingIterator: raise def _log_completed_response(self, *, is_async: bool) -> None: - if self._completed_response_logged: + if self._completed_response_logged or self.logging_obj is None: return self._completed_response_logged = True @@ -1116,22 +1122,38 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): def __init__( self, - response: httpx.Response, - model: str, - responses_api_provider_config: BaseResponsesAPIConfig, - logging_obj: LiteLLMLoggingObj, + response: httpx.Response | None = None, + model: str = "", + responses_api_provider_config: BaseResponsesAPIConfig | None = None, + logging_obj: LiteLLMLoggingObj | None = None, litellm_metadata: dict[str, object] | None = None, custom_llm_provider: str | None = None, request_data: dict[str, object] | None = None, call_type: str | None = None, + transformed_response: ResponsesAPIResponse | None = None, ): - transformed: Final = responses_api_provider_config.transform_response_api_response( - model=model, - raw_response=response, - logging_obj=logging_obj, - ) + if transformed_response is not None: + transformed: Final = transformed_response + elif responses_api_provider_config is not None and response is not None: + transformed: Final = responses_api_provider_config.transform_response_api_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + elif response is not None: + from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig + + transformed: Final = OpenAIResponsesAPIConfig().transform_response_api_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + else: + raise ValueError( + "Either transformed_response or response must be provided to MockResponsesAPIStreamingIterator" + ) super().__init__( - response=httpx.Response(200), + response=response or httpx.Response(200), model=model, responses_api_provider_config=None, logging_obj=logging_obj, diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index bb374b90f4e..7b8d9652990 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -223,3 +223,95 @@ async def test_bridged_follow_up_turn_keeps_the_addressed_response_id_off_the_pr ) assert isinstance(response, ResponsesAPIResponse) assert [item.type for item in response.output] == ["message"] + + +@pytest.mark.parametrize( + "converted_stream_flag", + [ + "_websearch_interception_converted_stream", + "_code_interpreter_interception_converted_stream", + "_headroom_interception_converted_stream", + ], +) +@pytest.mark.asyncio +async def test_async_fallback_wraps_converted_stream_as_synthetic_stream(converted_stream_flag): + from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator + from litellm.types.utils import Choices, Message, ModelResponse + + handler = LiteLLMCompletionTransformationHandler() + + async def fake_acompletion(**kwargs): + return ModelResponse( + id="chatcmpl-test", + created=1, + model="gpt-4o", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message(content="Search summary", role="assistant"), + ) + ], + ) + + with patch("litellm.acompletion", fake_acompletion): + response = await handler.response_api_handler( + model="gpt-4o", + input="Search for the weather", + responses_api_request={}, + custom_llm_provider="hosted_vllm", + _is_async=True, + **{converted_stream_flag: True}, + ) + + assert isinstance(response, BaseResponsesAPIStreamingIterator) + events = [event async for event in response] + assert len(events) > 0 + assert getattr(events[-1], "type", None) == "response.completed" + + +@pytest.mark.parametrize( + "converted_stream_flag", + [ + "_websearch_interception_converted_stream", + "_code_interpreter_interception_converted_stream", + "_headroom_interception_converted_stream", + ], +) +def test_sync_fallback_wraps_converted_stream_as_synthetic_stream(converted_stream_flag): + from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator + from litellm.types.utils import Choices, Message, ModelResponse + + handler = LiteLLMCompletionTransformationHandler() + + def fake_completion(**kwargs): + return ModelResponse( + id="chatcmpl-test-sync", + created=1, + model="gpt-4o", + object="chat.completion", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message(content="Sync search summary", role="assistant"), + ) + ], + ) + + with patch("litellm.completion", fake_completion): + response = handler.response_api_handler( + model="gpt-4o", + input="Search for the weather", + responses_api_request={}, + custom_llm_provider="hosted_vllm", + _is_async=False, + **{converted_stream_flag: True}, + ) + + assert isinstance(response, BaseResponsesAPIStreamingIterator) + events = list(response) + assert len(events) > 0 + assert getattr(events[-1], "type", None) == "response.completed" + From 562f5f8cc2dcbf38f2a62578b4b8c4762b8493eb Mon Sep 17 00:00:00 2001 From: agustin18 Date: Wed, 23 Sep 2026 23:13:29 +0000 Subject: [PATCH 2/4] style: format with ruff --- .../litellm_completion_transformation/handler.py | 10 ++++------ .../litellm_completion_transformation/test_handler.py | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index 8ba1b8b1cb1..67bd138f184 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -85,9 +85,8 @@ class LiteLLMCompletionTransformationHandler: ) ) - converted_stream: Final = ( - converted_stream_requested(kwargs) - or converted_stream_requested(litellm_completion_request) + converted_stream: Final = converted_stream_requested(kwargs) or converted_stream_requested( + litellm_completion_request ) if converted_stream and not kwargs.get("_agentic_loop_depth"): return MockResponsesAPIStreamingIterator( @@ -143,9 +142,8 @@ class LiteLLMCompletionTransformationHandler: ) ) - converted_stream: Final = ( - converted_stream_requested(kwargs) - or converted_stream_requested(litellm_completion_request) + converted_stream: Final = converted_stream_requested(kwargs) or converted_stream_requested( + litellm_completion_request ) if converted_stream and not kwargs.get("_agentic_loop_depth"): return MockResponsesAPIStreamingIterator( diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index 7b8d9652990..cfa6ac563f2 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -314,4 +314,3 @@ def test_sync_fallback_wraps_converted_stream_as_synthetic_stream(converted_stre events = list(response) assert len(events) > 0 assert getattr(events[-1], "type", None) == "response.completed" - From eac6ab293ffec687f64d71fef439f03f41770b42 Mon Sep 17 00:00:00 2001 From: agustin18 Date: Thu, 24 Sep 2026 00:24:35 +0000 Subject: [PATCH 3/4] fix(responses): ensure 100% test coverage and satisfy type-discipline budget --- .../handler.py | 4 +-- litellm/responses/streaming_iterator.py | 14 ++------ .../test_handler.py | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/litellm/responses/litellm_completion_transformation/handler.py b/litellm/responses/litellm_completion_transformation/handler.py index 67bd138f184..7f6b7efb955 100644 --- a/litellm/responses/litellm_completion_transformation/handler.py +++ b/litellm/responses/litellm_completion_transformation/handler.py @@ -94,7 +94,7 @@ class LiteLLMCompletionTransformationHandler: transformed_response=responses_api_response, logging_obj=kwargs.get("logging_obj"), custom_llm_provider=custom_llm_provider, - litellm_metadata=kwargs.get("litellm_metadata", {}), + litellm_metadata=kwargs.get("litellm_metadata"), ) return responses_api_response @@ -151,7 +151,7 @@ class LiteLLMCompletionTransformationHandler: transformed_response=responses_api_response, logging_obj=kwargs.get("logging_obj"), custom_llm_provider=litellm_completion_request.get("custom_llm_provider"), - litellm_metadata=kwargs.get("litellm_metadata", {}), + litellm_metadata=kwargs.get("litellm_metadata"), ) return responses_api_response diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index cb550f585d1..d1f76eff501 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -314,9 +314,9 @@ class BaseResponsesAPIStreamingIterator: # This matches the stream wrapper in litellm/litellm_core_utils/streaming_handler.py _model_call_details: Final = getattr(self.logging_obj, "model_call_details", None) _optional_params: Final = ( - _typed_gets_litellm_params(_model_call_details.get)("litellm_params", {}) + _typed_gets_litellm_params(_model_call_details.get)("litellm_params", {}) # mutable-ok: fallback dict if isinstance(_model_call_details, dict) - else {} + else {} # mutable-ok: fallback empty mapping ) _api_base: Final = get_api_base( model=model or "", @@ -1140,17 +1140,9 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): raw_response=response, logging_obj=logging_obj, ) - elif response is not None: - from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig - - transformed: Final = OpenAIResponsesAPIConfig().transform_response_api_response( - model=model, - raw_response=response, - logging_obj=logging_obj, - ) else: raise ValueError( - "Either transformed_response or response must be provided to MockResponsesAPIStreamingIterator" + "Either transformed_response or both responses_api_provider_config and response must be provided to MockResponsesAPIStreamingIterator" ) super().__init__( response=response or httpx.Response(200), diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index cfa6ac563f2..2c9ecb1648e 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -314,3 +314,39 @@ def test_sync_fallback_wraps_converted_stream_as_synthetic_stream(converted_stre events = list(response) assert len(events) > 0 assert getattr(events[-1], "type", None) == "response.completed" + + +def test_mock_responses_streaming_iterator_validation_and_config_branches(): + import httpx + + from litellm.responses.streaming_iterator import MockResponsesAPIStreamingIterator + from litellm.types.llms.openai import ResponsesAPIResponse + + with pytest.raises(ValueError, match="Either transformed_response or both"): + MockResponsesAPIStreamingIterator() + + class _MockConfig: + def transform_response_api_response(self, **kwargs): + return ResponsesAPIResponse( + id="resp_cfg_test", + created_at=1, + status="completed", + model="test-model", + object="response", + output=[], + ) + + class _MockLoggingObj: + def __init__(self): + self.model_call_details = {"litellm_params": {"api_key": "fake"}} + + logging_obj = _MockLoggingObj() + iterator = MockResponsesAPIStreamingIterator( + response=httpx.Response(200), + model="gpt-4o", + responses_api_provider_config=_MockConfig(), + logging_obj=logging_obj, + ) + events = list(iterator) + assert len(events) > 0 + assert getattr(events[-1], "type", None) == "response.completed" From 259821fbaace8cdd443266930a87e0e677c09e9c Mon Sep 17 00:00:00 2001 From: agustin18 Date: Thu, 24 Sep 2026 04:30:45 +0000 Subject: [PATCH 4/4] fix(responses): resolve basedpyright final assignment and mock logging handler stubs --- litellm/responses/streaming_iterator.py | 21 ++++++++++++------- .../test_handler.py | 6 ++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index d1f76eff501..b44a54b8fca 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -1132,15 +1132,20 @@ class MockResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): call_type: str | None = None, transformed_response: ResponsesAPIResponse | None = None, ): - if transformed_response is not None: - transformed: Final = transformed_response - elif responses_api_provider_config is not None and response is not None: - transformed: Final = responses_api_provider_config.transform_response_api_response( - model=model, - raw_response=response, - logging_obj=logging_obj, + transformed: Final[ResponsesAPIResponse | None] = ( + transformed_response + if transformed_response is not None + else ( + responses_api_provider_config.transform_response_api_response( + model=model, + raw_response=response, + logging_obj=logging_obj, + ) + if responses_api_provider_config is not None and response is not None + else None ) - else: + ) + if transformed is None: raise ValueError( "Either transformed_response or both responses_api_provider_config and response must be provided to MockResponsesAPIStreamingIterator" ) diff --git a/tests/unit/responses/litellm_completion_transformation/test_handler.py b/tests/unit/responses/litellm_completion_transformation/test_handler.py index 2c9ecb1648e..d8633f59ac0 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_handler.py +++ b/tests/unit/responses/litellm_completion_transformation/test_handler.py @@ -340,6 +340,12 @@ def test_mock_responses_streaming_iterator_validation_and_config_branches(): def __init__(self): self.model_call_details = {"litellm_params": {"api_key": "fake"}} + async def async_success_handler(self, *args, **kwargs): + pass + + def success_handler(self, *args, **kwargs): + pass + logging_obj = _MockLoggingObj() iterator = MockResponsesAPIStreamingIterator( response=httpx.Response(200),