fix: gracefully skip assistant messages with empty content in LLM for… (#9050)

fix: gracefully skip assistant messages with empty content in LLM format conversion

**Problem:**
Context window calculation crashed with AssertionError when converting messages
to Google/Anthropic/OpenAI format:
```
AssertionError at line 2047: assert self.tool_calls is not None or
text_content is not None or len(self.content) > 1
```

This happened when loading agents with old/malformed messages that had
`content=None` or `content=[]` in the database.

**Root Cause:**
The Message ORM model allows `content: Optional[List[...]] = None` (line 252),
but format conversion methods assumed content would always have extractable text
or tool calls.

Scenarios that triggered crashes:
1. Assistant message with `content=None` (old migrations/edge cases)
2. Assistant message with `content=[]` (message creation bugs)
3. Assistant message with single non-text content that doesn't match extraction logic

**Fix:**
Replaced assertions with defensive checks in 3 conversion methods:

1. `to_google_dict()` (line 2054) - Return None to skip unconvertible messages
2. `to_openai_responses_api_dicts()` (line 1476) - Return early to skip
3. `to_anthropic_dict()` (line 1794) - Return None to skip

Pattern: Check for empty content, return None/early to skip gracefully.

**Result:**
- Context window calculation no longer crashes on malformed/old messages
- Messages with no convertible content are silently skipped
- Consistent with existing Anthropic reasoning-only message handling (line 1308)

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

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
cthomas
2026-01-22 17:03:35 -08:00
committed by Caren Thomas
parent 4c2253dc76
commit 208992170c

View File

@@ -1471,7 +1471,10 @@ class Message(BaseMessage):
message_dicts.append(user_dict)
elif self.role == "assistant" or self.role == "approval":
assert self.tool_calls is not None or (self.content is not None and len(self.content) > 0)
# Validate that message has content OpenAI Responses API can process
if self.tool_calls is None and (self.content is None or len(self.content) == 0):
# Skip this message (similar to Anthropic handling at line 1308)
return message_dicts
# A few things may be in here, firstly reasoning content, secondly assistant messages, thirdly tool calls
# TODO check if OpenAI Responses is capable of R->A->T like Anthropic?
@@ -1787,8 +1790,11 @@ class Message(BaseMessage):
}
elif self.role == "assistant" or self.role == "approval":
# assert self.tool_calls is not None or text_content is not None, vars(self)
assert self.tool_calls is not None or len(self.content) > 0
# Validate that message has content Anthropic API can process
if self.tool_calls is None and (self.content is None or len(self.content) == 0):
# Skip this message (consistent with OpenAI dict handling)
return None
anthropic_message = {
"role": "assistant",
}
@@ -2044,7 +2050,16 @@ class Message(BaseMessage):
}
elif self.role == "assistant" or self.role == "approval":
assert self.tool_calls is not None or text_content is not None or len(self.content) > 1
# Validate that message has content Google API can process
if self.tool_calls is None and text_content is None and len(self.content) <= 1:
# Message has no tool calls, no extractable text, and not multi-part
logger.warning(
f"Assistant/approval message {self.id} has no content Google API can convert: "
f"tool_calls={self.tool_calls}, text_content={text_content}, content={self.content}"
)
# Return None to skip this message (similar to approval messages without tool_calls at line 1998)
return None
google_ai_message = {
"role": "model", # NOTE: different
}