Repository navigation
Empty tools: [] triggers tool-calling on Llama 3.x client-side templates #767
Description
Summary
The library sends "tools": [] in every chat() request body, even when the caller passed no tools. On servers that render Llama 3.x's chat template directly (notably rkllama), this routes the request into tool-calling mode: the template's tools is not none check passes for an empty list, so the model is told it has tools available and answers with a JSON function call instead of plain text.
Ollama's own server masks this because it coerces template inputs. Qwen's template uses if tools: (truthiness) rather than is not none, so Qwen-family models are unaffected regardless of which server renders the template.
Related but distinct from #733, which covers serialization crashes on malformed tool contents; this one is about the always-sent empty list.
Reproduction
Any Llama 3.x model served by rkllama (which renders the model's own chat template):
from ollama import Client
client = Client(host="http://<rkllama-host>:8080")
resp = client.chat(
model="llama3.2:3b",
messages=[{"role": "user", "content": "list three colors"}],
)
print(resp["message"]["content"])Expected: "Red, blue, green." or similar prose.
Observed: '{"name": "list_colors", "arguments": {}}' or similar JSON function invocation.
Measured on an Indiedroid Nova (RK3588 NPU, rkllama image built 2026-08-25, RKLLM runtime 1.3.0) with Llama 3.2 3B Instruct (W8A8, HanzoHuang build, RKLLM toolkit 1.3.0): 5/5 chat requests through this library produced JSON function calls; 0/3 produced JSON when the tools field was removed from the request body by hand.
Root cause
Llama 3.x's chat template guards the tool-calling preamble with:
{% if tools is not none %}
# render "you have access to the following tools" preamble
{% endif %}An empty list [] is not None, so the preamble renders and the model is instructed to answer with a tool call. Qwen's template uses {% if tools %} (truthiness), which treats [] as falsy and skips the preamble.
Suggested fix
Omit tools from the serialized request body when the list is empty — distinguish "no tools" from "tools, but empty list" at the serialization layer. Real tool lists would pass through unchanged.
Workaround
A custom httpx transport that strips "tools": [] from outgoing /api/chat requests works as a local fix. Example implementation (MIT):
import json
import httpx
from ollama import Client
class DropEmptyTools(httpx.BaseTransport):
"""Strip `"tools": []` from ollama chat requests before send."""
def __init__(self, inner: httpx.BaseTransport | None = None) -> None:
self._inner = inner or httpx.HTTPTransport()
def handle_request(self, request: httpx.Request) -> httpx.Response:
if request.method == "POST" and request.url.path.endswith("/api/chat"):
try:
body = json.loads(request.content)
except ValueError:
body = None
if isinstance(body, dict) and body.get("tools") == []:
del body["tools"]
headers = [(k, v) for k, v in request.headers.multi_items()
if k.lower() != "content-length"]
request = httpx.Request(
request.method, request.url,
headers=headers,
content=json.dumps(body).encode(),
extensions=request.extensions,
)
return self._inner.handle_request(request)
def close(self) -> None:
self._inner.close()
# Usage:
client = Client(host="http://your-server:8080", transport=DropEmptyTools())
Verified on my side: with DropEmptyTools in place, my downstream project’s test suite (120 tests, no server needed) still passes, and the model returns plain text for the repro above. Happy to run further tests on rkllama + Llama 3.x on RK3588 if useful — not a common setup, and I have it available.
Activity
Mrun25 commentedon Oct 19, 2026
Verified on current main (35d9667):
client.chat(model=..., messages=[...])without tools still sends"tools": []in the request body -_copy_tools(None)yields an empty list, andmodel_dump(exclude_none=True)keeps the key. Plan: normalize empty tool lists to absent at bothchat()call sites (sync and async), sotoolsis only sent when tools are actually provided, with tests. I will open a PR shortly.Mrun25 commentedon Oct 19, 2026
Opened a fix: #768