BerriAI/litellm · error · Exception

Content must be a string

Error message

Content must be a string

What it means

Thrown by litellm's Bytez adapter in _adapt_string_only_content_to_lists (litellm/llms/bytez/chat/transformation.py:446) when a message's content is neither a str, a dict, nor a list. The Bytez API expects content as an OpenAI-style list of parts, so the adapter normalizes strings/dicts/lists and rejects anything else. In practice this means content was None, a number, or some other object. Assistant messages with tool_calls frequently carry content: null in OpenAI-shaped payloads and trigger it.

Solutions

  1. Set every message's content to a string, an OpenAI content-part dict, or a list of those — replace content: null with "" on assistant/tool-call messages
  2. If you pass a list, ensure every element is either a plain string or an OpenAI content dict like {"type": "text", "text": "..."}
  3. Sanitize the message list right before the call (see validation snippet) and drop or stringify None values
  4. If you need tool-calling semantics, verify the Bytez provider actually supports tool_calls before forwarding assistant history

Example fix

# before
messages = [
    {"role": "user", "content": "hi"},
    {"role": "assistant", "content": None, "tool_calls": [...]},
]
litellm.completion(model="bytez/your-model", messages=messages)

# after
messages = [
    {"role": "user", "content": "hi"},
    {"role": "assistant", "content": "", "tool_calls": [...]},
]
litellm.completion(model="bytez/your-model", messages=messages)
Defensive patterns

Strategy: type-guard

Validate before calling

def has_valid_bytez_content(messages):
    for i, m in enumerate(messages):
        c = m.get("content")
        if not isinstance(c, (str, dict, list)):
            return False, f"messages[{i}] content is {type(c).__name__}"
        if isinstance(c, list):
            for j, part in enumerate(c):
                if not isinstance(part, (str, dict)):
                    return False, f"messages[{i}].content[{j}] is {type(part).__name__}"
    return True, None

ok, why = has_valid_bytez_content(messages)
if not ok:
    raise ValueError(f"Bytez request invalid: {why}")

Type guard

from typing import Any

def is_valid_message_content(content: Any) -> bool:
    """True when Bytez accepts the content value (str, dict, or list of str/dict)."""
    if isinstance(content, (str, dict)):
        return True
    if isinstance(content, list):
        return all(isinstance(p, (str, dict)) for p in content)
    return False  # includes None

Try / catch

try:
    litellm.completion(model="bytez/...", messages=messages)
except Exception as e:
    if "Content must be a string" in str(e):
        messages = [
            {**m, "content": m.get("content") if m.get("content") is not None else ""}
            for m in messages
        ]
        litellm.completion(model="bytez/...", messages=messages)
    else:
        raise

Prevention

When it happens

Trigger: Calling completion() with a bytez/* model and a message whose content is None (e.g. an assistant tool_call history entry with content omitted), an int/float, or any non str/dict/list object. Also triggered when a list item in content is not a string or dict (sibling error at line 442).

Common situations: Replaying OpenAI chat histories that contain {"role": "assistant", "content": null, "tool_calls": [...]} into a bytez model; building messages dynamically and leaving content unset; sending pre-formatted or non-OpenAI message shapes; agents that serialize tool output into non-string content fields.

Related errors


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

Appendix: 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 77b7c6c40c)