hiyouga/LlamaFactory · error · ValueError

Unknown identifier: {node.id}

Error message

Unknown identifier: {node.id}

What it means

The GLM-4-MoE / Qwen3.5-style tool extractor parses tool-call arguments with Python's ast module but accepts JSON-style literals: bare identifiers true/false/null are converted to True/False/None. Any other bare Name identifier (e.g. none, None, True, False, undefined) inside the arguments string raises ValueError('Unknown identifier: ...') in _ast_to_value.

Source

Thrown at src/llamafactory/data/tool_utils.py:822

                    kwargs_parts.append(f"{key}={json.dumps(value, ensure_ascii=False)}")

            calls.append(f"{name}({', '.join(kwargs_parts)})")

        return f"<|tool_call_start|>[{', '.join(calls)}]<|tool_call_end|>"

    @staticmethod
    def _ast_to_value(node: ast.AST) -> Any:
        """Convert an AST node to a Python value, handling JSON-style booleans/null."""
        # Handle JSON-style true/false/null as Name nodes
        if isinstance(node, ast.Name):
            if node.id == "true":
                return True
            elif node.id == "false":
                return False
            elif node.id == "null":
                return None
            else:
                raise ValueError(f"Unknown identifier: {node.id}")

        # Use literal_eval for other cases (strings, numbers, lists, dicts)
        return ast.literal_eval(node)

    @override
    @staticmethod
    def tool_extractor(content: str) -> Union[str, list["FunctionCall"]]:
        # Extract content between tool call markers
        start_marker = "<|tool_call_start|>"
        end_marker = "<|tool_call_end|>"

        start_idx = content.find(start_marker)
        if start_idx == -1:
            return content

        end_idx = content.find(end_marker, start_idx)
        if end_idx == -1:
            return content

View on GitHub (pinned to f28afaf635)

Solutions

  1. Normalize the arguments text to strict JSON before extraction: replace True/False/None with true/false/null (or json.dumps-serialize the dict instead of str/repr).
  2. Regenerate the offending dataset so arguments are serialized with json.dumps, never str().
  3. If the text is model output, add a cleaning step or few-shot examples enforcing JSON booleans.

Example fix

# before
{"name": "set_flag", "arguments": "{\"flag\": True}"}

# after
{"name": "set_flag", "arguments": "{\"flag\": true}"}
Defensive patterns

Strategy: fallback

Validate before calling

import re, json

def normalize_tool_args(text: str) -> str:
    # convert Python literals to JSON before extraction
    text = re.sub(r"\bTrue\b", "true", text)
    text = re.sub(r"\bFalse\b", "false", text)
    text = re.sub(r"\bNone\b", "null", text)
    json.loads(text)  # raises if still invalid
    return text

Try / catch

try:
    calls = utils.tool_extractor(content)
except ValueError as e:
    if "Unknown identifier" in str(e):
        calls = utils.tool_extractor(normalize_tool_args(content))  # one retry after JSON normalization
    else:
        raise

Prevention

When it happens

Trigger: Extracting tool calls from generated text whose arguments JSON contains Python-style None/True/False or a typo'd bare word (e.g. {"flag": True} instead of {"flag": true}); running the tool extractor during dataset construction or inference post-processing on model output that emitted Python literals.

Common situations: Fine-tuning on or evaluating models that emit Python-style booleans/None in JSON tool arguments; round-tripping tool call data through Python's repr instead of json.dumps.

Related errors


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