BerriAI/litellm · error · OCIError
Tool result message `content` must be a string
Error message
Tool result message `content` must be a string
What it means
The content of a tool result message (role='tool') must be a plain string in the OCI GENERIC format. Passing a list of content parts, a dict, or None raises OCIError(400) "Tool result message `content` must be a string" during request construction — unlike user messages, tool replies cannot be multipart.
Source
Thrown at litellm/llms/oci/chat/generic.py:200
raise OCIError(status_code=400, message="Message `tool_calls` must be a list")
new_messages.append(adapt_messages_to_generic_oci_standard_tool_call(role, tool_calls))
elif role in ["system", "user", "assistant"] and content is not None:
if not isinstance(content, (str, list)):
raise OCIError(
status_code=400,
message="Message `content` must be a string or list of content parts",
)
new_messages.append(adapt_messages_to_generic_oci_standard_content_message(role, content))
elif role == "tool":
if not isinstance(tool_call_id, str):
raise OCIError(
status_code=400,
message="Tool result message must have a string `tool_call_id`",
)
if not isinstance(content, str):
raise OCIError(
status_code=400,
message="Tool result message `content` must be a string",
)
new_messages.append(adapt_messages_to_generic_oci_standard_tool_response(role, tool_call_id, content))
return new_messages
# ---------------------------------------------------------------------------
# Tool definition adaptation
# ---------------------------------------------------------------------------
def adapt_tool_definition_to_oci_standard(tools: list[dict], vendor: OCIVendors) -> list[OCIToolDefinition]:
"""Convert OpenAI-format tool definitions to OCI GENERIC format.
Resolves ``$ref``/``$defs`` and ``anyOf`` that the OCI endpoint rejects.
"""View on GitHub (pinned to 6c2dcb801b)
Solutions
- Serialize tool output: 'content': json.dumps(result) or str(result).
- Special-case role='tool' in message-building helpers so it bypasses multipart formatting.
- Replace None content with a placeholder string like '' or 'null'.
Example fix
# before
{'role':'tool','tool_call_id':'c1','content': [{'type':'text','text':'42'}]}
# after
{'role':'tool','tool_call_id':'c1','content': '42'}
# structured output: 'content': json.dumps({'temp_c': 21}) Defensive patterns
Strategy: validation
Validate before calling
import json
messages.append({
'role': 'tool',
'tool_call_id': tc['id'],
'content': result if isinstance(result, str) else json.dumps(result),
}) Type guard
def tool_message_content_is_string(msg: object) -> bool:
return (
isinstance(msg, dict)
and msg.get('role') == 'tool'
and isinstance(msg.get('content'), str)
) Prevention
- In message builders, special-case role='tool' to bypass multipart content formatting.
- Serialize structured tool outputs with json.dumps at the boundary; never pass None content for tool replies.
When it happens
Trigger: Sending {'role':'tool','tool_call_id':'c1','content':[{'type':'text','text':'42'}]} or content as a dict/None after an assistant tool call, when calling an oci/ GENERIC model.
Common situations: Reusing a generic 'build content parts' helper for all roles including tool; structured tool outputs (JSON objects) inserted directly instead of serialized; optional tool outputs leaving None in the content slot.
Related errors
- Content item of type `text` must have a string `text` field
- Each tool call must be a dictionary
- 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/46dff8a760f10274.
Report an issue: GitHub.