From c33718d8fedcbbbe94f16fd245c2714618097d46 Mon Sep 17 00:00:00 2001 From: Zhu Lei Date: Fri, 16 Oct 2026 21:19:05 +0800 Subject: [PATCH] fix(responses): skip empty message item on tool-only turns in the chat bridge --- .../transformation.py | 4 +- .../test_litellm_completion_responses.py | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 2e7fb6da194..abe55220007 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -3247,7 +3247,9 @@ class LiteLLMCompletionResponsesConfig: choice=choice, ) message_output_items.extend(image_generation_items) - elif choice.message.content is not None: + elif choice.message.content is not None and ( + choice.message.content or not getattr(choice.message, "tool_calls", None) + ): message_output_items.append( GenericResponseOutputItem( type="message", diff --git a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py index 19bbb6481fb..cf5125ba269 100644 --- a/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py +++ b/tests/unit/responses/litellm_completion_transformation/test_litellm_completion_responses.py @@ -660,6 +660,46 @@ class TestLiteLLMCompletionResponsesConfig: assert "message" not in output_types assert "function_call" in output_types + def test_tool_call_only_response_with_empty_string_content_emits_no_message_item(self): + """Providers that return content "" instead of None for a tool-only turn + must also emit no message item, matching the streaming path which drops + falsy final content and native OpenAI tool-only turns.""" + chat_completion_response = ModelResponse( + id="test-response-id", + created=1234567890, + model="claude-sonnet-4-5", + object="chat.completion", + choices=[ + Choices( + finish_reason="tool_calls", + index=0, + message=Message( + content="", + role="assistant", + tool_calls=[ + ChatCompletionMessageToolCall( + id="toolu_01OnlyToolCall", + type="function", + function=Function(name="get_weather", arguments='{"city": "SF"}'), + ) + ], + ), + ) + ], + ) + + responses_api_response = ( + LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="what's the weather in SF?", + responses_api_request={}, + chat_completion_response=chat_completion_response, + ) + ) + + output_types = [item.type for item in responses_api_response.output] + assert "message" not in output_types + assert "function_call" in output_types + def test_content_bearing_response_still_emits_message_item(self): """Turns with real text content must keep their message output item.""" chat_completion_response = ModelResponse(