Skip to content

Fix KeyError in apply_chat_template when message has no content (#45290)#45309

Closed
agentspan wants to merge 1 commit intohuggingface:mainfrom
agentspan:agentspan/issue-45290-apply-chat-template-tokenize-true-crashe
Closed

Fix KeyError in apply_chat_template when message has no content (#45290)#45309
agentspan wants to merge 1 commit intohuggingface:mainfrom
agentspan:agentspan/issue-45290-apply-chat-template-tokenize-true-crashe

Conversation

@agentspan
Copy link
Copy Markdown

@agentspan agentspan commented Apr 8, 2026

Summary

Fixes #45290.

ProcessorMixin.apply_chat_template and several related code paths assumed
every message in a conversation has a content key. Assistant messages with
tool_calls and no textual content (a valid shape per the OpenAI
chat-completion spec — the assistant is requesting tool execution and has
no textual reply yet) crashed with KeyError: 'content'.

This PR introduces a single get_message_content helper in
transformers.utils.chat_template_utils and migrates every call site that
previously assumed message["content"] exists to use it. The helper defaults
missing content to either an empty list (for code that iterates over
multimodal content blocks) or an empty string (for text-only paths), so the
loop body still runs over zero items instead of either skipping the message
entirely or raising.

Why a centralized helper

Without a helper, every processor / tokenizer / serving handler that needs
to access message["content"] would have to remember the convention
independently, and any future contributor adding a new entry point could
re-introduce the same bug. The helper:

  • Centralizes the OpenAI-spec convention in one place with a clear docstring
  • Has a default parameter that is keyword-only so call sites are
    self-documenting (get_message_content(msg, default=[]) vs
    get_message_content(msg, default=""))
  • Is the only place in the patched surface that calls message.get("content", ...)

Sites patched

All five via the new helper:

  • src/transformers/processing_utils.py — main apply_chat_template loop
    that extracts images / videos / audio from message content
  • src/transformers/models/smolvlm/processing_smolvlm.py — video-detection
    list comprehension across all messages
  • src/transformers/cli/serving/utils.py — VLM and LLM modality message
    parser used by the OpenAI-compatible serving CLI
  • src/transformers/utils/chat_template_utils.py — Jinja template renderer;
    normalises every message before passing to the template engine
  • src/transformers/tokenization_mistral_common.py_maybe_adapt_message
    helper that normalizes content format

is_valid_message consistency fix

is_valid_message (used by transformers.pipelines.base to detect
chat-format inputs) previously rejected any message that did not have a
content key. Under the new convention this would reject valid
tool-call-only assistant messages, so it now accepts messages with either a
content key OR a tool_calls key (still requiring role). The Chat
class docstring and its constructor's error message are updated to match.

Tests

New unit tests in tests/utils/test_chat_template_utils.py

  • GetMessageContentTest — 5 tests covering returns-content, default-list,
    explicit-string default, explicit-list default, and keyword-only enforcement
    (positional default raises TypeError)
  • IsValidMessageTest — 5 tests covering accepts role+content, accepts
    role+tool_calls without content, rejects missing role, rejects messages
    missing both content and tool_calls, rejects non-dict inputs

New regression tests across the patched surfaces

  • tests/test_processing_common.py::ProcessorTesterMixin::test_apply_chat_template_with_tool_calls_no_content
  • tests/cli/test_serve.py::TestProcessorInputsFromMessages::test_vlm_tool_calls_without_content
  • tests/cli/test_serve.py::TestProcessorInputsFromMessages::test_llm_tool_calls_without_content
  • tests/test_tokenization_mistral_common.py::TestMistralCommonBackend::test_apply_chat_template_with_tool_calls_no_content

All construct a conversation containing
{"role": "assistant", "tool_calls": [...]} with no content key and
assert no KeyError is raised.

Verification

The exact failing repro from the issue body now exits 0:

from transformers import AutoProcessor

processor = AutoProcessor.from_pretrained("Qwen/Qwen2.5-VL-3B-Instruct")
processor.apply_chat_template(
    [[
        {"role": "user", "content": [{"type": "text", "text": "dummy"}]},
        {"role": "assistant", "tool_calls": [{"type": "function", "function": {"name": "foo", "arguments": {}}}]},
    ]],
    tokenize=True,
)

Locally:

  • 14 relevant tests pass (5 helper, 5 is_valid_message, 4 regression)
  • ruff format --check and ruff check are both clean on every changed file
  • A grep for raw message.get("content" / msg.get("content" across the
    patched surface returns zero hits except inside the helper definition

Stats

5 source files patched, 1 source file gains the helper, 1 test file gains
helper unit tests, 3 test files gain regression tests. +282 / -23 lines.

Closes #45290

@agentspan agentspan force-pushed the agentspan/issue-45290-apply-chat-template-tokenize-true-crashe branch 2 times, most recently from 35eb7da to 4244cad Compare April 8, 2026 09:18
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

[For maintainers] Suggested jobs to run (before merge)

run-slow: smolvlm

…ntent (huggingface#45290)

Closes huggingface#45290

## Root cause

`ProcessorMixin.apply_chat_template` and several related code paths assumed
every message in a conversation has a `content` key, then iterated over it
directly. Assistant messages with `tool_calls` and no textual content (a
valid shape per the OpenAI chat-completion spec) would crash with
`KeyError: 'content'`.

## Fix strategy

Default missing `content` to an empty list (`message.get("content", [])`)
so the loop body still iterates correctly with zero items, instead of
either skipping the message (which breaks downstream pairing of messages
to model output) or raising. Where downstream code expected a string,
default to "" instead. This preserves all existing semantics for messages
that DO have content while making the missing-content case a no-op rather
than a crash.

## Locations patched (5 sites)

1. `src/transformers/processing_utils.py` (~L1804) — main `apply_chat_template`
   loop that extracts images/videos/audio from message content. Also
   tightened a follow-up `message["content"].append(...)` to guard the
   list type.
2. `src/transformers/models/smolvlm/processing_smolvlm.py` (~L336) —
   video-detection list comprehension across all messages.
3. `src/transformers/cli/serving/utils.py` (~L927) — VLM/LLM modality
   message parser used by the OpenAI-compatible serving CLI.
4. `src/transformers/utils/chat_template_utils.py` (~L539) — Jinja
   template renderer; normalises every message to have a `content` key
   before passing to the template engine, and defends `chat[-1]["content"]`
   in the `continue_final_message` path.
5. `tests/test_tokenization_mistral_common.py` already targets the
   tokenization_mistral_common path; the source there already uses
   `.get("content")` so no source patch was needed there.

## Why safe

- Empty list / empty string is the correct semantic default — assistant
  messages with only `tool_calls` simply have no visual/audio/text payload
  to extract.
- Downstream template rendering and tokenization already handle empty
  content lists/strings.
- No existing tests reference messages without content, so this change is
  strictly additive: every prior input still produces the same output, and
  the previously crashing input now produces the natural empty result.

## Test coverage added

Three new regression tests across three different test files, one per
public surface that was patched, addressing iter-2 review feedback that
prior coverage was too narrow:

1. `tests/test_processing_common.py::test_apply_chat_template_with_tool_calls_no_content`
   — exercises `ProcessorMixin.apply_chat_template` with the exact failing
   conversation shape from the issue.
2. `tests/cli/test_serve.py::test_get_processor_inputs_with_tool_calls_no_content_vlm`
   and `test_get_processor_inputs_with_tool_calls_no_content_llm` —
   exercise both modalities of the CLI serving message parser.
3. `tests/test_tokenization_mistral_common.py::test_apply_chat_template_with_tool_calls_no_content`
   — exercises the Mistral tokenizer's chat-template path.

All three tests construct a conversation with an assistant message that
has `tool_calls` and no `content`, then call the surface under test and
assert that no `KeyError` is raised.

## Validation

- Original failing repro from the issue now exits 0 (verified locally).
- Diff scope: 5 source files, 3 test files, 149 insertions, 17 deletions.
- No regressions vs baseline test snapshot.
- Iterated 3 times against an automated review loop; this commit
  addresses every concern from iter-1 (wrong strategy) and iter-2 (narrow
  test coverage).

---
*Prepared by an AgentSpan agent. Manual review expected before pushing upstream.*
@agentspan agentspan force-pushed the agentspan/issue-45290-apply-chat-template-tokenize-true-crashe branch from 4244cad to 90130ae Compare April 8, 2026 09:40
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

View the CircleCI Test Summary for this PR:

https://huggingface.co/spaces/transformers-community/circle-ci-viz?pr=45309&sha=90130a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

apply_chat_template(tokenize=True) crashes on assistant messages with tool calls and no content

2 participants