diff --git a/litellm/llms/oci/chat/transformation.py b/litellm/llms/oci/chat/transformation.py index 915d2029af..3be373ca5e 100644 --- a/litellm/llms/oci/chat/transformation.py +++ b/litellm/llms/oci/chat/transformation.py @@ -772,7 +772,14 @@ def adapt_messages_to_generic_oci_standard( tool_calls = message.get("tool_calls") tool_call_id = message.get("tool_call_id") - if role in ["system", "user", "assistant"] and content is not None: + if role == "assistant" and tool_calls is not None: + if not isinstance(tool_calls, list): + raise Exception("Prop `tool_calls` must be a list of tool calls") + new_messages.append( + adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls) + ) + + elif role in ["system", "user", "assistant"] and content is not None: if not isinstance(content, (str, list)): raise Exception( "Prop `content` must be a string or a list of content items" @@ -781,13 +788,6 @@ def adapt_messages_to_generic_oci_standard( adapt_messages_to_generic_oci_standard_content_message(role, content) ) - elif role == "assistant" and tool_calls is not None: - if not isinstance(tool_calls, list): - raise Exception("Prop `tool_calls` must be a list of tool calls") - new_messages.append( - adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls) - ) - elif role == "tool": if not isinstance(tool_call_id, str): raise Exception("Prop `tool_call_id` is required and must be a string") diff --git a/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation_for_14158.py b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation_for_14158.py new file mode 100644 index 0000000000..950c1fcb4c --- /dev/null +++ b/tests/test_litellm/llms/oci/chat/test_oci_chat_transformation_for_14158.py @@ -0,0 +1,229 @@ +import pytest +from litellm.llms.oci.chat.transformation import adapt_messages_to_generic_oci_standard + +def test_adapt_messages_with_empty_content_and_tool_calls(): + """Test that assistant messages with empty content and tool_calls are processed correctly.""" + # Arrange + messages_with_empty_content = [ + {"role": "user", "content": "Tell me the weather in Tokyo."}, + { + "role": "assistant", + "content": "", # Empty string + "tool_calls": [ + { + "id": "call_test_empty", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city": "Tokyo"}' + } + } + ] + }, + { + "role": "tool", + "content": '{"weather": "Sunny", "temperature": "25°C"}', + "tool_call_id": "call_test_empty" + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages_with_empty_content) + + # Assert + assert len(result) == 3 + + # Check user message + assert result[0].role == "USER" + assert result[0].content[0].type == "TEXT" + assert result[0].content[0].text == "Tell me the weather in Tokyo." + + # Check assistant message with tool_calls (should prioritize tool_calls over empty content) + assert result[1].role == "ASSISTANT" + assert result[1].toolCalls is not None + assert len(result[1].toolCalls) == 1 + assert result[1].toolCalls[0].id == "call_test_empty" + assert result[1].toolCalls[0].name == "get_weather" + + # Check tool response message + assert result[2].role == "TOOL" # Tool responses have TOOL role, not USER + assert result[2].content[0].type == "TEXT" + assert "weather" in result[2].content[0].text + assert result[2].toolCallId == "call_test_empty" # Tool call ID is in separate field + +def test_adapt_messages_with_none_content_and_tool_calls(): + """Test that assistant messages with None content and tool_calls are processed correctly.""" + # Arrange + messages_with_none_content = [ + {"role": "user", "content": "Tell me the weather in Tokyo."}, + { + "role": "assistant", + "content": None, # None value + "tool_calls": [ + { + "id": "call_test_none", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city": "Tokyo"}' + } + } + ] + }, + { + "role": "tool", + "content": '{"weather": "Sunny", "temperature": "25°C"}', + "tool_call_id": "call_test_none" + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages_with_none_content) + + # Assert + assert len(result) == 3 + + # Check assistant message prioritizes tool_calls over None content + assert result[1].role == "ASSISTANT" + assert result[1].toolCalls is not None + assert len(result[1].toolCalls) == 1 + assert result[1].toolCalls[0].id == "call_test_none" + +def test_adapt_messages_with_tool_calls_only(): + """Test that assistant messages with only tool_calls (no content field) are processed correctly.""" + # Arrange + messages_no_content = [ + {"role": "user", "content": "Tell me the weather in Tokyo."}, + { + "role": "assistant", + # No content field at all + "tool_calls": [ + { + "id": "call_test_no_content", + "type": "function", + "function": { + "name": "get_weather", + "arguments": '{"city": "Tokyo"}' + } + } + ] + }, + { + "role": "tool", + "content": '{"weather": "Sunny", "temperature": "25°C"}', + "tool_call_id": "call_test_no_content" + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages_no_content) + + # Assert + assert len(result) == 3 + + # Check assistant message processes tool_calls correctly + assert result[1].role == "ASSISTANT" + assert result[1].toolCalls is not None + assert len(result[1].toolCalls) == 1 + assert result[1].toolCalls[0].id == "call_test_no_content" + +def test_adapt_messages_with_content_only(): + """Test that assistant messages with only content (no tool_calls) are processed correctly.""" + # Arrange + messages_content_only = [ + {"role": "user", "content": "Hello"}, + { + "role": "assistant", + "content": "Hello! How can I help you today?" + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages_content_only) + + # Assert + assert len(result) == 2 + + # Check assistant message with content only + assert result[1].role == "ASSISTANT" + assert result[1].content[0].type == "TEXT" + assert result[1].content[0].text == "Hello! How can I help you today?" + assert result[1].toolCalls is None + +def test_adapt_messages_tool_id_tracking(): + """Test that tool call IDs are properly tracked for validation.""" + # Arrange + messages = [ + {"role": "user", "content": "Test"}, + { + "role": "assistant", + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": { + "name": "test_func", + "arguments": '{"param": "value"}' + } + } + ] + }, + { + "role": "tool", + "content": "Result", + "tool_call_id": "call_123" + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages) + + # Assert + # Tool call should be processed and ID should be available for validation + assert result[1].toolCalls[0].id == "call_123" + + # Tool response should reference the same ID + tool_response_text = result[2].content[0].text + # Tool response text is just the content, tool_call_id is separate + assert tool_response_text == "Result" # The actual content + assert result[2].toolCallId == "call_123" # Tool call ID is in separate field + +def test_adapt_messages_multiple_tool_calls(): + """Test that multiple tool calls in a single message are processed correctly.""" + # Arrange + messages = [ + {"role": "user", "content": "Test multiple tools"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "func1", + "arguments": '{"param": "value1"}' + } + }, + { + "id": "call_2", + "type": "function", + "function": { + "name": "func2", + "arguments": '{"param": "value2"}' + } + } + ] + } + ] + + # Act + result = adapt_messages_to_generic_oci_standard(messages) + + # Assert + assert len(result) == 2 + assert result[1].role == "ASSISTANT" + assert len(result[1].toolCalls) == 2 + assert result[1].toolCalls[0].id == "call_1" + assert result[1].toolCalls[1].id == "call_2" +