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

  1. Always use a list: 'tool_calls': [tc] even for one call.
  2. json.loads() any stringified tool_calls before putting them into messages.
  3. 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

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


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/95b821b0627deadd. Report an issue: GitHub.