BerriAI/litellm · error · OCIError
Message `content` must be a string or list of content parts
Error message
Message `content` must be a string or list of content parts
What it means
For system/user/assistant messages with non-None content, OCI GENERIC accepts only a plain string or a list of content parts. Any other type (dict, None-adjacent ints, bytes, a single content-part object not wrapped in a list) raises OCIError 400 at translation time. Note: None content is allowed to fall through (e.g. tool-call-only assistant messages).
Solutions
- Use a plain string for simple prompts, or a list of part dicts for multimodal
- Wrap a lone part in a list: content=[part]
- Decode bytes: content = data.decode("utf-8")
- Coerce scalars: content = str(value)
Example fix
# before
{"role": "user", "content": {"type": "text", "text": "hi"}}
# after
{"role": "user", "content": "hi"}
# or
{"role": "user", "content": [{"type": "text", "text": "hi"}]} Defensive patterns
Strategy: type-guard
Validate before calling
def normalize_message_content(messages: list) -> list:
out = []
for m in messages:
c = m.get("content")
if c is not None and not isinstance(c, (str, list)):
if isinstance(c, bytes):
c = c.decode("utf-8")
elif isinstance(c, dict):
c = [c]
else:
c = str(c)
m = {**m, "content": c}
out.append(m)
return out Type guard
def message_content_valid(m) -> bool:
c = m.get("content")
return c is None or isinstance(c, (str, list)) Prevention
- Type your message builders: content: str | list[dict] — never Any
- Decode bytes and stringify scalars at the input edge, not deep in call paths
When it happens
Trigger: messages=[{"role":"user","content":{"type":"text","text":"hi"}}] (part object instead of list), content=42, or content=b"bytes" against an oci/generic.* model. Fires whenever content is not None and not str/list.
Common situations: Passing a single content part where a list is required; content built as dict by a template engine; bytes from file reads passed un-decoded; numbers from form inputs inserted directly.
Related errors
- Each content item must be a dictionary
- Content item of type `text` must have a string `text` field
- Content type ` ` is not supported by OCI
- Each content item must have a string `type` field
- Each tool call must be a dictionary
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/bb10f59ed862b7bc.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/llms/oci/chat/generic.py:187
def adapt_messages_to_generic_oci_standard(
messages: list[AllMessageValues],
) -> list[OCIMessage]:
"""Convert an OpenAI-format message array to OCI GENERIC format."""
new_messages: Final = []
for message in messages:
role = message["role"]
content = message.get("content")
tool_calls = message.get("tool_calls")
tool_call_id = message.get("tool_call_id")
if role == "assistant" and tool_calls is not None:
if not isinstance(tool_calls, list):
raise OCIError(status_code=400, message="Message `tool_calls` must be a list")
new_messages.append(adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls))
elif role in ["system", "user", "assistant"] and content is not None:
if not isinstance(content, (str, list)):
raise OCIError(
status_code=400,
message="Message `content` must be a string or list of content parts",
)
new_messages.append(adapt_messages_to_generic_oci_standard_content_message(role, content))
elif role == "tool":
if not isinstance(tool_call_id, str):
raise OCIError(
status_code=400,
message="Tool result message must have a string `tool_call_id`",
)
if not isinstance(content, str):
raise OCIError(
status_code=400,
message="Tool result message `content` must be a string",
)
new_messages.append(adapt_messages_to_generic_oci_standard_tool_response(role, tool_call_id, content))
View on GitHub (pinned to 77b7c6c40c)