mlflow/mlflow · error
Both `content` and `refusal` cannot be set
Error message
Both `content` and `refusal` cannot be set
What it means
In ChatMessage.__post_init__, MLflow enforces OpenAI-style semantics: a message may carry either content or refusal, not both. If refusal is truthy and content is also truthy (string or multimodal list), this ValueError is raised before any other validation.
Source
Thrown at mlflow/types/llm.py:212
**Optional** defaults to ``None``
"""
role: str
content: str | list[dict[str, Any]] | None = None
refusal: str | None = None
name: str | None = None
tool_calls: list[ToolCall] | None = None
tool_call_id: str | None = None
def __post_init__(self):
self._validate_field("role", str, True)
# The refusal/content mutual-exclusion invariant applies regardless of whether
# content is a str or a multimodal list, so check it before branching on type.
if self.refusal:
self._validate_field("refusal", str, True)
if self.content:
raise ValueError("Both `content` and `refusal` cannot be set")
if isinstance(self.content, list):
# Multimodal content is a list of content-part dicts (e.g. text and image_url
# blocks); validate the shape lightly (each part is a dict) rather than the
# str-content check, then validate the remaining fields as usual.
if not all(isinstance(part, dict) for part in self.content):
raise ValueError("`content` list items must all be dicts (content parts)")
elif not self.refusal:
if self.tool_calls:
self._validate_field("content", str, False)
else:
self._validate_field("content", str, True)
self._validate_field("name", str, False)
self._convert_dataclass_list("tool_calls", ToolCall, False)
self._validate_field("tool_call_id", str, False)
View on GitHub (pinned to 6a27f2decc)
Solutions
- Delete one of the two fields: keep content for normal replies, refusal for refusals.
- When ingesting provider payloads, set content=None if refusal is non-null.
- Add a preprocessing step that enforces mutual exclusion before constructing ChatMessage.
Example fix
// before ChatMessage(role="assistant", content="Here you go", refusal="I can't help with that") // after ChatMessage(role="assistant", refusal="I can't help with that")
Defensive patterns
Strategy: validation
Validate before calling
def clean_message(d):
d = dict(d)
if d.get("refusal"):
d["content"] = None
return d Type guard
def is_content_refusal_safe(d):
return not (d.get("refusal") and d.get("content")) Try / catch
try:
msg = ChatMessage(**raw)
except ValueError as e:
if "Both `content` and `refusal`" in str(e):
msg = ChatMessage(**{**raw, "content": None})
else:
raise Prevention
- Sanitize provider payloads: refusal wins, content becomes None.
- Never set both fields when synthesizing refusal messages.
- Add a fixture test covering provider responses that include both keys.
When it happens
Trigger: ChatMessage(role='assistant', content='...', refusal='...'), or merging a provider response object where both keys were populated, or constructing messages from a raw API response that includes both fields non-null.
Common situations: Passing through raw OpenAI/compatible-provider JSON without filtering, building refusal fallbacks on top of existing content, or copying chat history that retained both keys.
Related errors
- base_model must be a non-empty string (HuggingFace model ID
- Unsupported adapter type: {adapter_type}. Supported types: {
- Template must be a list of dicts with role and content
- The gateway configuration is invalid: {e}
- Tags must be a dictionary, got {type(tags).__name__}.
AI-assisted analysis of mlflow/mlflow@6a27f2decc (2026-08-29).
Data as JSON: /api/errors/1e160710b9094f86.
Report an issue: GitHub.