{"record":{"id":"95b821b0627deadd","repo":"BerriAI/litellm","slug":"message-tool-calls-must-be-a-list","errorCode":null,"errorMessage":"Message `tool_calls` must be a list","messagePattern":"Message `tool_calls` must be a list","errorType":"exception","errorClass":"OCIError","httpStatus":400,"severity":"error","filePath":"litellm/llms/oci/chat/generic.py","lineNumber":182,"sourceCode":"        toolCalls=None,\n        toolCallId=tool_call_id,\n    )\n\n\ndef adapt_messages_to_generic_oci_standard(\n    messages: list[AllMessageValues],\n) -> list[OCIMessage]:\n    \"\"\"Convert an OpenAI-format message array to OCI GENERIC format.\"\"\"\n    new_messages: Final = []\n    for message in messages:\n        role = message[\"role\"]\n        content = message.get(\"content\")\n        tool_calls = message.get(\"tool_calls\")\n        tool_call_id = message.get(\"tool_call_id\")\n\n        if role == \"assistant\" and tool_calls is not None:\n            if not isinstance(tool_calls, list):\n                raise OCIError(status_code=400, message=\"Message `tool_calls` must be a list\")\n            new_messages.append(adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls))\n\n        elif role in [\"system\", \"user\", \"assistant\"] and content is not None:\n            if not isinstance(content, (str, list)):\n                raise OCIError(\n                    status_code=400,\n                    message=\"Message `content` must be a string or list of content parts\",\n                )\n            new_messages.append(adapt_messages_to_generic_oci_standard_content_message(role, content))\n\n        elif role == \"tool\":\n            if not isinstance(tool_call_id, str):\n                raise OCIError(\n                    status_code=400,\n                    message=\"Tool result message must have a string `tool_call_id`\",\n                )\n            if not isinstance(content, str):\n                raise OCIError(","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/oci/chat/generic.py#L164-L200","documentation":"In the top-level message loop, an assistant message with a non-None `tool_calls` must have tool_calls as a list. Anything else (a single dict, a JSON string, an int) raises OCIError 400 before per-item validation. This is the coarse type gate that precedes adapt_messages_to_generic_oci_standard_tool_call.","triggerScenarios":"messages=[{\"role\":\"assistant\",\"tool_calls\":{\"id\":..., \"function\":...}}] (single object not wrapped in a list) or tool_calls=\"[...]\" (stringified) sent to an oci/generic.* model. Only fires when role == \"assistant\" and tool_calls is not None.","commonSituations":"Wrapping a single tool call without brackets; history stored as JSON and json.loads applied at the wrong level; frameworks returning a bare object for single calls; copy-paste from OpenAI docs examples that show the object form inline.","solutions":["Wrap tool calls in a list: \"tool_calls\": [call] even for a single call","json.loads the whole message list, not individual fields piecemeal","Normalize at the boundary: msg[\"tool_calls\"] = list(msg[\"tool_calls\"].values()) style fixes in your adapter","Type-check messages before dispatch (type guard below)"],"exampleFix":"# before\n{\"role\":\"assistant\",\"content\":None,\"tool_calls\":{\"id\":\"1\",\"type\":\"function\",\"function\":{\"name\":\"f\",\"arguments\":\"{}\"}}}\n\n# after\n{\"role\":\"assistant\",\"content\":None,\"tool_calls\":[{\"id\":\"1\",\"type\":\"function\",\"function\":{\"name\":\"f\",\"arguments\":\"{}\"}}]}","handlingStrategy":"validation","validationCode":"import json\n\ndef normalize_assistant_messages(messages: list) -> list:\n    out = []\n    for m in messages:\n        tc = m.get(\"tool_calls\")\n        if m.get(\"role\") == \"assistant\" and tc is not None and not isinstance(tc, list):\n            tc = [tc] if isinstance(tc, dict) else json.loads(tc)\n            m = {**m, \"tool_calls\": tc}\n        out.append(m)\n    return out","typeGuard":"def tool_calls_is_list(m) -> bool:\n    tc = m.get(\"tool_calls\")\n    return tc is None or isinstance(tc, list)","tryCatchPattern":null,"preventionTips":["Always wrap tool calls in a list, including the single-call case","json.loads the serialized message array once, at the top level"],"tags":["oci","tool-calls","request-validation","message-history"],"backgroundTag":"request-body-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}