From 02f776b0166c9b5d7c1b546d1d7bed931ea1b208 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:58:30 -0800 Subject: [PATCH] fix: handle system messages with mixed TextContent + ImageContent (#9418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: handle system messages with mixed TextContent + ImageContent System messages injected by external tools (e.g. packify.ai MCP) can contain both TextContent and ImageContent. The assertions in to_openai_responses_dicts and to_anthropic_dict expected exactly one TextContent, causing AssertionError in production. Extract all text parts and join them, matching how to_openai_dict already handles this case. 🤖 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * fix: replace asserts with logger.warning + graceful skip Asserts are the wrong tool for production input validation — if a system message has only non-text content, we should warn and skip rather than crash the request. 🤖 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta --------- Co-authored-by: Letta --- letta/schemas/message.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/letta/schemas/message.py b/letta/schemas/message.py index 99902d7d..d56d9210 100644 --- a/letta/schemas/message.py +++ b/letta/schemas/message.py @@ -1532,11 +1532,17 @@ class Message(BaseMessage): message_dicts = [] if self.role == "system": - assert len(self.content) == 1 and isinstance(self.content[0], TextContent), vars(self) + text_parts = [c.text for c in (self.content or []) if isinstance(c, TextContent)] + if not text_parts: + logger.warning( + f"System message {self.id} has no text content, skipping: roles={[type(c).__name__ for c in (self.content or [])]}" + ) + return message_dicts + system_text = "\n\n".join(text_parts) message_dicts.append( { "role": "developer", - "content": self.content[0].text, + "content": system_text, } ) @@ -1847,10 +1853,20 @@ class Message(BaseMessage): if self.role == "system": # NOTE: this is not for system instructions, but instead system "events" - assert text_content is not None, vars(self) + system_text = text_content + if system_text is None: + text_parts = [c.text for c in (self.content or []) if isinstance(c, TextContent)] + if not text_parts: + from letta.log import get_logger as _get_logger + + _get_logger(__name__).warning( + f"System message {self.id} has no text content, skipping: roles={[type(c).__name__ for c in (self.content or [])]}" + ) + return None + system_text = "\n\n".join(text_parts) # Two options here, we would use system.package_system_message, # or use a more Anthropic-specific packaging ie xml tags - user_system_event = add_xml_tag(string=f"SYSTEM ALERT: {text_content}", xml_tag="event") + user_system_event = add_xml_tag(string=f"SYSTEM ALERT: {system_text}", xml_tag="event") anthropic_message = { "content": user_system_event, "role": "user",