@@ -14,11 +14,14 @@ def truncate_messages_by_size(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
14
14
# type: (List[Dict[str, Any]], int) -> List[Dict[str, Any]]
15
15
"""
16
16
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.
17
18
18
19
This function prioritizes keeping the most recent messages while ensuring the total
19
20
serialized size stays under the specified byte limit. It uses the Sentry serializer
20
21
to get accurate size estimates that match what will actually be sent.
21
22
23
+ Always preserves at least one message, even if content needs to be truncated.
24
+
22
25
:param messages: List of message objects (typically with 'role' and 'content' keys)
23
26
:param max_bytes: Maximum allowed size in bytes for the serialized messages
24
27
: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):
28
31
29
32
truncated_messages = list (messages )
30
33
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
+ )
33
39
serialized_json = json .dumps (serialized , separators = ("," , ":" ))
34
40
current_size = len (serialized_json .encode ("utf-8" ))
35
41
36
42
if current_size <= max_bytes :
37
43
break
38
44
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
40
92
41
93
return truncated_messages
42
94
@@ -59,51 +111,33 @@ def serialize_gen_ai_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES):
59
111
return None
60
112
61
113
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
+ )
63
117
return json .dumps (serialized_messages , separators = ("," , ":" ))
64
118
65
119
truncated_messages = truncate_messages_by_size (messages , max_bytes )
66
120
if not truncated_messages :
67
121
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
+ )
69
125
70
126
return json .dumps (serialized_messages , separators = ("," , ":" ))
71
127
72
128
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
-
95
129
def truncate_and_serialize_messages (messages , max_bytes = MAX_GEN_AI_MESSAGE_BYTES ):
96
130
# type: (Optional[List[Dict[str, Any]]], int) -> Any
97
131
"""
98
- Truncate messages and return AnnotatedValue for automatic _meta creation.
132
+ Truncate messages and return serialized string or AnnotatedValue for automatic _meta creation.
99
133
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.
103
137
104
138
:param messages: List of message objects or None
105
139
: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
107
141
"""
108
142
if not messages :
109
143
return None
@@ -112,12 +146,20 @@ def truncate_and_serialize_messages(messages, max_bytes=MAX_GEN_AI_MESSAGE_BYTES
112
146
if not truncated_messages :
113
147
return None
114
148
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
+
115
154
original_count = len (messages )
116
155
truncated_count = len (truncated_messages )
117
156
157
+ # If truncation occurred, wrap the serialized string in AnnotatedValue for _meta
118
158
if original_count != truncated_count :
119
159
return AnnotatedValue (
120
- value = serialize_gen_ai_messages ( truncated_messages ) ,
160
+ value = serialized_json ,
121
161
metadata = {"len" : original_count },
122
162
)
123
- return truncated_messages
163
+
164
+ # No truncation, return plain serialized string
165
+ return serialized_json
0 commit comments