llama4-tools-lenient¶
Source: fixes/llama4-tools-lenient/
A tool-call parser plugin and chat template for Llama 4 that read every call syntax the model writes and render tool history the way the model expects.
| Affects | Llama 4 Scout and Maverick with tool calling |
| Releases | v0.26.0, v0.30.0 |
| Patches | nothing: llama4_tools_lenient.py is a --tool-parser-plugin, llama4-tools.jinja a --chat-template |
| Upstream | not filed yet |
| Test | test_llama4_tools_lenient.py (stdlib; the template test needs jinja2) |
Problem¶
Llama 4 writes tool calls in several forms: Meta's documented <function=NAME>{json}</function>,
Python lists such as [read(path="a.py")], bare calls, calls inside
<|python_start|> ... <|python_end|>, and JSON objects. The stock llama4_pythonic parser reads
only the Python list form, and only at the start of the reply. The chat template shipped with the
checkpoints re-renders past calls as param="<raw value>" (so quotes, newlines and lists break the
model's own history) and JSON-encodes tool results into one escaped line.
Symptom¶
Tool calls come back as plain content, arguments arrive as strings where the schema wants
numbers, booleans or arrays, and multi-turn agent sessions degrade as the model copies its broken
history. Agents also see the model plan a whole task as one parallel list (read, then an edit of
text it has not read, then a test run), where the blind steps fail.
Who is affected¶
Anyone serving Llama 4 with --enable-auto-tool-choice.
The fix¶
llama4_tools_lenient.py registers the llama4_lenient parser. It accepts all the forms above,
repairs arguments from the request's tool schemas (no evaluation), streams prose as it arrives and
sends each call whole once it parses. LLAMA4_TOOL_CALL_POLICY chooses which calls of a parallel
list are returned:
| Value | Behaviour |
|---|---|
independent (default) |
only calls that cannot depend on an earlier call in the same turn: a leading run of read-only calls, or of edits to distinct files |
reads |
a leading run of read-only calls, else the first call |
first |
only the first call (also used when a request sets parallel_tool_calls: false) |
all |
every call, as upstream |
LLAMA4_READONLY_TOOLS and LLAMA4_EDIT_TOOLS (comma-separated tool names) override the
built-in tool lists. llama4-tools.jinja renders past calls as valid Python literals, passes tool
results verbatim and ends tool turns with <|eom|> as Meta's reference encoder does; its options
(fmt, sys_mode, defs, result, eom, history, seq_rule) are documented at the top of the
file and set with --default-chat-template-kwargs.
apply.py writes nothing. It checks that the plugin compiles, the template exists and the
installed vLLM still has the parser API the plugin uses, then prints the serve flags:
python3 fixes/llama4-tools-lenient/apply.py
vllm serve meta-llama/Llama-4-Scout-17B-16E-Instruct \
--enable-auto-tool-choice \
--tool-parser-plugin fixes/llama4-tools-lenient/llama4_tools_lenient.py \
--tool-call-parser llama4_lenient \
--chat-template fixes/llama4-tools-lenient/llama4-tools.jinja
Verify¶
python3 fixes/llama4-tools-lenient/apply.py # 0 = plugin usable, 2 = vLLM API drifted
pytest fixes/llama4-tools-lenient
End to end: run an agent task that reads, edits and tests a file. Calls should arrive as
tool_calls with typed arguments, one dependent step per turn.