BerriAI/litellm · error · OCIError

Each content item must be a dictionary

Error message

Each content item must be a dictionary

What it means

When translating an OpenAI-format chat message to OCI GENERIC format, LiteLLM iterates multipart `content` arrays and requires every element to be a dict (an OpenAI content-part object). Encountering a non-dict item (e.g. a bare string) raises OCIError 400 before any network call. This is strict input validation: OCI GENERIC accepts typed content parts, so raw strings inside a list are rejected early.

Solutions

  1. Wrap bare strings in a text part: [{"type":"text","text":"just a string"}]
  2. Or hoist the whole message to plain string content: {"role":"user","content":"just a string"}
  3. Validate/normalize your message builder so content lists only ever contain dicts with a type key
  4. If a framework feeds you mixed lists, map str -> {"type":"text","text":s} before calling litellm

Example fix

# before
messages = [{"role": "user", "content": ["analyze this"]}]
litellm.completion(model="oci/generic.my-model", messages=messages)  # 400

# after
messages = [{"role": "user", "content": [{"type": "text", "text": "analyze this"}]}]
Defensive patterns

Strategy: validation

Validate before calling

def normalize_content(content):
    if isinstance(content, str):
        return content
    return [
        {"type": "text", "text": item} if isinstance(item, str) else item
        for item in content
    ]

messages = [{"role": "user", "content": normalize_content(msg["content"])} for msg in messages]

Type guard

def is_valid_oci_content(content) -> bool:
    if isinstance(content, str):
        return True
    return isinstance(content, list) and all(isinstance(p, dict) for p in content)

Prevention

When it happens

Trigger: Calling litellm.completion(model="oci/generic...", messages=[{"role":"user","content":["just a string"]}]) — a content list whose element is not a dict. Also triggered by frameworks that build content lists with mixed str/dict elements before forwarding to LiteLLM.

Common situations: Hand-built content arrays mixing strings and part objects; LangChain/custom wrappers that append plain text into a multimodal content list; migrating OpenAI calls (which tolerate lenient shapes) to OCI GENERIC models; data from user input spliced unvalidated into content.

Related errors


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

Appendix: source

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

# ---------------------------------------------------------------------------
# Message building
# ---------------------------------------------------------------------------


def adapt_messages_to_generic_oci_standard_content_message(role: str, content: str | list) -> OCIMessage:
    """Convert a plain-text or multipart content message to OCI format."""
    new_content: Final[list[OCIContentPartUnion]] = []
    if isinstance(content, str):
        return OCIMessage(
            role=open_ai_to_generic_oci_role_map[role],
            content=[OCITextContentPart(text=content)],
            toolCalls=None,
            toolCallId=None,
        )

    for content_item in content:
        if not isinstance(content_item, dict):
            raise OCIError(status_code=400, message="Each content item must be a dictionary")

        item_type = content_item.get("type")
        if not isinstance(item_type, str):
            raise OCIError(
                status_code=400,
                message="Each content item must have a string `type` field",
            )
        if item_type not in ["text", "image_url"]:
            raise OCIError(
                status_code=400,
                message=f"Content type `{item_type}` is not supported by OCI",
            )

        if item_type == "text":
            text = content_item.get("text")
            if not isinstance(text, str):
                raise OCIError(
                    status_code=400,

View on GitHub (pinned to 77b7c6c40c)