@@ -14,11 +14,14 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
1414 # type: (List[Dict[str, Any]], int) -> List[Dict[str, Any]]
1515 """
1616 Truncate messages by removing the oldest ones until the serialized size is within limits.
17+ If the last message is still too large, truncate its content instead of removing it entirely.
1718
1819 This function prioritizes keeping the most recent messages while ensuring the total
1920 serialized size stays under the specified byte limit. It uses the Sentry serializer
2021 to get accurate size estimates that match what will actually be sent.
2122
23+ Always preserves at least one message, even if content needs to be truncated.
24+
2225 :param messages: List of message objects (typically with 'role' and 'content' keys)
2326 :param max_bytes: Maximum allowed size in bytes for the serialized messages
2427 :returns: Truncated list of messages that fits within the size limit
@@ -28,15 +31,64 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
2831
2932 truncated_messages = list (messages )
3033
31- while truncated_messages :
32- serialized = serialize (truncated_messages , is_vars = False )
34+ # First, remove older messages until we're under the limit or have only one message left
35+ while len (truncated_messages ) > 1 :
36+ serialized = serialize (
37+ truncated_messages , is_vars = False , max_value_length = round (max_bytes * 0.8 )
38+ )
3339 serialized_json = json .dumps (serialized , separators = ("," , ":" ))
3440 current_size = len (serialized_json .encode ("utf-8" ))
3541
3642 if current_size <= max_bytes :
3743 break
3844
39- truncated_messages .pop (0 )
45+ truncated_messages .pop (0 ) # Remove oldest message
46+
47+ # If we still have one message but it's too large, truncate its content
48+ # This ensures we always preserve at least one message
49+ if len (truncated_messages ) == 1 :
50+ serialized = serialize (
51+ truncated_messages , is_vars = False , max_value_length = round (max_bytes * 0.8 )
52+ )
53+ serialized_json = json .dumps (serialized , separators = ("," , ":" ))
54+ current_size = len (serialized_json .encode ("utf-8" ))
55+
56+ if current_size > max_bytes :
57+ # Truncate the content of the last message
58+ last_message = truncated_messages [0 ].copy ()
59+ content = last_message .get ("content" , "" )
60+
61+ if content and isinstance (content , str ):
62+ # Binary search to find the optimal content length
63+ left , right = 0 , len (content )
64+ best_length = 0
65+
66+ while left <= right :
67+ mid = (left + right ) // 2
68+ test_message = last_message .copy ()
69+ test_message ["content" ] = content [:mid ] + (
70+ "..." if mid < len (content ) else ""
71+ )
72+
73+ test_serialized = serialize (
74+ [test_message ],
75+ is_vars = False ,
76+ max_value_length = round (max_bytes * 0.8 ),
77+ )
78+ test_json = json .dumps (test_serialized , separators = ("," , ":" ))
79+ test_size = len (test_json .encode ("utf-8" ))
80+
81+ if test_size <= max_bytes :
82+ best_length = mid
83+ left = mid + 1
84+ else :
85+ right = mid - 1
86+
87+ # Apply the truncation
88+ if best_length < len (content ):
89+ last_message ["content" ] = content [:best_length ] + "..."
90+
91+ truncated_messages [0 ] = last_message
4092
4193 return truncated_messages
4294
@@ -59,51 +111,33 @@ def serialize_gen_ai_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
59111 return None
60112
61113 if isinstance (messages , AnnotatedValue ):
62- serialized_messages = serialize (messages , is_vars = False )
114+ serialized_messages = serialize (
115+ messages , is_vars = False , max_value_length = round (max_bytes * 0.8 )
116+ )
63117 return json .dumps (serialized_messages , separators = ("," , ":" ))
64118
65119 truncated_messages = truncate_messages_by_size (messages , max_bytes )
66120 if not truncated_messages :
67121 return None
68- serialized_messages = serialize (truncated_messages , is_vars = False )
122+ serialized_messages = serialize (
123+ truncated_messages , is_vars = False , max_value_length = round (max_bytes * 0.8 )
124+ )
69125
70126 return json .dumps (serialized_messages , separators = ("," , ":" ))
71127
72128
73- def get_messages_metadata (original_messages , truncated_messages ):
74- # type: (List[Dict[str, Any]], List[Dict[str, Any]]) -> Dict[str, Any]
75- """
76- Generate metadata about message truncation for debugging/monitoring.
77-
78- :param original_messages: The original list of messages
79- :param truncated_messages: The truncated list of messages
80- :returns: Dictionary with metadata about the truncation
81- """
82- original_count = len (original_messages ) if original_messages else 0
83- truncated_count = len (truncated_messages ) if truncated_messages else 0
84-
85- metadata = {
86- "original_count" : original_count ,
87- "truncated_count" : truncated_count ,
88- "messages_removed" : original_count - truncated_count ,
89- "was_truncated" : original_count != truncated_count ,
90- }
91-
92- return metadata
93-
94-
95129def truncate_and_serialize_messages (messages , max_bytes = MAX_GEN_AI_MESSAGE_BYTES ):
96130 # type: (Optional[List[Dict[str, Any]]], int) -> Any
97131 """
98- Truncate messages and return AnnotatedValue for automatic _meta creation.
132+ Truncate messages and return serialized string or AnnotatedValue for automatic _meta creation.
99133
100- This function handles truncation and returns the truncated messages wrapped in an
101- AnnotatedValue (when truncation occurs) so that Sentry's serializer can automatically
102- create the appropriate _meta structure.
134+ This function handles truncation and always returns serialized JSON strings. When truncation
135+ occurs, it wraps the serialized string in an AnnotatedValue so that Sentry's serializer can
136+ automatically create the appropriate _meta structure.
103137
104138 :param messages: List of message objects or None
105139 :param max_bytes: Maximum allowed size in bytes for the serialized messages
106- :returns: List of messages , AnnotatedValue (if truncated), or None
140+ :returns: JSON string , AnnotatedValue containing JSON string (if truncated), or None
107141 """
108142 if not messages :
109143 return None
@@ -112,12 +146,20 @@ def truncate_and_serialize_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES
112146 if not truncated_messages :
113147 return None
114148
149+ # Always serialize to JSON string
150+ serialized_json = serialize_gen_ai_messages (truncated_messages , max_bytes )
151+ if not serialized_json :
152+ return None
153+
115154 original_count = len (messages )
116155 truncated_count = len (truncated_messages )
117156
157+ # If truncation occurred, wrap the serialized string in AnnotatedValue for _meta
118158 if original_count != truncated_count :
119159 return AnnotatedValue (
120- value = serialize_gen_ai_messages ( truncated_messages ) ,
160+ value = serialized_json ,
121161 metadata = {"len" : original_count },
122162 )
123- return truncated_messages
163+
164+ # No truncation, return plain serialized string
165+ return serialized_json
0 commit comments