BerriAI/litellm · error · Exception

Content must be a string

Error message

Content must be a string

What it means

Thrown by Bytez's content normalization when a message's `content` field is not a str, dict, or list (e.g. an int, None, or bytes). The helper only understands string, single-dict, and list-of-parts shapes; anything else is rejected client-side before an HTTP request is made.

Source

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

        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:
            type = content_item["type"]
            if type == "text":
                value: str = content_item["text"]
                words = value.split(" ")

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Wrap non-string content in `str(...)` or `json.dumps(...)` before the call.
  2. Replace `content: None` with `content: ""` or omit the message.
  3. Validate message shape client-side with a typed message builder.
  4. Re-check the failing message index from the traceback and fix that specific entry.

Example fix

// before
messages = [{"role": "user", "content": user_id}]  # int

// after
messages = [{"role": "user", "content": str(user_id)}]
Defensive patterns

Strategy: validation

Validate before calling

def validate_message_shape(messages):
    for i, m in enumerate(messages):
        c = m.get("content")
        if not isinstance(c, (str, dict, list)):
            raise ValueError(f"messages[{i}].content must be str/dict/list, got {type(c).__name__}")

Type guard

def has_valid_content(message) -> bool:
    return isinstance(message.get("content"), (str, dict, list))

Prevention

When it happens

Trigger: Sending `messages=[{"role": "user", "content": 42}]` or `content: None` to a Bytez model. Also happens when tool/result messages are constructed with non-string payloads (e.g. raw JSON bytes or a number) that other providers tolerate.

Common situations: Interpolating a computed value into content without `str()` conversion; forwarding `None` content for assistant/tool messages; passing structured data (dict output of a function) as the whole content instead of `json.dumps(data)`.

Related errors


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