fix: preserve assistant text content alongside tool_calls in Responses API conversion

Assistant messages with both `content` and `tool_calls` silently dropped
the text content during chat-completion-to-Responses-API conversion. The
elif branch handling tool_calls never checked the content field, so text
like "Let me check that." was lost. Now emits a message item for the text
before the function_call items.
This commit is contained in:
sohumt123 2026-04-05 19:53:21 -04:00
parent d251238bd7
commit 4b2be80f1f
2 changed files with 128 additions and 0 deletions

View file

@ -260,6 +260,17 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
for r_item in _get_reasoning_items(msg):
input_items.append(_reasoning_item_to_response_input(r_item))
# Preserve text content when present alongside tool_calls
if content:
input_items.append(
{
"type": "message",
"role": "assistant",
"content": self._convert_content_to_responses_format(
content, "assistant"
),
}
)
for tool_call in tool_calls:
function = tool_call.get("function")
if function:

View file

@ -2431,3 +2431,120 @@ def test_reasoning_items_streaming_emitted_on_response_completed():
ri["encrypted_content"] == encrypted
), "encrypted_content must be preserved in streaming"
assert ri["summary"][0]["text"] == summary_text
def test_convert_chat_completion_messages_to_responses_api_assistant_content_with_tool_calls():
"""
Test that assistant messages with both text content and tool_calls preserve the text content.
When an OpenAI model returns an assistant message like:
{"role": "assistant", "content": "Let me check that.", "tool_calls": [...]}
the text "Let me check that." must appear as a message item before the function_call items.
"""
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler = LiteLLMResponsesTransformationHandler()
# Case 1: String content + tool_calls — content must be preserved
messages = [
{
"role": "assistant",
"content": "I'll check both cities for you.",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "SF"}',
},
},
],
},
]
items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
# Should have a message item and a function_call item
assert len(items) == 2, f"Expected 2 items, got {len(items)}: {items}"
message_item = items[0]
assert message_item["type"] == "message", f"Expected first item type 'message', got '{message_item['type']}'"
assert message_item["role"] == "assistant"
assert any(
part.get("text") == "I'll check both cities for you."
for part in message_item["content"]
), f"Text content not found in message item: {message_item['content']}"
function_call_item = items[1]
assert function_call_item["type"] == "function_call"
assert function_call_item["call_id"] == "call_1"
assert function_call_item["name"] == "get_weather"
def test_convert_chat_completion_messages_to_responses_api_none_content_with_tool_calls():
"""
Test that assistant messages with content=None and tool_calls do not emit a message item.
This preserves the existing behavior.
"""
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler = LiteLLMResponsesTransformationHandler()
messages = [
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "NYC"}',
},
},
],
},
]
items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
# Should only have the function_call item, no message item
assert len(items) == 1, f"Expected 1 item, got {len(items)}: {items}"
assert items[0]["type"] == "function_call"
def test_convert_chat_completion_messages_to_responses_api_empty_content_with_tool_calls():
"""
Test that assistant messages with content="" and tool_calls do not emit an empty message item.
"""
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler = LiteLLMResponsesTransformationHandler()
messages = [
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_xyz",
"type": "function",
"function": {
"name": "search",
"arguments": '{"q": "test"}',
},
},
],
},
]
items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
# Should only have the function_call item, no empty message item
assert len(items) == 1, f"Expected 1 item, got {len(items)}: {items}"
assert items[0]["type"] == "function_call"