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

  1. Delete one of the two fields: keep content for normal replies, refusal for refusals.
  2. When ingesting provider payloads, set content=None if refusal is non-null.
  3. 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

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


AI-assisted analysis of mlflow/mlflow@6a27f2decc (2026-08-29). Data as JSON: /api/errors/1e160710b9094f86. Report an issue: GitHub.