mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-11 00:25:15 +00:00
fix(ollama): forward tool_calls and tool_call_id in transform_request (#26122)
tool_calls on assistant messages were translated to OllamaToolCall format
but never copied into the outgoing OllamaChatCompletionMessage, so Ollama
received {role: assistant, content: ''} with no tool_calls. The model
then had no record of having made a tool call, causing it to re-issue
the identical call on every turn (infinite loop).
Similarly, tool_call_id on role:tool messages was silently dropped.
Ollama uses this field to resolve the tool name from conversation history.
Also add tool_call_id to OllamaChatCompletionMessage TypedDict.
Fixes #26094
This commit is contained in:
@@ -265,8 +265,9 @@ class OllamaChatConfig(BaseConfig):
|
||||
): # avoid message serialization issues - https://github.com/BerriAI/litellm/issues/5319
|
||||
m = m.model_dump(exclude_none=True)
|
||||
tool_calls = m.get("tool_calls")
|
||||
new_tools: Optional[List[OllamaToolCall]] = None
|
||||
if tool_calls is not None and isinstance(tool_calls, list):
|
||||
new_tools: List[OllamaToolCall] = []
|
||||
new_tools = []
|
||||
for tool in tool_calls:
|
||||
typed_tool = ChatCompletionAssistantToolCall(**tool) # type: ignore
|
||||
if typed_tool["type"] == "function":
|
||||
@@ -280,7 +281,6 @@ class OllamaChatConfig(BaseConfig):
|
||||
)
|
||||
)
|
||||
new_tools.append(ollama_tool_call)
|
||||
cast(dict, m)["tool_calls"] = new_tools
|
||||
reasoning_content, parsed_content = _extract_reasoning_content(
|
||||
cast(dict, m)
|
||||
)
|
||||
@@ -296,6 +296,11 @@ class OllamaChatConfig(BaseConfig):
|
||||
ollama_message["content"] = content_str
|
||||
if images is not None:
|
||||
ollama_message["images"] = images
|
||||
if new_tools is not None:
|
||||
ollama_message["tool_calls"] = new_tools
|
||||
tool_call_id = m.get("tool_call_id")
|
||||
if tool_call_id is not None:
|
||||
ollama_message["tool_call_id"] = cast(str, tool_call_id)
|
||||
|
||||
new_messages.append(ollama_message)
|
||||
|
||||
|
||||
@@ -37,3 +37,4 @@ class OllamaChatCompletionMessage(TypedDict, total=False):
|
||||
images: List[str]
|
||||
tool_calls: List[OllamaToolCall]
|
||||
tool_name: str
|
||||
tool_call_id: str
|
||||
|
||||
@@ -746,3 +746,98 @@ class TestOllamaReasoningContentStreaming:
|
||||
result = iterator.chunk_parser(done_chunk)
|
||||
assert result.choices[0].delta.reasoning_content == "Final thought"
|
||||
assert result.choices[0].finish_reason == "stop"
|
||||
|
||||
|
||||
class TestOllamaToolCallTransformation:
|
||||
def test_transform_request_preserves_tool_calls(self):
|
||||
"""
|
||||
tool_calls on assistant messages must survive transform_request.
|
||||
Previously the translated OllamaToolCall list was built but never
|
||||
copied into the outgoing OllamaChatCompletionMessage, so Ollama
|
||||
received {role: assistant, content: ''} with no tool_calls and
|
||||
the model re-issued the same call on every turn.
|
||||
Regression: https://github.com/BerriAI/litellm/issues/26094
|
||||
"""
|
||||
config = OllamaChatConfig()
|
||||
messages = cast(
|
||||
list[AllMessageValues],
|
||||
[
|
||||
{"role": "user", "content": "What's the weather in SF?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "San Francisco, CA"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
result = config.transform_request(
|
||||
model="gemma4:27b",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assistant_msg = result["messages"][1]
|
||||
assert "tool_calls" in assistant_msg, "tool_calls must be forwarded to Ollama"
|
||||
assert len(assistant_msg["tool_calls"]) == 1
|
||||
tc = assistant_msg["tool_calls"][0]
|
||||
assert tc["function"]["name"] == "get_weather"
|
||||
assert tc["function"]["arguments"] == {"location": "San Francisco, CA"}
|
||||
|
||||
def test_transform_request_forwards_tool_call_id(self):
|
||||
"""
|
||||
tool_call_id on role:tool messages must be forwarded so Ollama can
|
||||
resolve the tool name from the conversation history.
|
||||
Regression: https://github.com/BerriAI/litellm/issues/26094
|
||||
"""
|
||||
config = OllamaChatConfig()
|
||||
messages = cast(
|
||||
list[AllMessageValues],
|
||||
[
|
||||
{"role": "user", "content": "What's the weather in SF?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_abc123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "San Francisco, CA"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_abc123",
|
||||
"content": "Sunny, 72°F",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
result = config.transform_request(
|
||||
model="gemma4:27b",
|
||||
messages=messages,
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
tool_msg = result["messages"][2]
|
||||
assert tool_msg["role"] == "tool"
|
||||
assert tool_msg["content"] == "Sunny, 72°F"
|
||||
assert "tool_call_id" in tool_msg, "tool_call_id must be forwarded to Ollama"
|
||||
assert tool_msg["tool_call_id"] == "call_abc123"
|
||||
|
||||
Reference in New Issue
Block a user