deepset-ai/haystack · error · ValueError

A `ChatMessage` must contain at least one `TextContent`, `To

Error message

A `ChatMessage` must contain at least one `TextContent`, `ToolCall`, `ToolCallResult`,
              `ImageContent`, `FileContent`, `ReasoningContent`.

What it means

A ChatMessage with an empty content list cannot be serialized for the OpenAI Responses API; haystack raises ValueError because the API requires at least one content item (text, tool call, tool result, image, file, or reasoning).

Source

Thrown at haystack/components/generators/chat/openai_responses.py:996

        if isinstance(part, FileContent):
            return {
                "type": "input_file",
                # Filename is optional but if not provided, OpenAI expects a file_id of a previous file upload.
                # We use a dummy filename to avoid this issue.
                "filename": part.filename or "filename",
                "file_data": f"data:{part.mime_type or 'application/pdf'};base64,{part.base64_data}",
            }
        raise ValueError(f"Unsupported content type: {type(part)}")

    text_contents = message.texts
    tool_calls = message.tool_calls
    tool_call_results = message.tool_call_results
    images = message.images
    reasonings = message.reasonings
    files = message.files

    if not any([text_contents, tool_calls, tool_call_results, images, reasonings, files]):
        raise ValueError(
            """A `ChatMessage` must contain at least one `TextContent`, `ToolCall`, `ToolCallResult`,
              `ImageContent`, `FileContent`, or `ReasoningContent`."""
        )
    if len(tool_call_results) > 0 and len(message._content) > 1:
        raise ValueError(
            "For OpenAI compatibility, a `ChatMessage` with a `ToolCallResult` cannot contain any other content."
        )

    formatted_messages: list[dict[str, Any]] = []
    openai_msg: dict[str, Any] = {"role": message._role.value}
    if message._name is not None:
        openai_msg["name"] = message._name

    # user message
    if message._role.value == "user":
        content = [convert_part(part) for part in message._content]
        openai_msg["content"] = content
        return [openai_msg]

View on GitHub (pinned to e318778c9b)

Solutions

  1. Ensure every message passed to the generator has at least one content part
  2. Skip or drop empty messages from the message list before run()
  3. Add a placeholder TextContent if the message must be sent

Example fix

// before
msg = ChatMessage(_role=ChatRole.USER, _content=[])
// after
msg = ChatMessage.from_user("(empty)") if not contents else ChatMessage(_role=ChatRole.USER, _content=contents)
Defensive patterns

Strategy: validation

Validate before calling

for m in messages:
    if not m._content:
        raise ValueError("message has no content")  # or drop it

Type guard

def has_content(m) -> bool:
    return bool(m._content)

Try / catch

try:
    gen.run(messages=messages)
except ValueError as e:
    if "must contain at least one" in str(e):
        messages = [m for m in messages if m._content]
        gen.run(messages=messages)

Prevention

When it happens

Trigger: Calling OpenAIResponsesChatGenerator.run/count with a ChatMessage constructed with no contents, e.g. ChatMessage(_role=ChatRole.USER, _content=[]).

Common situations: Building messages dynamically where all branches skipped adding content; filtering out contents (e.g. stripping images) leaving an empty message.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/bc96298c5faf38f7. Report an issue: GitHub.