Fix TypeError when an assistant tool call has content: null

#272

Summary

The chat template raises TypeError on an assistant message that has content: null and tool_calls. That is the exact shape the OpenAI Chat Completions API returns for a turn that only calls tools, so appending an assistant response to the conversation and rendering it back, the normal agent loop crashes.

Reproduction

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("openai/gpt-oss-120b")

tools = [{"type": "function", "function": {
    "name": "get_weather", "description": "Get weather.",
    "parameters": {"type": "object", "properties": {"city": {"type": "string"}}}}}]

messages = [
    {"role": "user", "content": "Weather in Zurich?"},
    # exactly what the API returns for a tool-only turn
    {"role": "assistant", "content": None, "tool_calls": [
        {"id": "c1", "type": "function",
         "function": {"name": "get_weather", "arguments": {"city": "Zurich"}}}]},
    {"role": "tool", "name": "get_weather", "tool_call_id": "c1", "content": "18C"},
]

tok.apply_chat_template(messages, tools=tools, add_generation_prompt=True)
# TypeError: argument of type 'NoneType' is not iterable

Cause

{%- if "content" in message %}
    {%- if "<|channel|>analysis<|message|>" in message.content or ... %}

The guard tests whether the key is present. The body then runs a substring match against its value. When content is present and null, the guard passes and "..." in None raises.

The check is a validation guard: it exists to reject <|channel|> tags smuggled in through content. A null content cannot contain a tag, so it should be skipped, not inspected.

The identical pattern appears two lines below for thinking, and fails the same way on thinking: null.

Fix

Test the value rather than the key:

-        {%- if "content" in message %}
+        {%- if message.content %}
             {%- if "<|channel|>analysis<|message|>" in message.content or "<|channel|>final<|message|>" in message.content %}

-        {%- if "thinking" in message %}
+        {%- if message.thinking %}
             {%- if "<|channel|>analysis<|message|>" in message.thinking or "<|channel|>final<|message|>" in message.thinking %}

Both are validation guards, so skipping them for empty or null values changes nothing about what gets rendered for any value that previously worked.

Verification

I rendered every snapshot of 49 conversation shapes through the template before and after the change. Every conversation that rendered before still renders, and produces a byte-identical prompt. The only difference is that the shapes which previously raised now render.

Omitting the content key entirely, or passing "", already worked before this patch. Only the spec-compliant null failed, which is why this has been easy to miss locally and hard to avoid in production.

Related, not included

Line 305 has a third instance of the same root cause:

{%- if "thinking" in message %}
    {{- "<|start|>assistant<|channel|>analysis<|message|>" + message.thinking + "<|end|>" }}

str + None also raises. I left it out because it is on the loop.last and not add_generation_prompt path that the comment describes as training-only, and because changing it alters behaviour for thinking: "" (currently emits an empty analysis block, would emit nothing). Happy to include it if you would prefer the complete fix.

Found with ChatLint, which renders chat templates against a corpus of conversation shapes and checks that they do not crash, drop content, or rewrite history.

Ready to merge
This branch is ready to get merged automatically.

Sign up or log in to comment