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 that carry content, the OCI GENERIC adapter accepts content as either a plain string or a list of content-part dicts. Any other type (dict, int, None-handled-elsewhere objects, bytes) raises OCIError(400) 'Message `content` must be a string or list of content parts' client-side.

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 6c2dcb801b)

Solutions

  1. Use 'content': 'plain text' or 'content': [{'type':'text','text':'...'}, ...].
  2. Coerce unknown content: str() scalars, or wrap dicts into part lists, before calling litellm.
  3. Add a pre-flight type check on every message's content field.

Example fix

# before
{'role':'user','content': {'text':'hello'}}

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

Strategy: type-guard

Validate before calling

def normalize_message_content(msg):
    content = msg.get('content')
    if isinstance(content, dict):
        msg['content'] = [content] if set(content) & {'type', 'text', 'image_url'} else str(content)
    elif not isinstance(content, (str, list, type(None))):
        msg['content'] = str(content)
    return msg

Type guard

def message_content_is_valid(msg: object) -> bool:
    if not isinstance(msg, dict):
        return False
    c = msg.get('content')
    return c is None or isinstance(c, (str, list))

Prevention

When it happens

Trigger: Sending {'role':'user','content': {'text':'hi'}} (dict instead of list of parts), content as a number, or content as raw bytes to an oci/ GENERIC model.

Common situations: Data-driven prompts where content is sometimes a single object; YAML/JSON configs parsed into dicts and passed through unmodified; content accidentally left as a template object or lazy proxy.

Related errors


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