diff --git a/litellm/llms/oci/chat/cohere.py b/litellm/llms/oci/chat/cohere.py index 39caebba03a..441c67d59d1 100644 --- a/litellm/llms/oci/chat/cohere.py +++ b/litellm/llms/oci/chat/cohere.py @@ -60,7 +60,11 @@ def adapt_messages_to_cohere_standard( ) -> List[CohereMessage]: """Build a Cohere ``chatHistory`` list from an OpenAI-format message array. - - All messages except the last are included (the last becomes ``message``). + - All messages except the *last user message* are included. The caller pulls + the last user message into the request's top-level ``message`` field, so + trailing tool results (the standard agentic continuation pattern) still + appear in ``chatHistory`` and reach the model. + - If no user message exists, every message is included (no slice). - Tool results are expressed as OCI ``CohereToolMessage.toolResults`` entries, with the originating call's name and parameters resolved from the preceding assistant message via a ``tool_call_id`` lookup. @@ -85,8 +89,22 @@ def adapt_messages_to_cohere_standard( parameters=params, ) + last_user_index = next( + ( + i + for i in range(len(messages) - 1, -1, -1) + if messages[i].get("role") == "user" + ), + None, + ) + history_source = ( + messages + if last_user_index is None + else [m for i, m in enumerate(messages) if i != last_user_index] + ) + chat_history: List[CohereMessage] = [] - for msg in messages[:-1]: + for msg in history_source: role = msg.get("role") content = _extract_text_content(msg.get("content")) diff --git a/tests/test_litellm/llms/oci/chat/test_oci_cohere_tool_calls.py b/tests/test_litellm/llms/oci/chat/test_oci_cohere_tool_calls.py index 4414d1ddd10..3ae64c03240 100644 --- a/tests/test_litellm/llms/oci/chat/test_oci_cohere_tool_calls.py +++ b/tests/test_litellm/llms/oci/chat/test_oci_cohere_tool_calls.py @@ -326,20 +326,24 @@ class TestOCICohereToolCalls: chat_history = adapt_messages_to_cohere_standard(messages) - # First message is the user message - assert chat_history[0].role == "USER" - assert chat_history[0].message == "What's the weather?" + # The last user message is consumed by the request's top-level `message` + # field, so chatHistory carries the assistant tool call and tool result. + assert len(chat_history) == 2 - # Second message is the assistant with tool calls and no text - assistant_msg = chat_history[1] + assistant_msg = chat_history[0] assert assistant_msg.role == "CHATBOT" assert assistant_msg.message is None or assistant_msg.message == "" assert assistant_msg.toolCalls is not None assert len(assistant_msg.toolCalls) == 1 assert assistant_msg.toolCalls[0].name == "get_weather" + tool_msg = chat_history[1] + assert tool_msg.role == "TOOL" + assert tool_msg.toolResults[0].call.name == "get_weather" + assert tool_msg.toolResults[0].outputs[0]["output"] == "Sunny, 25C" + def test_cohere_chat_history_with_tool_calls(self): - """Test chat history transformation with tool calls""" + """Tool results trailing the last user turn must be preserved in chatHistory.""" config = OCIChatConfig() messages = [ @@ -367,26 +371,27 @@ class TestOCICohereToolCalls: chat_history = adapt_messages_to_cohere_standard(messages) - # Verify chat history structure (excludes last message) + # The last user message becomes the request's top-level `message`. + # Everything else — including the trailing tool result — must remain in + # chatHistory so the model can see the tool output. assert len(chat_history) == 2 - # Check user message - user_msg = chat_history[0] - assert user_msg.role == "USER" - assert user_msg.message == "What's the weather like in Tokyo?" - - # Check assistant message with tool calls - assistant_msg = chat_history[1] + assistant_msg = chat_history[0] assert assistant_msg.role == "CHATBOT" assert assistant_msg.message == "I will look up the weather in Tokyo." assert assistant_msg.toolCalls is not None assert len(assistant_msg.toolCalls) == 1 assert assistant_msg.toolCalls[0].name == "get_weather" - # The parameters should be parsed as JSON assert assistant_msg.toolCalls[0].parameters == {"location": "Tokyo"} - # Note: The tool message (last message) is excluded from chat history - # This is the expected behavior for Cohere models + tool_msg = chat_history[1] + assert tool_msg.role == "TOOL" + assert tool_msg.toolResults[0].call.name == "get_weather" + assert tool_msg.toolResults[0].call.parameters == {"location": "Tokyo"} + assert ( + tool_msg.toolResults[0].outputs[0]["output"] + == "The weather in Tokyo is 22°C with partly cloudy skies." + ) def test_cohere_streaming_chunk_handling(self): """Test Cohere streaming chunk handling"""