BerriAI/litellm · error · OCIError

Message `content` must be a string or list of content parts

Error message

Message `content` must be a string or list of content parts

What it means

For system/user/assistant messages with non-None content, OCI GENERIC accepts only a plain string or a list of content parts. Any other type (dict, None-adjacent ints, bytes, a single content-part object not wrapped in a list) raises OCIError 400 at translation time. Note: None content is allowed to fall through (e.g. tool-call-only assistant messages).

Solutions

  1. Use a plain string for simple prompts, or a list of part dicts for multimodal
  2. Wrap a lone part in a list: content=[part]
  3. Decode bytes: content = data.decode("utf-8")
  4. Coerce scalars: content = str(value)

Example fix

# before
{"role": "user", "content": {"type": "text", "text": "hi"}}

# after
{"role": "user", "content": "hi"}
# or
{"role": "user", "content": [{"type": "text", "text": "hi"}]}
Defensive patterns

Strategy: type-guard

Validate before calling

def normalize_message_content(messages: list) -> list:
    out = []
    for m in messages:
        c = m.get("content")
        if c is not None and not isinstance(c, (str, list)):
            if isinstance(c, bytes):
                c = c.decode("utf-8")
            elif isinstance(c, dict):
                c = [c]
            else:
                c = str(c)
            m = {**m, "content": c}
        out.append(m)
    return out

Type guard

def message_content_valid(m) -> bool:
    c = m.get("content")
    return c is None or isinstance(c, (str, list))

Prevention

When it happens

Trigger: messages=[{"role":"user","content":{"type":"text","text":"hi"}}] (part object instead of list), content=42, or content=b"bytes" against an oci/generic.* model. Fires whenever content is not None and not str/list.

Common situations: Passing a single content part where a list is required; content built as dict by a template engine; bytes from file reads passed un-decoded; numbers from form inputs inserted directly.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/bb10f59ed862b7bc. Report an issue: GitHub.

Appendix: source

Thrown at litellm/llms/oci/chat/generic.py:187

def adapt_messages_to_generic_oci_standard(
    messages: list[AllMessageValues],
) -> list[OCIMessage]:
    """Convert an OpenAI-format message array to OCI GENERIC format."""
    new_messages: Final = []
    for message in messages:
        role = message["role"]
        content = message.get("content")
        tool_calls = message.get("tool_calls")
        tool_call_id = message.get("tool_call_id")

        if role == "assistant" and tool_calls is not None:
            if not isinstance(tool_calls, list):
                raise OCIError(status_code=400, message="Message `tool_calls` must be a list")
            new_messages.append(adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls))

        elif role in ["system", "user", "assistant"] and content is not None:
            if not isinstance(content, (str, list)):
                raise OCIError(
                    status_code=400,
                    message="Message `content` must be a string or list of content parts",
                )
            new_messages.append(adapt_messages_to_generic_oci_standard_content_message(role, content))

        elif role == "tool":
            if not isinstance(tool_call_id, str):
                raise OCIError(
                    status_code=400,
                    message="Tool result message must have a string `tool_call_id`",
                )
            if not isinstance(content, str):
                raise OCIError(
                    status_code=400,
                    message="Tool result message `content` must be a string",
                )
            new_messages.append(adapt_messages_to_generic_oci_standard_tool_response(role, tool_call_id, content))

View on GitHub (pinned to 77b7c6c40c)