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, soView on GitHub (pinned to 203007d190)
Solutions
- Guard client-side: skip or refuse to send user turns whose text is empty.
- For multimodal [] content, require at least one part or fallback text.
- 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
- Disable submit while the input is blank
- For multimodal content arrays, require at least one part or fallback text
- Skip appending empty turns in agent loops
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
- "tool_calls" is only valid on role="assistant" messages.
- "tool_call_id" is only valid on role="tool" messages.
- "name" is only valid on role="tool" messages.
- Provide either content_base64 or file_ids, not both
- Provide either content_base64 or file_ids
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/c9ff19f1d13525f7.
Report an issue: GitHub.