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 contentView on GitHub (pinned to f28afaf635)
Solutions
- 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).
- Regenerate the offending dataset so arguments are serialized with json.dumps, never str().
- 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
- Always serialize tool arguments with json.dumps, never str()/repr().
- Reject or clean model outputs containing True/False/None literals before feeding extraction.
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
- Invalid JSON format in tool description: {str([content])}.
- tools is not valid JSON: {tools!r}
- Invalid tools
- Cannot stream function calls.
- Invalid JSON format in function message: {str([content])}.
AI-assisted analysis of hiyouga/LlamaFactory@f28afaf635 (2026-08-14).
Data as JSON: /api/errors/95e5517ff2c9ac28.
Report an issue: GitHub.