mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
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:
parent
e907e5ee9b
commit
bf438da481
3 changed files with 109 additions and 2 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue