diff --git a/litellm/llms/ollama/chat/transformation.py b/litellm/llms/ollama/chat/transformation.py index f9e7071db41..300ea389f76 100644 --- a/litellm/llms/ollama/chat/transformation.py +++ b/litellm/llms/ollama/chat/transformation.py @@ -257,7 +257,8 @@ class OllamaChatConfig(BaseConfig): function=OllamaToolCallFunction( name=typed_tool["function"].get("name") or "", arguments=arguments, - ) + ), + **({"id": tool_id} if (tool_id := typed_tool.get("id")) is not None else {}), ) new_tools.append(ollama_tool_call) reasoning_content, parsed_content = extract_reasoning_content(cast(dict, m)) diff --git a/litellm/types/llms/ollama.py b/litellm/types/llms/ollama.py index 4783eac9a8d..fb9d9e8c363 100644 --- a/litellm/types/llms/ollama.py +++ b/litellm/types/llms/ollama.py @@ -1,4 +1,6 @@ from typing_extensions import ( + NotRequired, + ReadOnly, Required, TypedDict, ) @@ -12,6 +14,7 @@ class OllamaToolCallFunction( class OllamaToolCall(TypedDict): + id: NotRequired[ReadOnly[str]] function: OllamaToolCallFunction diff --git a/tests/unit/llms/ollama/test_ollama_chat_transformation.py b/tests/unit/llms/ollama/test_ollama_chat_transformation.py index 1a0bbe171a9..71d58f85e6c 100644 --- a/tests/unit/llms/ollama/test_ollama_chat_transformation.py +++ b/tests/unit/llms/ollama/test_ollama_chat_transformation.py @@ -854,6 +854,64 @@ class TestOllamaReasoningContentStreaming: class TestOllamaToolCallTransformation: + @pytest.mark.parametrize("stream", [False, True]) + def test_transform_request_keeps_parallel_call_ids_paired_with_results(self, stream: bool) -> None: + result: Final = OllamaChatConfig().transform_request( + model="minimax-m3.1-flash", + messages=[ + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "id": "call_first", + "type": "function", + "function": {"name": "read_fixture", "arguments": '{"path":"first.txt"}'}, + }, + { + "id": "call_second", + "type": "function", + "function": {"name": "read_fixture", "arguments": '{"path":"second.txt"}'}, + }, + ], + }, + {"role": "tool", "tool_call_id": "call_second", "content": "second result"}, + {"role": "tool", "tool_call_id": "call_first", "content": "first result"}, + ], + optional_params={"stream": stream}, + litellm_params={}, + headers={}, + ) + + assert result["messages"][0]["tool_calls"] == [ + {"id": "call_first", "function": {"name": "read_fixture", "arguments": {"path": "first.txt"}}}, + {"id": "call_second", "function": {"name": "read_fixture", "arguments": {"path": "second.txt"}}}, + ] + assert result["messages"][1:] == [ + {"role": "tool", "tool_call_id": "call_second", "content": "second result", "images": []}, + {"role": "tool", "tool_call_id": "call_first", "content": "first result", "images": []}, + ] + assert result["stream"] is stream + + def test_transform_request_does_not_invent_an_id_for_an_unidentified_call(self) -> None: + result: Final = OllamaChatConfig().transform_request( + model="minimax-m3.1-flash", + messages=[ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "function": {"name": "read_fixture", "arguments": "{}"}} + ], + } + ], + optional_params={}, + litellm_params={}, + headers={}, + ) + + assert result["messages"][0]["tool_calls"] == [{"function": {"name": "read_fixture", "arguments": {}}}] + def test_transform_request_preserves_tool_calls(self): """ tool_calls on assistant messages must survive transform_request.