From 74836ce65f9a433c8ffaa2133240cb85685eb005 Mon Sep 17 00:00:00 2001 From: FenjuFu Date: Wed, 30 Sep 2026 20:38:19 +0800 Subject: [PATCH] fix(llm): forward tool definitions to Ollama OllamaProvider built the /api/chat payload without the tools from LLMInput, so Ollama models were never told any tools existed and a ReActAgent backed by Ollama never ran one. The response side already parsed message.tool_calls. Send the tools in the OpenAI function format that /api/chat accepts, matching the other providers. Fixes #3274 Signed-off-by: FenjuFu --- src/llm/providers/ollama.py | 2 ++ tests/test_provider_tools.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/llm/providers/ollama.py b/src/llm/providers/ollama.py index 58505ac7b..c36304c93 100644 --- a/src/llm/providers/ollama.py +++ b/src/llm/providers/ollama.py @@ -78,6 +78,8 @@ class OllamaProvider(LLMProvider): options["num_predict"] = input.max_tokens if options: payload["options"] = options + if input.tools: + payload["tools"] = [tool.to_openai_tool() for tool in input.tools] data = json.dumps(payload).encode("utf-8") req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}) diff --git a/tests/test_provider_tools.py b/tests/test_provider_tools.py index ba4c09415..f73f5fdd6 100644 --- a/tests/test_provider_tools.py +++ b/tests/test_provider_tools.py @@ -168,6 +168,37 @@ def test_ollama_provider_serializes_generation_options( assert output.stop_reason == "stop" +def test_ollama_provider_serializes_tools_and_parses_tool_calls(monkeypatch): + requests = [] + + def fake_urlopen(request, timeout): + requests.append(request) + return BytesIO( + b'{"message": {"content": "", "tool_calls": [{"function": ' + b'{"name": "search", "arguments": {"query": "ollama"}}}]}, "done_reason": "stop"}' + ) + + monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen) + provider = OllamaProvider(base_url="http://localhost:11434", default_model="qwen3") + + output = provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")], tools=[_tool()])) + + assert json.loads(requests[0].data)["tools"] == [ + { + "type": "function", + "function": { + "name": "search", + "description": "Search", + "parameters": {"type": "object", "properties": {"query": {"type": "string"}}}, + "strict": True, + }, + } + ] + assert output.has_tool_calls + assert output.tool_calls[0].name == "search" + assert output.tool_calls[0].arguments == {"query": "ollama"} + + def test_claude_provider_serializes_tools_for_messages_api(): provider = ClaudeProvider(api_key="test") client = _AnthropicClient()