BerriAI/litellm · error · Exception

`content` can only contain strings or openai content dicts

Error message

`content` can only contain strings or openai content dicts

What it means

As a pre-step, _adapt_string_only_content_to_lists normalizes message content: a list content is fine only if every item is a str or a dict; any other item type (None, int, a nested list, an object) raises this generic Exception. Strings become text parts; dicts pass through to the later per-part validation.

Solutions

  1. Filter or fix the list before the call: keep only strings and valid content dicts.
  2. Replace None placeholders by omitting the part entirely.
  3. Convert non-string scalars with str() when they are meant to be text.

Example fix

# before
content = ["describe", image_url_or_none]  # None item -> Exception
messages = [{"role": "user", "content": content}]

# after
content = [c for c in ["describe", image_url_or_none] if c is not None]
messages = [{"role": "user", "content": content}]
Defensive patterns

Strategy: validation

Validate before calling

def clean_content_list(content: list) -> list:
    cleaned = []
    for item in content:
        if isinstance(item, str):
            cleaned.append({"type": "text", "text": item})
        elif isinstance(item, dict):
            cleaned.append(item)
        # None / numbers / nested lists are dropped
    return cleaned

Type guard

def is_bytez_content_list(content: object) -> bool:
    return isinstance(content, list) and all(
        isinstance(i, (str, dict)) for i in content
    )

Try / catch

try:
    litellm.completion(model="bytez/org/model", messages=messages)
except Exception as e:
    if "can only contain strings or openai content dicts" in str(e):
        messages = [
            {"role": m["role"], "content": clean_content_list(m["content"])}
            for m in messages
        ]
        litellm.completion(model="bytez/org/model", messages=messages)
    else:
        raise

Prevention

When it happens

Trigger: content=["hi", None] (a None slipped in from an optional field); content=[42] or content=[["a", "b"]] (nested list); JSON-decoded payloads where numbers/booleans appear as content items.

Common situations: Optional image/text slots defaulting to None instead of being omitted; f-string or template bugs inserting non-strings; data from external systems not schema-checked before reaching the LLM call.

Related errors


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

Appendix: source

Thrown at litellm/llms/bytez/chat/transformation.py:442

        content = message.get("content")

        new_content = []

        if isinstance(content, str):
            new_content.append({"type": "text", "text": content})

        elif isinstance(content, dict):
            new_content.append(content)

        elif isinstance(content, list):
            new_content_items = []
            for content_item in content:
                if isinstance(content_item, str):
                    new_content_items.append({"type": "text", "text": content_item})
                elif isinstance(content_item, dict):
                    new_content_items.append(content_item)
                else:
                    raise Exception("`content` can only contain strings or openai content dicts")

            new_content += new_content_items
        else:
            raise Exception("Content must be a string")

        new_messages.append({"role": role, "content": new_content})

    return new_messages


# TODO get this from the api instead of doing it here, will require backend work
def get_tokens_from_messages(messages: list[dict]):
    total = 0

    for message in messages:
        content: list[dict] = message["content"]

        for content_item in content:

View on GitHub (pinned to 77b7c6c40c)