chat_template: string tool-call arguments crash rendering, and non-string values are written as Python repr instead of JSON

#30
by SamJoshua - opened

Hi OpenBMB β€” while wiring MiniCPM5-2B into a tool-using agent harness on CPU I hit two chat_template.jinja issues that are easy to miss and matter for real multi-turn tool loops. (Not model-quality complaints. Maintainers already merged a related template cleanup in #19, so flagging these as a follow-up.)

1. OpenAI-style string arguments crashes the template

In the assistant tool_calls branch the template always does:

{%- set args_dict = tool_call.arguments %}
{%- for param_name, param_value in args_dict.items() %}

OpenAI Chat Completions (and many agent frameworks) pass arguments as a JSON string, not a dict. On that path Jinja raises and apply_chat_template aborts:

UndefinedError: 'str object' has no attribute 'items'

So any history that came from an OpenAI-compatible server / LangChain / custom tracer cannot be re-rendered for the next turn. Mage-VL's Hub template (and most Qwen-style ones) already branch on arguments is string.

Repro (CPU, transformers 5.22.2):

from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("openbmb/MiniCPM5-2B")

tok.apply_chat_template([
    {"role": "user", "content": "Weather in SF?"},
    {"role": "assistant", "content": "",
     "tool_calls": [{"type": "function", "function": {
         "name": "get_weather",
         "arguments": '{"city": "SF", "unit": "c"}'  # string, OpenAI-style
     }}]},
], tokenize=False, add_generation_prompt=False)
# UndefinedError: 'str object' has no attribute 'items'

Fix sketch: if arguments is a string, json.loads it (or emit the raw JSON into the XML params); if it is already a mapping, keep the .items() loop.

2. Non-string argument values are silently Python-repr corrupted

When arguments is a dict, non-string values are dumped with {{ param_value }} instead of tojson. Bools become True/False, and nested objects become Python repr:

<function name="book_flight"><param name="flights">2</param><param name="refundable">False</param><param name="meta">{'cabin': 'economy', 'extras': ['wifi']}</param></function>

That is not valid JSON, not valid for the SGLang minicpm5 parser's usual expectations, and it poisons SFT / BC round-trips: re-tokenize the rendered turn and the tool args no longer match what the agent actually called.

Repro:

out = tok.apply_chat_template([
    {"role": "user", "content": "book it"},
    {"role": "assistant", "content": "",
     "tool_calls": [{"type": "function", "function": {
         "name": "book_flight",
         "arguments": {
             "flights": 2,
             "refundable": False,
             "meta": {"cabin": "economy", "extras": ["wifi"]},
         }
     }}]},
], tokenize=False, add_generation_prompt=False)
print(out)
# ...<param name="refundable">False</param>
# ...<param name="meta">{'cabin': 'economy', 'extras': ['wifi']}</param>...

Fix sketch: for each param value, if it is a string use the existing CDATA / raw path; otherwise {{ param_value | tojson }} (and ideally CDATA-wrap when the JSON contains < / & / newlines).

Happy to open a tiny PR that only touches the tool_calls argument block in chat_template.jinja if that's easier to review.

β€” Sam (building / red-teaming tool-using and computer-use agents)

Sign up or log in to comment