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
- 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
- If you pass a list, ensure every element is either a plain string or an OpenAI content dict like {"type": "text", "text": "..."}
- Sanitize the message list right before the call (see validation snippet) and drop or stringify None values
- 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
- Normalize assistant/tool-call history entries to content: "" before sending to bytez models
- Validate message shape once at your app boundary instead of per call site
- Keep message construction in one helper so content is always typed str | list[dict]
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
- Bedrock batch embedding `input` must be a string (or a…
- `content` can only contain strings or openai content dicts
- e.response.text
- error - , Received response - , Type of response
- extract_file_data does not accept bare str inputs. Pass…
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)