BerriAI/litellm · error · OCIError

Each tool call must be a dictionary

Error message

Each tool call must be a dictionary

What it means

When adapting an assistant message that carries tool_calls, the OCI GENERIC adapter iterates the list and requires every element to be a dict. A non-dict element (string, tuple, Pydantic object, None) raises OCIError(400) 'Each tool call must be a dictionary' during request construction.

Source

Thrown at litellm/llms/oci/chat/generic.py:119

                    status_code=400,
                    message="Prop `image_url` must be a string or an object with a `url` property",
                )
            new_content.append(OCIImageContentPart(imageUrl=OCIImageUrl(url=image_url)))

    return OCIMessage(
        role=open_ai_to_generic_oci_role_map[role],
        content=new_content,
        toolCalls=None,
        toolCallId=None,
    )


def adapt_messages_to_generic_oci_standard_tool_call(role: str, tool_calls: list) -> OCIMessage:
    """Convert an assistant tool-call message to OCI format."""
    tool_calls_formatted: Final = []
    for tool_call in tool_calls:
        if not isinstance(tool_call, dict):
            raise OCIError(status_code=400, message="Each tool call must be a dictionary")
        if tool_call.get("type") != "function":
            raise OCIError(status_code=400, message="OCI only supports function tool calls")

        tool_call_id = tool_call.get("id")
        if not isinstance(tool_call_id, str):
            raise OCIError(status_code=400, message="Tool call `id` must be a string")

        tool_function = tool_call.get("function")
        if not isinstance(tool_function, dict):
            raise OCIError(status_code=400, message="Tool call `function` must be a dictionary")

        function_name = tool_function.get("name")
        if not isinstance(function_name, str):
            raise OCIError(status_code=400, message="Tool call `function.name` must be a string")

        arguments = tool_call["function"].get("arguments", "{}")
        if not isinstance(arguments, str):
            raise OCIError(

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Make each tool call a full dict: {'id':str,'type':'function','function':{'name':str,'arguments':json_str}}.
  2. When echoing back assistant history from another provider, dump tool call objects with model_dump() (Pydantic) or equivalent before inserting them.
  3. Add a pre-flight check that all(isinstance(tc, dict) for tc in msg.get('tool_calls', [])).

Example fix

# before
{'role':'assistant','tool_calls': ['call_abc|get_weather|{}']}

# after
{'role':'assistant','tool_calls': [{'id':'call_abc','type':'function','function':{'name':'get_weather','arguments':'{"city":"SF"}'}}]}
Defensive patterns

Strategy: type-guard

Validate before calling

for msg in messages:
    tcs = msg.get('tool_calls')
    if tcs is not None:
        assert all(isinstance(tc, dict) for tc in tcs), 'tool_calls must all be dicts'

Type guard

def are_valid_tool_call_dicts(tool_calls: object) -> bool:
    return isinstance(tool_calls, list) and all(isinstance(tc, dict) for tc in tool_calls)

Prevention

When it happens

Trigger: Sending messages=[{'role':'assistant','tool_calls':['finish()']}] or tool_calls containing objects produced by another SDK (e.g. OpenAI's ChatCompletionMessageToolCall Pydantic instances dumped incompletely) to an oci/ GENERIC model.

Common situations: Replaying captured OpenAI responses where tool_calls were serialized to strings; a conversation store that JSON-round-trips and occasionally flattens tool call dicts; appending hand-written tool call shorthands instead of full dicts.

Related errors


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