BerriAI/litellm · error · OCIError
Tool call `function.arguments` must be a JSON string
Error message
Tool call `function.arguments` must be a JSON string
What it means
OCI GENERIC carries a tool call's arguments as a JSON string (matching OpenAI's convention), so function.arguments must be a str. A dict, list, None-as-absent-then-dict, or number raises OCIError 400. Note the translator defaults missing arguments to "{}" — only a present-but-non-string value fails.
Solutions
- Serialize dict arguments back to a JSON string: json.dumps(args) if isinstance(args, dict) else args
- Send arguments exactly as received from the previous model response (always a string there)
- Add a normalization pass over history before OCI calls (see validationCode)
- Beware mixed histories: normalize per-item, not globally
Example fix
# before
{"function": {"name": "get_weather", "arguments": {"city": "SF"}}}
# after
{"function": {"name": "get_weather", "arguments": "{\"city\":\"SF\"}"}} Defensive patterns
Strategy: validation
Validate before calling
import json
def normalize_tool_arguments(tool_calls: list) -> list:
out = []
for tc in tool_calls:
fn = tc.get("function", {})
args = fn.get("arguments", "{}")
if not isinstance(args, str):
args = json.dumps(args)
tc = {**tc, "function": {**fn, "arguments": args}}
out.append(tc)
return out Type guard
def tool_arguments_is_json_string(tc) -> bool:
args = tc.get("function", {}).get("arguments", "{}")
return isinstance(args, str) Prevention
- One canonical history format: arguments always a JSON string; convert at both load and save boundaries
- Never auto-parse arguments in stored history without re-serializing on the way out
When it happens
Trigger: Replaying assistant tool_calls where arguments was left as a parsed dict (common after json.loads of the whole history or from frameworks that auto-parse), e.g. {"function":{"name":"f","arguments":{"city":"SF"}}} to an oci/generic.* model.
Common situations: Auto-parsing arguments for display/editing and forgetting to re-serialize; frameworks (some agent libs) exposing arguments as dicts; hand-constructed tool_calls from code; LLM-generated JSON where arguments is naturally an object.
Related errors
- Each tool call must be a dictionary
- Message `tool_calls` must be a list
- OCI only supports function tool calls
- Tool call `function` must be a dictionary
- Tool call `function.name` must be a string
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/8554522c47658361.
Report an issue: GitHub.
Appendix: source
Thrown at litellm/llms/oci/chat/generic.py:137
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(
status_code=400,
message="Tool call `function.arguments` must be a JSON string",
)
tool_calls_formatted.append(
OCIToolCall(
id=tool_call_id,
type="FUNCTION",
name=function_name,
arguments=arguments,
)
)
return OCIMessage(
role=open_ai_to_generic_oci_role_map[role],
content=None,
toolCalls=tool_calls_formatted,
toolCallId=None,View on GitHub (pinned to 77b7c6c40c)