fix(ollama): use native tool calling for ollama/ models

Requests with tools on the ollama/ prefix went to /api/generate with the
tools pasted into a "Produce JSON OUTPUT ONLY" system prompt and format=json.
On the turn after a tool result the model is still forced to emit a function
call, so agents loop forever (#40575), and streamed calls come back as plain
text (#35711)

Route them through the ollama_chat /api/chat path instead, which sends tools
and tool messages natively. This is the same change #18924 made for
ollama_chat
This commit is contained in:
Meryem Sakin 2026-09-10 20:53:57 +03:00
parent e907e5ee9b
commit bf438da481
3 changed files with 109 additions and 2 deletions

View file

@ -5308,6 +5308,8 @@ def completion(
GenericLiteLLMParams(**_supplemental_provider_params) if _supplemental_provider_params else None
),
)
if custom_llm_provider == "ollama" and (tools is not None or functions is not None):
custom_llm_provider = "ollama_chat" # rebind-ok: /api/generate has no native tool calling
## RESPONSES API BRIDGE LOGIC ## - check early and normalize model name
responses_api_model_info, model = responses_api_bridge_check(

View file

@ -6,7 +6,7 @@ import httpx
import pytest
import litellm
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
from litellm.llms.ollama.completion.transformation import (
OllamaConfig,
OllamaTextCompletionResponseIterator,
@ -544,3 +544,105 @@ async def test_ollama_async_completion_inlines_remote_images_off_the_event_loop(
assert response.choices[0].message.content == "Green"
assert async_only_image_fetch.fetched == [image_url]
assert captured["body"]["images"] == [async_only_image_fetch.base64_png]
GRAPH_STATS_TOOLS = [
{
"type": "function",
"function": {
"name": "graph_stats",
"description": "Return node and edge counts of the code graph",
"parameters": {"type": "object", "properties": {}},
},
}
]
def test_ollama_tool_result_turn_is_sent_to_native_chat_api():
"""https://github.com/BerriAI/litellm/issues/40575"""
requests = []
def handle(request):
requests.append((request.url.path, json.loads(request.content)))
return httpx.Response(
200,
json={
"model": "qwen3.8:27b",
"message": {"role": "assistant", "content": "The graph has 190921 nodes."},
"done": True,
"done_reason": "stop",
"prompt_eval_count": 1,
"eval_count": 1,
},
)
response = litellm.completion(
model="ollama/qwen3.8:27b",
messages=[
{"role": "user", "content": "How many nodes does the graph have?"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{"id": "call_1", "type": "function", "function": {"name": "graph_stats", "arguments": "{}"}}
],
},
{"role": "tool", "tool_call_id": "call_1", "name": "graph_stats", "content": '{"nodes": 190921}'},
],
tools=GRAPH_STATS_TOOLS,
api_base="http://ollama.example:11434",
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handle))),
)
assert [path for path, _ in requests] == ["/api/chat"]
body = requests[0][1]
assert body["tools"] == GRAPH_STATS_TOOLS
assert "format" not in body
assert [m["role"] for m in body["messages"]] == ["user", "assistant", "tool"]
assert body["messages"][2]["content"] == '{"nodes": 190921}'
assert response.choices[0].message.content == "The graph has 190921 nodes."
assert response.choices[0].message.tool_calls is None
assert response.choices[0].finish_reason == "stop"
def test_ollama_streamed_tool_call_is_returned_as_tool_call():
"""https://github.com/BerriAI/litellm/issues/35711"""
chunks = [
{
"model": "qwen3.8:27b",
"message": {
"role": "assistant",
"content": "",
"tool_calls": [{"function": {"name": "graph_stats", "arguments": {}}}],
},
"done": False,
},
{
"model": "qwen3.8:27b",
"message": {"role": "assistant", "content": ""},
"done": True,
"done_reason": "stop",
"prompt_eval_count": 1,
"eval_count": 1,
},
]
def handle(request):
assert request.url.path == "/api/chat"
return httpx.Response(200, content="\n".join(json.dumps(chunk) for chunk in chunks).encode())
streamed = list(
litellm.completion(
model="ollama/qwen3.8:27b",
messages=[{"role": "user", "content": "How many nodes does the graph have?"}],
tools=GRAPH_STATS_TOOLS,
stream=True,
api_base="http://ollama.example:11434",
client=HTTPHandler(client=httpx.Client(transport=httpx.MockTransport(handle))),
)
)
tool_calls = [tool_call for chunk in streamed for tool_call in chunk.choices[0].delta.tool_calls or []]
assert [tool_call.function.name for tool_call in tool_calls] == ["graph_stats"]
assert "".join(chunk.choices[0].delta.content or "" for chunk in streamed) == ""
assert streamed[-1].choices[0].finish_reason == "tool_calls"

View file

@ -2,6 +2,7 @@
Test for GitHub issue #11267 - System message format issue with Ollama + tools
"""
import copy
from unittest.mock import patch
@ -49,6 +50,8 @@ def test_system_message_format_issue_reproduction():
}
]
original_messages = copy.deepcopy(messages)
response = completion(
model=model,
messages=messages,
@ -57,7 +60,7 @@ def test_system_message_format_issue_reproduction():
mock_response=True,
)
assert len(messages[1]["content"]) == 2
assert messages == original_messages
if __name__ == "__main__":