Skip to content

Commit da4ae1c

Browse files
chore: Remove GenAI input truncation functions (#7442)
Remove dead GenAI input truncation code. Both `truncate_and_annotate_messages()` and `truncate_and_annotate_embedding_inputs()` were only called in the static trace lifecycle.
1 parent 70988ac commit da4ae1c

9 files changed

Lines changed: 22 additions & 1331 deletions

File tree

‎sentry_sdk/ai/utils.py‎

Lines changed: 1 addition & 270 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
import inspect
22
import json
3-
from copy import deepcopy
43
from typing import TYPE_CHECKING
54

6-
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
7-
from sentry_sdk.ai.consts import DATA_URL_BASE64_REGEX
8-
95
if TYPE_CHECKING:
10-
from typing import Any, Dict, List, Optional, Tuple, Union
6+
from typing import Any, Dict, Optional, Tuple, Union
117

128
from sentry_sdk.tracing import Span
139

1410
import sentry_sdk
1511
from sentry_sdk.traces import StreamedSpan
1612
from sentry_sdk.utils import logger
1713

18-
MAX_GEN_AI_MESSAGE_BYTES = 20_000 # 20KB
19-
# Maximum characters when only a single message is left after bytes truncation
20-
MAX_SINGLE_MESSAGE_CONTENT_CHARS = 10_000
21-
2214

2315
class GEN_AI_ALLOWED_MESSAGE_ROLES:
2416
SYSTEM = "system"
@@ -431,38 +423,6 @@ def transform_content_part(
431423
return None
432424

433425

434-
def transform_message_content(content: "Any") -> "Any":
435-
"""
436-
Transform message content, handling both string content and list of content blocks.
437-
438-
For list content, each item is transformed using transform_content_part().
439-
Items that cannot be transformed (return None) are kept as-is.
440-
441-
Args:
442-
content: Message content - can be a string, list of content blocks, or other
443-
444-
Returns:
445-
- String content: returned as-is
446-
- List content: list with each transformable item converted to standardized format
447-
- Other: returned as-is
448-
"""
449-
if isinstance(content, str):
450-
return content
451-
452-
if isinstance(content, (list, tuple)):
453-
transformed = []
454-
for item in content:
455-
if isinstance(item, dict):
456-
result = transform_content_part(item)
457-
# If transformation succeeded, use the result; otherwise keep original
458-
transformed.append(result if result is not None else item)
459-
else:
460-
transformed.append(item)
461-
return transformed
462-
463-
return content
464-
465-
466426
def _normalize_data(data: "Any", unpack: bool = True) -> "Any":
467427
# convert pydantic data (e.g. OpenAI v1+) to json compatible format
468428
if hasattr(data, "model_dump"):
@@ -536,235 +496,6 @@ def normalize_message_roles(messages: "list[dict[str, Any]]") -> "list[dict[str,
536496
return normalized_messages
537497

538498

539-
def _truncate_single_message_content_if_present(
540-
message: "Dict[str, Any]", max_chars: int
541-
) -> "Dict[str, Any]":
542-
"""
543-
Truncate a message's content to at most `max_chars` characters and append an
544-
ellipsis if truncation occurs.
545-
"""
546-
if not isinstance(message, dict) or "content" not in message:
547-
return message
548-
content = message["content"]
549-
550-
if isinstance(content, str):
551-
if len(content) <= max_chars:
552-
return message
553-
message["content"] = content[:max_chars] + "..."
554-
return message
555-
556-
if isinstance(content, list):
557-
remaining = max_chars
558-
for item in content:
559-
if isinstance(item, dict) and "text" in item:
560-
text = item["text"]
561-
if isinstance(text, str):
562-
if len(text) > remaining:
563-
item["text"] = text[:remaining] + "..."
564-
remaining = 0
565-
else:
566-
remaining -= len(text)
567-
return message
568-
569-
return message
570-
571-
572-
def _find_truncation_index(messages: "List[Dict[str, Any]]", max_bytes: int) -> int:
573-
"""
574-
Find the index of the first message that would exceed the max bytes limit.
575-
Compute the individual message sizes, and return the index of the first message from the back
576-
of the list that would exceed the max bytes limit.
577-
"""
578-
running_sum = 0
579-
for idx in range(len(messages) - 1, -1, -1):
580-
size = len(json.dumps(messages[idx], separators=(",", ":")).encode("utf-8"))
581-
running_sum += size
582-
if running_sum > max_bytes:
583-
return idx + 1
584-
585-
return 0
586-
587-
588-
def _is_image_type_with_blob_content(item: "Dict[str, Any]") -> bool:
589-
"""
590-
Some content blocks contain an image_url property with base64 content as its value.
591-
This is used to identify those while not leading to unnecessary copying of data when the image URL does not contain base64 content.
592-
"""
593-
if item.get("type") != "image_url":
594-
return False
595-
596-
image_url_val = item.get("image_url")
597-
image_url = (
598-
image_url_val.get("url", "")
599-
if isinstance(image_url_val, dict)
600-
else (image_url_val or "")
601-
)
602-
data_url_match = DATA_URL_BASE64_REGEX.match(image_url)
603-
604-
return bool(data_url_match)
605-
606-
607-
def redact_blob_message_parts(
608-
messages: "List[Dict[str, Any]]",
609-
) -> "List[Dict[str, Any]]":
610-
"""
611-
Redact blob message parts from the messages by replacing blob content with "[Filtered]".
612-
613-
This function creates a deep copy of messages that contain blob content to avoid
614-
mutating the original message dictionaries. Messages without blob content are
615-
returned as-is to minimize copying overhead.
616-
617-
e.g:
618-
{
619-
"role": "user",
620-
"content": [
621-
{
622-
"text": "How many ponies do you see in the image?",
623-
"type": "text"
624-
},
625-
{
626-
"type": "blob",
627-
"modality": "image",
628-
"mime_type": "image/jpeg",
629-
"content": "data:image/jpeg;base64,..."
630-
}
631-
]
632-
}
633-
becomes:
634-
{
635-
"role": "user",
636-
"content": [
637-
{
638-
"text": "How many ponies do you see in the image?",
639-
"type": "text"
640-
},
641-
{
642-
"type": "blob",
643-
"modality": "image",
644-
"mime_type": "image/jpeg",
645-
"content": "[Filtered]"
646-
}
647-
]
648-
}
649-
"""
650-
651-
# First pass: check if any message contains blob content
652-
has_blobs = False
653-
for message in messages:
654-
if not isinstance(message, dict):
655-
continue
656-
content = message.get("content")
657-
if isinstance(content, list):
658-
for item in content:
659-
if isinstance(item, dict) and (
660-
item.get("type") == "blob" or _is_image_type_with_blob_content(item)
661-
):
662-
has_blobs = True
663-
break
664-
if has_blobs:
665-
break
666-
667-
# If no blobs found, return original messages to avoid unnecessary copying
668-
if not has_blobs:
669-
return messages
670-
671-
# Deep copy messages to avoid mutating the original
672-
messages_copy = deepcopy(messages)
673-
674-
# Second pass: redact blob content in the copy
675-
for message in messages_copy:
676-
if not isinstance(message, dict):
677-
continue
678-
679-
content = message.get("content")
680-
if isinstance(content, list):
681-
for item in content:
682-
if isinstance(item, dict):
683-
if item.get("type") == "blob":
684-
item["content"] = BLOB_DATA_SUBSTITUTE
685-
elif _is_image_type_with_blob_content(item):
686-
if isinstance(item["image_url"], dict):
687-
item["image_url"]["url"] = BLOB_DATA_SUBSTITUTE
688-
else:
689-
item["image_url"] = BLOB_DATA_SUBSTITUTE
690-
691-
return messages_copy
692-
693-
694-
def truncate_messages_by_size(
695-
messages: "List[Dict[str, Any]]",
696-
max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES,
697-
max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS,
698-
) -> "Tuple[List[Dict[str, Any]], int]":
699-
"""
700-
Returns a truncated messages list, consisting of
701-
- the last message, with its content truncated to `max_single_message_chars` characters,
702-
if the last message's size exceeds `max_bytes` bytes; otherwise,
703-
- the maximum number of messages, starting from the end of the `messages` list, whose total
704-
serialized size does not exceed `max_bytes` bytes.
705-
706-
In the single message case, the serialized message size may exceed `max_bytes`, because
707-
truncation is based only on character count in that case.
708-
"""
709-
serialized_json = json.dumps(messages, separators=(",", ":"))
710-
current_size = len(serialized_json.encode("utf-8"))
711-
712-
if current_size <= max_bytes:
713-
return messages, 0
714-
715-
truncation_index = _find_truncation_index(messages, max_bytes)
716-
if truncation_index < len(messages):
717-
truncated_messages = messages[truncation_index:]
718-
else:
719-
truncation_index = len(messages) - 1
720-
truncated_messages = messages[-1:]
721-
722-
if len(truncated_messages) == 1:
723-
truncated_messages[0] = _truncate_single_message_content_if_present(
724-
deepcopy(truncated_messages[0]), max_chars=max_single_message_chars
725-
)
726-
727-
return truncated_messages, truncation_index
728-
729-
730-
def truncate_and_annotate_messages(
731-
messages: "Optional[List[Dict[str, Any]]]",
732-
span: "Any",
733-
scope: "Any",
734-
max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS,
735-
) -> "Optional[List[Dict[str, Any]]]":
736-
if not messages:
737-
return None
738-
739-
messages = redact_blob_message_parts(messages)
740-
741-
truncated_message = _truncate_single_message_content_if_present(
742-
deepcopy(messages[-1]), max_chars=max_single_message_chars
743-
)
744-
if len(messages) > 1:
745-
scope._gen_ai_original_message_count[span.span_id] = len(messages)
746-
747-
return [truncated_message]
748-
749-
750-
def truncate_and_annotate_embedding_inputs(
751-
messages: "Optional[List[Dict[str, Any]]]",
752-
span: "Any",
753-
scope: "Any",
754-
max_bytes: int = MAX_GEN_AI_MESSAGE_BYTES,
755-
) -> "Optional[List[Dict[str, Any]]]":
756-
if not messages:
757-
return None
758-
759-
messages = redact_blob_message_parts(messages)
760-
761-
truncated_messages, removed_count = truncate_messages_by_size(messages, max_bytes)
762-
if removed_count > 0:
763-
scope._gen_ai_original_message_count[span.span_id] = len(messages)
764-
765-
return truncated_messages
766-
767-
768499
def set_conversation_id(conversation_id: str) -> None:
769500
"""
770501
Set the conversation_id in the scope.

‎sentry_sdk/integrations/langchain.py‎

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@
1111
normalize_message_roles,
1212
set_data_normalized,
1313
transform_content_part,
14-
truncate_and_annotate_messages,
1514
)
1615
from sentry_sdk.consts import OP, SPANDATA
1716
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
1817
from sentry_sdk.scope import should_send_default_pii
1918
from sentry_sdk.traces import StreamedSpan
2019
from sentry_sdk.tracing_utils import (
2120
_get_value,
22-
has_span_streaming_enabled,
2321
)
2422
from sentry_sdk.utils import (
2523
capture_internal_exceptions,
@@ -425,17 +423,11 @@ def on_llm_start(
425423
for prompt in prompts
426424
]
427425

428-
scope = sentry_sdk.get_current_scope()
429-
messages_data = (
430-
truncate_and_annotate_messages(normalized_messages, span, scope)
431-
if not has_span_streaming_enabled(client.options)
432-
else normalized_messages
433-
)
434-
if messages_data is not None:
426+
if normalized_messages is not None:
435427
set_data_normalized(
436428
span,
437429
SPANDATA.GEN_AI_REQUEST_MESSAGES,
438-
messages_data,
430+
normalized_messages,
439431
unpack=False,
440432
)
441433

@@ -537,17 +529,11 @@ def on_chat_model_start(
537529
)
538530
normalized_messages = normalize_message_roles(normalized_messages)
539531

540-
scope = sentry_sdk.get_current_scope()
541-
messages_data = (
542-
truncate_and_annotate_messages(normalized_messages, span, scope)
543-
if not has_span_streaming_enabled(client.options)
544-
else normalized_messages
545-
)
546-
if messages_data is not None:
532+
if normalized_messages is not None:
547533
set_data_normalized(
548534
span,
549535
SPANDATA.GEN_AI_REQUEST_MESSAGES,
550-
messages_data,
536+
normalized_messages,
551537
unpack=False,
552538
)
553539

@@ -1158,17 +1144,11 @@ def new_invoke(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
11581144
if input is not None and record_inputs:
11591145
normalized_messages = normalize_message_roles([input])
11601146

1161-
scope = sentry_sdk.get_current_scope()
1162-
messages_data = (
1163-
truncate_and_annotate_messages(normalized_messages, span, scope)
1164-
if not has_span_streaming_enabled(client.options)
1165-
else normalized_messages
1166-
)
1167-
if messages_data is not None:
1147+
if normalized_messages is not None:
11681148
set_data_normalized(
11691149
span,
11701150
SPANDATA.GEN_AI_REQUEST_MESSAGES,
1171-
messages_data,
1151+
normalized_messages,
11721152
unpack=False,
11731153
)
11741154

@@ -1220,17 +1200,11 @@ def new_stream(self: "Any", *args: "Any", **kwargs: "Any") -> "Any":
12201200
if input is not None and record_inputs:
12211201
normalized_messages = normalize_message_roles([input])
12221202

1223-
scope = sentry_sdk.get_current_scope()
1224-
messages_data = (
1225-
truncate_and_annotate_messages(normalized_messages, span, scope)
1226-
if not has_span_streaming_enabled(client.options)
1227-
else normalized_messages
1228-
)
1229-
if messages_data is not None:
1203+
if normalized_messages is not None:
12301204
set_data_normalized(
12311205
span,
12321206
SPANDATA.GEN_AI_REQUEST_MESSAGES,
1233-
messages_data,
1207+
normalized_messages,
12341208
unpack=False,
12351209
)
12361210

0 commit comments

Comments
 (0)