From dd8b4b1d5460be74056becc8708f8494e67d8ca2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 12:08:47 +0000 Subject: [PATCH] fix(responses-bridge): latch a single chunk id across bridged streaming chunks --- .../transformation.py | 8 ++- ...responses_transformation_transformation.py | 72 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index aecb2552b53..dd646d0d9b1 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -1074,6 +1074,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): def __init__(self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False): super().__init__(streaming_response, sync_stream, json_mode) + self._chat_completion_id: Optional[str] = None def _handle_string_chunk( self, str_line: Union[str, "BaseModel"] @@ -1381,4 +1382,9 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator): ModelResponseStream: OpenAI-formatted streaming chunk """ verbose_logger.debug(f"Chat provider: transform_streaming_response called with chunk: {chunk}") - return OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(chunk) + parsed_chunk = OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(chunk) + if self._chat_completion_id is None and parsed_chunk.id: + self._chat_completion_id = parsed_chunk.id + elif self._chat_completion_id is not None: + parsed_chunk.id = self._chat_completion_id + return parsed_chunk 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 6a1de0586dd..8b758ea74be 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 @@ -2401,6 +2401,78 @@ def test_parallel_tool_calls_comprehensive_streaming_integration(): ) +def test_bridge_streaming_chunks_share_single_id(): + """Regression test for #32607. + + When a chat completions request is bridged to the Responses API and the + streaming events are translated back, every emitted chunk must share one + stable id. Previously each ModelResponseStream got a fresh + "chatcmpl-", which broke clients that group tool-call deltas by + chunk.id (splitting a single tool call across multiple messages). + """ + from litellm.completion_extras.litellm_responses_transformation.transformation import ( + OpenAiResponsesToChatCompletionStreamIterator, + ) + + chunks = [ + {"type": "response.created", "response": {"id": "resp_001"}}, + { + "type": "response.output_item.added", + "output_index": 0, + "item": { + "type": "function_call", + "id": "fc_001", + "call_id": "call_1", + "name": "get_weather", + }, + }, + { + "type": "response.function_call_arguments.delta", + "output_index": 0, + "delta": '{"city":"Amsterdam"}', + }, + { + "type": "response.output_item.done", + "output_index": 0, + "item": { + "type": "function_call", + "id": "fc_001", + "call_id": "call_1", + "name": "get_weather", + "arguments": '{"city":"Amsterdam"}', + }, + }, + { + "type": "response.completed", + "response": { + "id": "resp_001", + "status": "completed", + "output": [ + { + "type": "function_call", + "call_id": "call_1", + "name": "get_weather", + "arguments": '{"city":"Amsterdam"}', + } + ], + }, + }, + ] + + iterator = OpenAiResponsesToChatCompletionStreamIterator( + streaming_response=None, sync_stream=True + ) + ids = [iterator.chunk_parser(chunk).id for chunk in chunks] + + assert len(set(ids)) == 1, ( + f"All bridged streaming chunks must share one id, got {len(set(ids))} " + f"distinct ids: {set(ids)}" + ) + assert ids[0] and ids[0].startswith("chatcmpl-"), ( + f"Bridged chunk id must keep the chat completion prefix, got {ids[0]!r}" + ) + + def test_map_optional_params_preserves_reasoning_summary(): """Test that reasoning_effort dict with summary field is preserved.