From 49ba871f71f59d17bbede40806e68cb4d2a724fa Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 19 May 2026 08:16:51 +0000 Subject: [PATCH] fix(oci/cohere): preserve trailing tool result in chatHistory When the last message in the OpenAI-format input is a tool result (the standard agentic continuation pattern), the prior messages[:-1] slice silently dropped that tool result from chatHistory and the model never saw it. Excluding the last user message by index instead keeps tool results that trail the last user turn intact. --- litellm/llms/oci/chat/cohere.py | 22 ++++++++++- .../oci/chat/test_oci_cohere_tool_calls.py | 39 +++++++++++-------- 2 files changed, 42 insertions(+), 19 deletions(-) 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"""