From 2ae5e6f1a4d978c914b06f167de8fcf0d0952a28 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 07:15:27 +0000 Subject: [PATCH] fix(responses): preserve tool output adjacency --- .../transformation.py | 19 +++++++++ .../test_litellm_completion_responses.py | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 6b1ca3564e3..7e75696890c 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -402,6 +402,25 @@ class LiteLLMCompletionResponsesConfig: ) ) + if ( + messages + and len(chat_completion_messages) == 1 + and not LiteLLMCompletionResponsesConfig._is_input_item_function_call(input_item=_input) + and not LiteLLMCompletionResponsesConfig._is_input_item_tool_call_output(input_item=_input) + ): + last_msg = messages[-1] + new_msg = chat_completion_messages[0] + if ( + isinstance(last_msg, dict) + and isinstance(new_msg, dict) + and last_msg.get("role") == "assistant" + and last_msg.get("tool_calls") + and last_msg.get("content") is None + and new_msg.get("role") == "assistant" + ): + last_msg["content"] = new_msg.get("content") + continue + if LiteLLMCompletionResponsesConfig._is_input_item_function_call(input_item=_input): call_id_raw = _input.get("call_id") or _input.get("id") or "" if call_id_raw: diff --git a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py index d8e3f495ced..626f3d9b5da 100644 --- a/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/test_litellm/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -2370,6 +2370,47 @@ class TestStreamingIDConsistency: len(tool_messages) == 2 ), f"Expected 2 tool messages, got {len(tool_messages)}" + def test_assistant_message_between_function_call_and_output_is_merged(self): + input_items = [ + {"type": "message", "role": "user", "content": "Run pwd"}, + { + "type": "function_call", + "call_id": "call_01", + "name": "exec_command", + "arguments": '{"cmd":"pwd"}', + }, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "I'll inspect the working directory.", + } + ], + }, + { + "type": "function_call_output", + "call_id": "call_01", + "output": "/workspace", + }, + ] + + messages = LiteLLMCompletionResponsesConfig._transform_response_input_param_to_chat_completion_message( + input=input_items + ) + + assert [message.get("role") for message in messages] == [ + "user", + "assistant", + "tool", + ] + assert messages[1].get("content") == [ + {"type": "text", "text": "I'll inspect the working directory."} + ] + assert messages[1].get("tool_calls")[0].get("id") == "call_01" + assert messages[2].get("tool_call_id") == "call_01" + def test_single_tool_call_still_works_after_merge_fix(self): """ Ensure the parallel-tool-call merging fix does not break the existing