unslothai/unsloth · error · ValueError

role="{self.role}" messages require "content".

Error message

role="{self.role}" messages require "content".

What it means

Raised by the ChatMessage model validator when a user or system message has content that is None or an empty list. Assistant and tool messages get empty-content normalization (assistant collapses to a Post-Stop sentinel None, tool to ""), but user/system messages must actually say something.

Source

Thrown at studio/backend/models/inference.py:1274

            raise ValueError('"tool_calls" is only valid on role="assistant" messages.')
        if self.tool_call_id is not None and self.role != "tool":
            raise ValueError('"tool_call_id" is only valid on role="tool" messages.')
        if self.name is not None and self.role != "tool":
            raise ValueError('"name" is only valid on role="tool" messages.')

        if self.role == "tool":
            # tool_call_id resolution happens at ChatCompletionRequest scope.
            # OpenAI accepts empty tool results (commands with no output);
            # normalize to "" instead of a 400 agentic clients treat as fatal.
            if self.content is None or self.content == []:
                self.content = ""
        elif self.role == "assistant":
            # Post-Stop sentinel: collapse content="" / [] to None.
            if (self.content == "" or self.content == []) and not self.tool_calls:
                self.content = None
        else:  # "user" | "system"
            if self.content is None or self.content == []:
                raise ValueError(f'role="{self.role}" messages require "content".')
        return self


class ThinkingConfig(BaseModel):
    """Anthropic-compatible thinking/reasoning configuration.
    Use type='disabled' to turn off thinking, or type='enabled' to turn it on.
    Only type is read; extra fields (e.g. budget_tokens) are ignored, since
    Unsloth sets provider thinking budgets itself.
    """

    type: Literal["disabled", "enabled"] = "disabled"


# Recognized permission_mode values. The field accepts a plain string rather than
# a Literal so an unrecognized value from a newer UI/client degrades to the safest
# gate ("ask") instead of a 422. None stays unset at the request boundary: the tool
# loops normalize it to the product default "auto", while the route's confirm-gate
# derivation keeps an unset mode lenient (a non-streaming request cannot prompt, so

View on GitHub (pinned to 203007d190)

Solutions

  1. Guard client-side: skip or refuse to send user turns whose text is empty.
  2. For multimodal [] content, require at least one part or fallback text.
  3. Check the turn-building loop for accidental empty appends after trimming whitespace.

Example fix

# before
messages.append({"role": "user", "content": user_text})  # user_text can be ''

# after
if not user_text or not user_text.strip():
    raise ValueError('empty user turn')
messages.append({"role": "user", "content": user_text})
Defensive patterns

Strategy: validation

Validate before calling

def valid_content(msg: dict) -> bool:
    role, content = msg.get('role'), msg.get('content')
    if role in ('user', 'system'):
        return content is not None and content != [] and content != ''
    return True

Prevention

When it happens

Trigger: POST /v1/chat/completions with {"role": "user"} (content omitted), {"role": "user", "content": null}, or {"role": "system", "content": []}. Often a client bug that appends an empty turn when user input is blank.

Common situations: Chat UIs that submit before the user types anything; agents appending an empty user turn after a tool loop; multimodal clients sending content: [] when an attachment fails to load and no text was typed.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/c9ff19f1d13525f7. Report an issue: GitHub.