BerriAI/litellm · error · OCIError

Each tool call must be a dictionary

Error message

Each tool call must be a dictionary

What it means

When converting an assistant message that carries `tool_calls` to OCI GENERIC format, each element of the tool_calls list must be a dict (an OpenAI tool-call object). A non-dict element (string, tuple, serialized JSON) raises OCIError 400 during message adaptation. Fail-fast client-side validation of replayed assistant tool-call messages.

Solutions

  1. Deserialize before sending: tool_calls=[json.loads(tc) if isinstance(tc, str) else tc for tc in msg["tool_calls"]]
  2. Keep tool_calls as plain dicts end-to-end in your history store (avoid str()/repr() round-trips)
  3. Echo back the exact list you received from a previous litellm/OpenAI response without transformation
  4. Add a shape assertion on persisted history at load time

Example fix

# before (history reloaded from DB with stringified calls)
msg = {"role": "assistant", "content": None, "tool_calls": [json.dumps(tc)]}

# after
msg = {"role": "assistant", "content": None, "tool_calls": [tc]}  # keep dicts
Defensive patterns

Strategy: validation

Validate before calling

import json

def normalize_tool_calls(tool_calls: list) -> list:
    return [json.loads(tc) if isinstance(tc, str) else tc for tc in tool_calls]

Type guard

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

Prevention

When it happens

Trigger: Replaying a conversation history where an assistant message has tool_calls=["{...}"] (JSON-stringified call) or a tuple — common when history round-tripped through a queue/DB and items were serialized. Only assistant messages with tool_calls != None hit this path, for oci/generic.* models.

Common situations: Storing tool_calls as JSON strings in a DB and loading them back without json.loads; LangChain intermediate representations; manually echoing the model's tool_calls but double-encoding; message dicts built from LLM-generated JSON.

Related errors


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

Appendix: 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 77b7c6c40c)