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
- Filter or fix the list before the call: keep only strings and valid content dicts.
- Replace None placeholders by omitting the part entirely.
- 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
- Omit optional content slots entirely rather than inserting None placeholders.
- Schema-check inbound message payloads at your API boundary, not inside the LLM client.
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
- Prop `type` is not a string
- Prop ` ` is not supported
- Prop ` ` is not a string
- Each content item must be a dictionary
- kwarg `messages` must be an array of messages that follow…
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)