fix: handle missing tool_call_id in Anthropic message conversion (#8381)

* fix: handle missing tool_call_id in Anthropic message conversion

- Add null check for self.tool_returns before iterating
- Fall back to message's tool_call_id when tool_return.tool_call_id is None
- Improve error message to show actual tool name from message.name
- Only raise error if no valid tool_call_id is available from either source

This fixes the error "Anthropic API requires tool_use_id to be set" that
occurs when a ToolReturn object in the database doesn't have tool_call_id
set, by using the message-level tool_call_id as a fallback.

Fixes #8379

🤖 Generated with [Letta Code](https://letta.com)

Co-authored-by: datadog-official[bot] <datadog-official[bot]@users.noreply.github.com>
Co-Authored-By: Letta <noreply@letta.com>

* fix: restrict tool_call_id fallback to single tool returns

The message-level `self.tool_call_id` is set to the first tool return's ID
for legacy compatibility. For parallel tool calls with multiple tool_returns,
using this as a fallback would incorrectly assign the first tool return's ID
to all subsequent returns missing their own ID.

This change:
- Only allows the fallback when there's exactly one tool return
- For multiple tool returns, each must have its own ID or raise an error
- Adds tool return index to error messages for better debugging

Co-authored-by: Kian Jones <kianjones9@users.noreply.github.com>

🤖 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

---------

Co-authored-by: letta-code <248085862+letta-code@users.noreply.github.com>
Co-authored-by: datadog-official[bot] <datadog-official[bot]@users.noreply.github.com>
Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: Kian Jones <11655409+kianjones9@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-01-08 11:10:05 -08:00
committed by Caren Thomas
parent d2e19bbc05
commit b559bf8403

View File

@@ -1731,29 +1731,42 @@ class Message(BaseMessage):
elif self.role == "tool":
# NOTE: Anthropic uses role "user" for "tool" responses
content = []
for tool_return in self.tool_returns:
if not tool_return.tool_call_id:
from letta.log import get_logger
# Handle the case where tool_returns is None or empty
if self.tool_returns:
# For single tool returns, we can use the message's tool_call_id as fallback
# since self.tool_call_id == tool_returns[0].tool_call_id for legacy compatibility.
# For multiple tool returns (parallel tool calls), each must have its own ID
# to correctly map results to their corresponding tool invocations.
use_message_fallback = len(self.tool_returns) == 1
for idx, tool_return in enumerate(self.tool_returns):
# Get tool_call_id from tool_return; only use message fallback for single returns
resolved_tool_call_id = tool_return.tool_call_id
if not resolved_tool_call_id and use_message_fallback:
resolved_tool_call_id = self.tool_call_id
if not resolved_tool_call_id:
from letta.log import get_logger
logger = get_logger(__name__)
logger.error(
f"Missing tool_call_id in tool return. "
f"Message ID: {self.id}, "
f"Tool name: {getattr(tool_return, 'name', 'unknown')}, "
f"Tool return: {tool_return}"
logger = get_logger(__name__)
logger.error(
f"Missing tool_call_id in tool return and no fallback available. "
f"Message ID: {self.id}, "
f"Tool name: {self.name or 'unknown'}, "
f"Tool return index: {idx}/{len(self.tool_returns)}, "
f"Tool return status: {tool_return.status}"
)
raise TypeError(
f"Anthropic API requires tool_use_id to be set. "
f"Message ID: {self.id}, Tool: {self.name or 'unknown'}, "
f"Tool return index: {idx}/{len(self.tool_returns)}"
)
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
content.append(
{
"type": "tool_result",
"tool_use_id": resolved_tool_call_id,
"content": func_response,
}
)
raise TypeError(
f"Anthropic API requires tool_use_id to be set. "
f"Message ID: {self.id}, Tool: {getattr(tool_return, 'name', 'unknown')}"
)
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
content.append(
{
"type": "tool_result",
"tool_use_id": tool_return.tool_call_id,
"content": func_response,
}
)
if content:
anthropic_message = {
"role": "user",