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"] = images

View on GitHub (pinned to f28afaf635)

Solutions

  1. Pass tools as a JSON string produced by json.dumps(tool_list)
  2. If tools is already a list/dict, serialize it before the call
  3. 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

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


AI-assisted analysis of hiyouga/LlamaFactory@f28afaf635 (2026-08-14). Data as JSON: /api/errors/219ea4bdb8d5266d. Report an issue: GitHub.