Skip to content

Commit 80d905d

Browse files
committed
truncate message lengths without meta so far
1 parent 9e68c96 commit 80d905d

File tree

2 files changed

+144
-132
lines changed

2 files changed

+144
-132
lines changed

sentry_sdk/ai/message_utils.py

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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-
95129
def 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

Comments
 (0)