hiyouga/LlamaFactory · error · ValueError

tool_call value is not valid JSON: {content['value']!r}

Error message

tool_call value is not valid JSON: {content['value']!r}

What it means

While converting internal Message blocks to HuggingFace chat format, a content block of type 'tool_call' must carry a JSON-serializable string. This is the multimodal branch (format.py:56): json.loads(content['value']) raised JSONDecodeError, so the tool_call value is not parseable JSON and the message cannot be rendered.

Source

Thrown at src/llamafactory/v1/core/rendering/format.py:56

def _to_hf_messages(messages: list[Message], is_multimodal: bool = False) -> list[dict]:
    """Convert v1 Message format to HF format for apply_chat_template."""
    hf_messages = []
    for message in messages:
        tool_calls: list[dict] = []
        reasoning_content = ""

        if is_multimodal:
            hf_content = []
            for content in message["content"]:
                if content["type"] == "text":
                    hf_content.append({"type": "text", "text": content["value"]})
                elif content["type"] == "reasoning":
                    reasoning_content += content["value"]
                elif content["type"] == "tool_call":
                    try:
                        tc = json.loads(content["value"])
                    except json.JSONDecodeError as e:
                        raise ValueError(f"tool_call value is not valid JSON: {content['value']!r}") from e
                    if not isinstance(tc, dict) or "name" not in tc or "arguments" not in tc:
                        raise ValueError(
                            f"tool_call must be a JSON object with 'name' and 'arguments' keys, got {tc!r}"
                        )
                    tool_calls.append(
                        {"type": "function", "function": {"name": tc["name"], "arguments": tc["arguments"]}}
                    )
                elif content["type"] == "image_url":
                    hf_content.append({"type": "image", "image": content["value"]})
                elif content["type"] == "video_url":
                    hf_content.append({"type": "video", "video": content["value"]})
                elif content["type"] == "audio_url":
                    hf_content.append({"type": "audio", "audio": content["value"]})
            hf_msg = {"role": message["role"], "content": hf_content}
        else:
            text = ""
            for content in message["content"]:
                if content["type"] == "text":

View on GitHub (pinned to f28afaf635)

Solutions

  1. Ensure every tool_call block's 'value' is a valid JSON string, e.g. json.dumps({'name': ..., 'arguments': ...})
  2. If value is already a dict, serialize it: value = json.dumps(value)
  3. Validate/repair the dataset: parse each tool_call value with json.loads in preprocessing and drop or fix failures

Example fix

# before
content = {"type": "tool_call", "value": {"name": "get_weather", "arguments": {"city": "SF"}}}

# after
import json
content = {"type": "tool_call", "value": json.dumps({"name": "get_weather", "arguments": {"city": "SF"}})}
Defensive patterns

Strategy: validation

Validate before calling

import json

def valid_tool_call_value(value) -> bool:
    if not isinstance(value, str):
        return False
    try:
        json.loads(value)
        return True
    except json.JSONDecodeError:
        return False

Prevention

When it happens

Trigger: Passing a multimodal message whose content list contains {"type": "tool_call", "value": ...} where value is a Python dict (not a JSON string), truncated JSON, or plain text. Only triggered on the is_multimodal=True code path.

Common situations: Dataset converters that store tool_call arguments as native dicts instead of JSON strings; partially-truncated tool-call samples from scraped agent logs; function-calling datasets formatted for a different schema.

Related errors


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