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
- Make each tool call a full dict: {'id':str,'type':'function','function':{'name':str,'arguments':json_str}}.
- When echoing back assistant history from another provider, dump tool call objects with model_dump() (Pydantic) or equivalent before inserting them.
- 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
- Echo tool calls from the model's response object instead of reconstructing them from strings.
- Use .model_dump() when converting Pydantic tool call objects from other SDKs into message history.
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
- Each content item must be a dictionary
- Each content item must have a string `type` field
- OCI only supports function tool calls
- Tool call `id` must be a string
- Tool call `function` must be a dictionary
AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15).
Data as JSON: /api/errors/475123c3a35a4e69.
Report an issue: GitHub.