BerriAI/litellm · error · OCIError

Tool result message must have a string `tool_call_id`

Error message

Tool result message must have a string `tool_call_id`

What it means

Tool result messages (role='tool') must reference the assistant tool call they answer via a string 'tool_call_id'. If it is missing, None, or non-string, OCIError(400) 'Tool result message must have a string `tool_call_id`' is raised while building the OCI request. Without this id, OCI cannot pair the result with its FUNCTION invocation.

Source

Thrown at litellm/llms/oci/chat/generic.py:195

        tool_calls = message.get("tool_calls")
        tool_call_id = message.get("tool_call_id")

        if role == "assistant" and tool_calls is not None:
            if not isinstance(tool_calls, list):
                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
# ---------------------------------------------------------------------------

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Copy the id from the corresponding assistant tool call: for tc in response.choices[0].message.tool_calls: reply {'role':'tool','tool_call_id':tc.id,'content':...}.
  2. When fanning out parallel tool calls, keep a mapping id -> result and emit one tool message per id.
  3. Pre-flight check that every role='tool' message has a non-empty string tool_call_id matching some prior assistant call id.

Example fix

# before
messages.append({'role':'tool','content':'21C'})

# after
messages.append({'role':'tool','tool_call_id': tc.id, 'content':'21C'})
Defensive patterns

Strategy: validation

Validate before calling

# when returning tool results from the assistant's tool calls
for tc in assistant_msg['tool_calls']:
    result = run_tool(tc['function']['name'], tc['function']['arguments'])
    assert isinstance(tc.get('id'), str) and tc['id']
    messages.append({'role': 'tool', 'tool_call_id': tc['id'], 'content': str(result)})

Type guard

def tool_message_is_valid(msg: object) -> bool:
    return (
        isinstance(msg, dict)
        and msg.get('role') == 'tool'
        and isinstance(msg.get('tool_call_id'), str)
        and bool(msg['tool_call_id'])
    )

Prevention

When it happens

Trigger: Sending {'role':'tool','content':'42'} without tool_call_id, or with tool_call_id as an integer/None, after an assistant tool-call turn to an oci/ GENERIC model.

Common situations: Executing tools in parallel and losing track of which call id to echo; trimming ids for token savings; storing tool results keyed by tool name instead of call id and forgetting to map back.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/f70e9d7a325c204a. Report an issue: GitHub.