{"record":{"id":"475123c3a35a4e69","repo":"BerriAI/litellm","slug":"each-tool-call-must-be-a-dictionary","errorCode":null,"errorMessage":"Each tool call must be a dictionary","messagePattern":"Each tool call must be a dictionary","errorType":"validation","errorClass":"OCIError","httpStatus":400,"severity":"error","filePath":"litellm/llms/oci/chat/generic.py","lineNumber":119,"sourceCode":"                    status_code=400,\n                    message=\"Prop `image_url` must be a string or an object with a `url` property\",\n                )\n            new_content.append(OCIImageContentPart(imageUrl=OCIImageUrl(url=image_url)))\n\n    return OCIMessage(\n        role=open_ai_to_generic_oci_role_map[role],\n        content=new_content,\n        toolCalls=None,\n        toolCallId=None,\n    )\n\n\ndef adapt_messages_to_generic_oci_standard_tool_call(role: str, tool_calls: list) -> OCIMessage:\n    \"\"\"Convert an assistant tool-call message to OCI format.\"\"\"\n    tool_calls_formatted: Final = []\n    for tool_call in tool_calls:\n        if not isinstance(tool_call, dict):\n            raise OCIError(status_code=400, message=\"Each tool call must be a dictionary\")\n        if tool_call.get(\"type\") != \"function\":\n            raise OCIError(status_code=400, message=\"OCI only supports function tool calls\")\n\n        tool_call_id = tool_call.get(\"id\")\n        if not isinstance(tool_call_id, str):\n            raise OCIError(status_code=400, message=\"Tool call `id` must be a string\")\n\n        tool_function = tool_call.get(\"function\")\n        if not isinstance(tool_function, dict):\n            raise OCIError(status_code=400, message=\"Tool call `function` must be a dictionary\")\n\n        function_name = tool_function.get(\"name\")\n        if not isinstance(function_name, str):\n            raise OCIError(status_code=400, message=\"Tool call `function.name` must be a string\")\n\n        arguments = tool_call[\"function\"].get(\"arguments\", \"{}\")\n        if not isinstance(arguments, str):\n            raise OCIError(","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/oci/chat/generic.py#L101-L137","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Deserialize before sending: tool_calls=[json.loads(tc) if isinstance(tc, str) else tc for tc in msg[\"tool_calls\"]]","Keep tool_calls as plain dicts end-to-end in your history store (avoid str()/repr() round-trips)","Echo back the exact list you received from a previous litellm/OpenAI response without transformation","Add a shape assertion on persisted history at load time"],"exampleFix":"# before (history reloaded from DB with stringified calls)\nmsg = {\"role\": \"assistant\", \"content\": None, \"tool_calls\": [json.dumps(tc)]}\n\n# after\nmsg = {\"role\": \"assistant\", \"content\": None, \"tool_calls\": [tc]}  # keep dicts","handlingStrategy":"validation","validationCode":"import json\n\ndef normalize_tool_calls(tool_calls: list) -> list:\n    return [json.loads(tc) if isinstance(tc, str) else tc for tc in tool_calls]","typeGuard":"def has_valid_tool_call_items(tool_calls) -> bool:\n    return isinstance(tool_calls, list) and all(isinstance(tc, dict) for tc in tool_calls)","tryCatchPattern":null,"preventionTips":["Store tool_calls as structured dicts in history, never stringified","json.loads at the exact level you json.dumps'd — one level only"],"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"}