hiyouga/LlamaFactory · error · ValueError
tools is not valid JSON: {tools!r}
Error message
tools is not valid JSON: {tools!r} What it means
The optional `tools` argument to the renderer must be a JSON string (it is escaped like user text, then json.loads'd). If parsing fails, this error is raised with the offending string repr. Note the escape step inserts zero-width spaces into the tools text only for self-validation; the parsed object is still required to be valid JSON.
Source
Thrown at src/llamafactory/v1/core/rendering/rendering.py:78
template_caller = processor if is_multimodal else tokenizer
if not getattr(template_caller, "chat_template", None):
template_caller.chat_template = _FALLBACK_CHATML_JINJA
# 0. Neutralize special-token strings in user-controlled text (no-op for normal data).
specials = _special_token_strings(tokenizer)
special_ids = {tid for tid, t in tokenizer.added_tokens_decoder.items() if getattr(t, "special", False)}
messages = _escape_special_in_messages(messages, specials, special_ids, tokenizer)
hf_messages = _to_hf_messages(messages, is_multimodal=is_multimodal)
tools_parsed = None
if tools:
tools = _escape_special(tools, specials, special_ids, tokenizer) # E3: tools text is user-controlled
try:
tools_parsed = json.loads(tools)
except json.JSONDecodeError as e:
raise ValueError(f"tools is not valid JSON: {tools!r}") from e
if not isinstance(tools_parsed, list):
tools_parsed = [tools_parsed]
if not is_generate and hf_messages and hf_messages[-1]["role"] == "assistant":
kwargs["enable_thinking"] = bool(hf_messages[-1].get("reasoning_content"))
def _encode(hf_msgs: list[dict], src_msgs: list[Message], add_generation_prompt: bool):
"""Render + tokenize, expanding media via the processor. Returns (input_ids, mm_outputs)."""
text = template_caller.apply_chat_template(
hf_msgs, tokenize=False, add_generation_prompt=add_generation_prompt, tools=tools_parsed, **kwargs
)
if is_multimodal and _count_media_in_messages(src_msgs) != (0, 0, 0):
images, videos, audios = _extract_media_from_messages(src_msgs)
# Every placeholder must come from a media block (escaping broke any literal ones).
_check_placeholder_counts(processor, text, len(images), len(videos), len(audios))
proc_kwargs = {"return_tensors": "pt"}
if images:
proc_kwargs["images"] = imagesView on GitHub (pinned to f28afaf635)
Solutions
- Pass tools as a JSON string produced by json.dumps(tool_list)
- If tools is already a list/dict, serialize it before the call
- Validate with json.loads(tools) in a unit test or preprocessing assert before training
Example fix
# before
renderer.render_messages(messages, tools=[{"type": "function", "function": {...}}])
# after
import json
renderer.render_messages(messages, tools=json.dumps([{"type": "function", "function": {...}}])) Defensive patterns
Strategy: validation
Validate before calling
import json
def prepare_tools(tools) -> str:
if isinstance(tools, (list, dict)):
tools = json.dumps(tools)
json.loads(tools) # fail early, clearly
return tools Prevention
- Build tools strings with json.dumps from Python objects
- Keep tool schemas in .json files loaded and validated once at startup
When it happens
Trigger: Calling render_messages/messages_to_model_input with tools=... where tools is a Python list/dict (not serialized), a malformed JSON string, or a string whose JSON was broken by embedding special-token text that the escaper mutated.
Common situations: Passing json-output of a tool-definition builder with trailing commas; double-encoding (tools=json.dumps(json.dumps(x))); tool schemas copied from docs containing smart quotes.
Related errors
- Invalid tools
- Cannot stream function calls.
- Invalid JSON format in tool description: {str([content])}.
- Unknown identifier: {node.id}
- tool_call value is not valid JSON: {content['value']!r}
AI-assisted analysis of hiyouga/LlamaFactory@f28afaf635 (2026-08-14).
Data as JSON: /api/errors/219ea4bdb8d5266d.
Report an issue: GitHub.