BerriAI/litellm · error · OCIError
Message `tool_calls` must be a list
Error message
Message `tool_calls` must be a list
What it means
At the message level, when an assistant message has a non-None 'tool_calls' field, that field must be a list. Anything else (string, dict, None-checked-elsewhere objects) raises OCIError(400) 'Message `tool_calls` must be a list' in adapt_messages_to_generic_oci_standard before an OCI request is built.
Source
Thrown at litellm/llms/oci/chat/generic.py:182
toolCalls=None,
toolCallId=tool_call_id,
)
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(View on GitHub (pinned to 6c2dcb801b)
Solutions
- Always use a list: 'tool_calls': [tc] even for one call.
- json.loads() any stringified tool_calls before putting them into messages.
- Configure JSON serializers not to collapse single-element arrays to scalars.
Example fix
# before
{'role':'assistant','tool_calls': {'id':'c1','type':'function','function':{...}}}
# after
{'role':'assistant','tool_calls': [{'id':'c1','type':'function','function':{'name':'f','arguments':'{}'}}]} Defensive patterns
Strategy: validation
Validate before calling
for msg in messages:
tcs = msg.get('tool_calls')
if isinstance(tcs, dict):
msg['tool_calls'] = [tcs] # wrap single call
elif isinstance(tcs, str):
import json
msg['tool_calls'] = json.loads(tcs) # parse stored string
assert msg.get('tool_calls') is None or isinstance(msg['tool_calls'], list) Type guard
def tool_calls_is_list(msg: object) -> bool:
return not (isinstance(msg, dict) and msg.get('tool_calls') is not None) or isinstance(msg.get('tool_calls'), list) Prevention
- Always emit tool_calls as an array, even for one element.
- If persisting conversations, parse JSON columns back into objects before sending.
When it happens
Trigger: Sending {'role':'assistant','tool_calls': {'id':...}} (single object instead of list) or tool_calls as a serialized JSON string to an oci/ GENERIC model.
Common situations: Wrapping a single tool call without brackets; storing tool_calls as a JSON string in a DB column and passing it through unparsed; serializers emitting an object for single-element arrays.
Related errors
- Tool call `function` must be a dictionary
- Each tool call must be a dictionary
- OCI only supports function tool calls
- Tool call `id` must be a string
- Tool call `function.name` must be a string
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/95b821b0627deadd.
Report an issue: GitHub.