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

Thrown by Bytez's `_adapt_string_only_content_to_lists` helper when a message's `content` list contains an element that is neither a `str` nor a `dict`. The helper normalizes mixed string/dict content into a uniform list of dicts; any other Python object (int, None, bytes, Pydantic model) is rejected before the request is sent.

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

Solutions

  1. Serialize every content item to a plain str or dict before calling litellm (e.g. call `.model_dump()` on Pydantic parts).
  2. Check for None elements introduced by conditional appends (`parts.append(x if cond else None)`).
  3. Add a guard like `assert all(isinstance(p, (str, dict)) for p in content)` in dev builds.
  4. Inspect the exact message list with a debug print right before the completion call.

Example fix

// before
content = ["describe", None, image_part_pydantic]

// after
content = ["describe", image_part_pydantic.model_dump()]
Defensive patterns

Strategy: type-guard

Validate before calling

def normalize_content_items(items):
    out = []
    for it in items:
        if isinstance(it, str):
            out.append({"type": "text", "text": it})
        elif isinstance(it, dict):
            out.append(it)
        elif hasattr(it, "model_dump"):
            out.append(it.model_dump())
        else:
            raise TypeError(f"unsupported content item: {type(it)!r}")
    return out

Type guard

def is_valid_content_item(item) -> bool:
    return isinstance(item, (str, dict))

Prevention

When it happens

Trigger: Passing `content: [None]`, `content: [123]`, or a list containing an unserialized object (e.g. a Pydantic message part or PIL Image) to a Bytez completion call. String content and dict content items are fine; everything else raises.

Common situations: Building messages programmatically and accidentally appending a non-serialized object or None; passing OpenAI SDK typed objects (e.g. `ChatCompletionContentPartImage`) directly instead of `.model_dump()` dicts.

Related errors


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