Skip to content

Commit d4db6d3

Browse files
fix(ai): Keep single content input message (#5345)
Store only the last input message on the `gen_ai.request.messages` attribute. Keep prior logic that progressively trims away messages only for embeddings. Remove tests that check the existence of conversation histories or prompt messages that are no longer set on the attribute.
1 parent dafd62f commit d4db6d3

File tree

11 files changed

+179
-211
lines changed

11 files changed

+179
-211
lines changed

sentry_sdk/ai/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,26 @@ def truncate_messages_by_size(
682682

683683

684684
def truncate_and_annotate_messages(
685+
messages: "Optional[List[Dict[str, Any]]]",
686+
span: "Any",
687+
scope: "Any",
688+
max_single_message_chars: int = MAX_SINGLE_MESSAGE_CONTENT_CHARS,
689+
) -> "Optional[List[Dict[str, Any]]]":
690+
if not messages:
691+
return None
692+
693+
messages = redact_blob_message_parts(messages)
694+
695+
truncated_message = _truncate_single_message_content_if_present(
696+
deepcopy(messages[-1]), max_chars=max_single_message_chars
697+
)
698+
if len(messages) > 1:
699+
scope._gen_ai_original_message_count[span.span_id] = len(messages)
700+
701+
return [truncated_message]
702+
703+
704+
def truncate_and_annotate_embedding_inputs(
685705
messages: "Optional[List[Dict[str, Any]]]",
686706
span: "Any",
687707
scope: "Any",

sentry_sdk/integrations/litellm.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
set_data_normalized,
1010
truncate_and_annotate_messages,
1111
transform_openai_content_part,
12+
truncate_and_annotate_embedding_inputs,
1213
)
1314
from sentry_sdk.consts import SPANDATA
1415
from sentry_sdk.integrations import DidNotEnable, Integration
@@ -118,7 +119,9 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None:
118119
if isinstance(embedding_input, list)
119120
else [embedding_input]
120121
)
121-
messages_data = truncate_and_annotate_messages(input_list, span, scope)
122+
messages_data = truncate_and_annotate_embedding_inputs(
123+
input_list, span, scope
124+
)
122125
if messages_data is not None:
123126
set_data_normalized(
124127
span,

sentry_sdk/integrations/openai.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
set_data_normalized,
1212
normalize_message_roles,
1313
truncate_and_annotate_messages,
14+
truncate_and_annotate_embedding_inputs,
1415
)
1516
from sentry_sdk.ai._openai_completions_api import (
1617
_is_system_instruction as _is_system_instruction_completions,
@@ -414,7 +415,9 @@ def _set_embeddings_input_data(
414415
):
415416
normalized_messages = normalize_message_roles(messages) # type: ignore
416417
scope = sentry_sdk.get_current_scope()
417-
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
418+
messages_data = truncate_and_annotate_embedding_inputs(
419+
normalized_messages, span, scope
420+
)
418421
if messages_data is not None:
419422
set_data_normalized(
420423
span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False

tests/integrations/anthropic/test_anthropic.py

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -899,7 +899,25 @@ def test_set_output_data_with_input_json_delta(sentry_init):
899899
assert span._data.get(SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS) == 30
900900

901901

902-
def test_anthropic_message_role_mapping(sentry_init, capture_events):
902+
# Test messages with mixed roles including "ai" that should be mapped to "assistant"
903+
@pytest.mark.parametrize(
904+
"test_message,expected_role",
905+
[
906+
({"role": "system", "content": "You are helpful."}, "system"),
907+
({"role": "user", "content": "Hello"}, "user"),
908+
(
909+
{"role": "ai", "content": "Hi there!"},
910+
"assistant",
911+
), # Should be mapped to "assistant"
912+
(
913+
{"role": "assistant", "content": "How can I help?"},
914+
"assistant",
915+
), # Should stay "assistant"
916+
],
917+
)
918+
def test_anthropic_message_role_mapping(
919+
sentry_init, capture_events, test_message, expected_role
920+
):
903921
"""Test that Anthropic integration properly maps message roles like 'ai' to 'assistant'"""
904922
sentry_init(
905923
integrations=[AnthropicIntegration(include_prompts=True)],
@@ -924,13 +942,7 @@ def mock_messages_create(*args, **kwargs):
924942

925943
client.messages._post = mock.Mock(return_value=mock_messages_create())
926944

927-
# Test messages with mixed roles including "ai" that should be mapped to "assistant"
928-
test_messages = [
929-
{"role": "system", "content": "You are helpful."},
930-
{"role": "user", "content": "Hello"},
931-
{"role": "ai", "content": "Hi there!"}, # Should be mapped to "assistant"
932-
{"role": "assistant", "content": "How can I help?"}, # Should stay "assistant"
933-
]
945+
test_messages = [test_message]
934946

935947
with start_transaction(name="anthropic tx"):
936948
client.messages.create(
@@ -948,22 +960,7 @@ def mock_messages_create(*args, **kwargs):
948960
# Parse the stored messages
949961
stored_messages = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
950962

951-
# Verify that "ai" role was mapped to "assistant"
952-
assert len(stored_messages) == 4
953-
assert stored_messages[0]["role"] == "system"
954-
assert stored_messages[1]["role"] == "user"
955-
assert (
956-
stored_messages[2]["role"] == "assistant"
957-
) # "ai" should be mapped to "assistant"
958-
assert stored_messages[3]["role"] == "assistant" # should stay "assistant"
959-
960-
# Verify content is preserved
961-
assert stored_messages[2]["content"] == "Hi there!"
962-
assert stored_messages[3]["content"] == "How can I help?"
963-
964-
# Verify no "ai" roles remain
965-
roles = [msg["role"] for msg in stored_messages]
966-
assert "ai" not in roles
963+
assert stored_messages[0]["role"] == expected_role
967964

968965

969966
def test_anthropic_message_truncation(sentry_init, capture_events):
@@ -1010,9 +1007,60 @@ def test_anthropic_message_truncation(sentry_init, capture_events):
10101007

10111008
parsed_messages = json.loads(messages_data)
10121009
assert isinstance(parsed_messages, list)
1013-
assert len(parsed_messages) == 2
1014-
assert "small message 4" in str(parsed_messages[0])
1015-
assert "small message 5" in str(parsed_messages[1])
1010+
assert len(parsed_messages) == 1
1011+
assert "small message 5" in str(parsed_messages[0])
1012+
1013+
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
1014+
1015+
1016+
@pytest.mark.asyncio
1017+
async def test_anthropic_message_truncation_async(sentry_init, capture_events):
1018+
"""Test that large messages are truncated properly in Anthropic integration."""
1019+
sentry_init(
1020+
integrations=[AnthropicIntegration(include_prompts=True)],
1021+
traces_sample_rate=1.0,
1022+
send_default_pii=True,
1023+
)
1024+
events = capture_events()
1025+
1026+
client = AsyncAnthropic(api_key="z")
1027+
client.messages._post = mock.AsyncMock(return_value=EXAMPLE_MESSAGE)
1028+
1029+
large_content = (
1030+
"This is a very long message that will exceed our size limits. " * 1000
1031+
)
1032+
messages = [
1033+
{"role": "user", "content": "small message 1"},
1034+
{"role": "assistant", "content": large_content},
1035+
{"role": "user", "content": large_content},
1036+
{"role": "assistant", "content": "small message 4"},
1037+
{"role": "user", "content": "small message 5"},
1038+
]
1039+
1040+
with start_transaction():
1041+
await client.messages.create(max_tokens=1024, messages=messages, model="model")
1042+
1043+
assert len(events) > 0
1044+
tx = events[0]
1045+
assert tx["type"] == "transaction"
1046+
1047+
chat_spans = [
1048+
span for span in tx.get("spans", []) if span.get("op") == OP.GEN_AI_CHAT
1049+
]
1050+
assert len(chat_spans) > 0
1051+
1052+
chat_span = chat_spans[0]
1053+
assert chat_span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
1054+
assert SPANDATA.GEN_AI_REQUEST_MESSAGES in chat_span["data"]
1055+
1056+
messages_data = chat_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]
1057+
assert isinstance(messages_data, str)
1058+
1059+
parsed_messages = json.loads(messages_data)
1060+
assert isinstance(parsed_messages, list)
1061+
assert len(parsed_messages) == 1
1062+
assert "small message 5" in str(parsed_messages[0])
1063+
10161064
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
10171065

10181066

tests/integrations/google_genai/test_google_genai.py

Lines changed: 10 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,60 +1497,6 @@ def test_generate_content_with_content_object(
14971497
]
14981498

14991499

1500-
def test_generate_content_with_conversation_history(
1501-
sentry_init, capture_events, mock_genai_client
1502-
):
1503-
"""Test generate_content with list of Content objects (conversation history)."""
1504-
sentry_init(
1505-
integrations=[GoogleGenAIIntegration(include_prompts=True)],
1506-
traces_sample_rate=1.0,
1507-
send_default_pii=True,
1508-
)
1509-
events = capture_events()
1510-
1511-
mock_http_response = create_mock_http_response(EXAMPLE_API_RESPONSE_JSON)
1512-
1513-
# Create conversation history
1514-
contents = [
1515-
genai_types.Content(
1516-
role="user", parts=[genai_types.Part(text="What is the capital of France?")]
1517-
),
1518-
genai_types.Content(
1519-
role="model",
1520-
parts=[genai_types.Part(text="The capital of France is Paris.")],
1521-
),
1522-
genai_types.Content(
1523-
role="user", parts=[genai_types.Part(text="What about Germany?")]
1524-
),
1525-
]
1526-
1527-
with mock.patch.object(
1528-
mock_genai_client._api_client, "request", return_value=mock_http_response
1529-
):
1530-
with start_transaction(name="google_genai"):
1531-
mock_genai_client.models.generate_content(
1532-
model="gemini-1.5-flash", contents=contents, config=create_test_config()
1533-
)
1534-
1535-
(event,) = events
1536-
invoke_span = event["spans"][0]
1537-
1538-
messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
1539-
assert len(messages) == 3
1540-
assert messages[0]["role"] == "user"
1541-
assert messages[0]["content"] == [
1542-
{"text": "What is the capital of France?", "type": "text"}
1543-
]
1544-
assert (
1545-
messages[1]["role"] == "assistant"
1546-
) # "model" should be normalized to "assistant"
1547-
assert messages[1]["content"] == [
1548-
{"text": "The capital of France is Paris.", "type": "text"}
1549-
]
1550-
assert messages[2]["role"] == "user"
1551-
assert messages[2]["content"] == [{"text": "What about Germany?", "type": "text"}]
1552-
1553-
15541500
def test_generate_content_with_dict_format(
15551501
sentry_init, capture_events, mock_genai_client
15561502
):
@@ -1720,17 +1666,12 @@ def test_generate_content_with_function_response(
17201666
invoke_span = event["spans"][0]
17211667

17221668
messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
1723-
assert len(messages) == 2
1669+
assert len(messages) == 1
17241670
# First message is user message
1725-
assert messages[0]["role"] == "user"
1726-
assert messages[0]["content"] == [
1727-
{"text": "What's the weather in Paris?", "type": "text"}
1728-
]
1729-
# Second message is tool message
1730-
assert messages[1]["role"] == "tool"
1731-
assert messages[1]["content"]["toolCallId"] == "call_123"
1732-
assert messages[1]["content"]["toolName"] == "get_weather"
1733-
assert messages[1]["content"]["output"] == '"Sunny, 72F"'
1671+
assert messages[0]["role"] == "tool"
1672+
assert messages[0]["content"]["toolCallId"] == "call_123"
1673+
assert messages[0]["content"]["toolName"] == "get_weather"
1674+
assert messages[0]["content"]["output"] == '"Sunny, 72F"'
17341675

17351676

17361677
def test_generate_content_with_mixed_string_and_content(
@@ -1771,18 +1712,10 @@ def test_generate_content_with_mixed_string_and_content(
17711712
invoke_span = event["spans"][0]
17721713

17731714
messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
1774-
assert len(messages) == 3
1775-
# String becomes user message
1776-
assert messages[0]["role"] == "user"
1777-
assert messages[0]["content"] == "Hello, this is a string message"
1778-
# Model role normalized to assistant
1779-
assert messages[1]["role"] == "assistant"
1780-
assert messages[1]["content"] == [
1781-
{"text": "Hi! How can I help you?", "type": "text"}
1782-
]
1715+
assert len(messages) == 1
17831716
# User message
1784-
assert messages[2]["role"] == "user"
1785-
assert messages[2]["content"] == [{"text": "Tell me a joke", "type": "text"}]
1717+
assert messages[0]["role"] == "user"
1718+
assert messages[0]["content"] == [{"text": "Tell me a joke", "type": "text"}]
17861719

17871720

17881721
def test_generate_content_with_part_object_directly(
@@ -1850,13 +1783,9 @@ def test_generate_content_with_list_of_dicts(
18501783
invoke_span = event["spans"][0]
18511784

18521785
messages = json.loads(invoke_span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES])
1853-
assert len(messages) == 3
1786+
assert len(messages) == 1
18541787
assert messages[0]["role"] == "user"
1855-
assert messages[0]["content"] == [{"text": "First user message", "type": "text"}]
1856-
assert messages[1]["role"] == "assistant"
1857-
assert messages[1]["content"] == [{"text": "First model response", "type": "text"}]
1858-
assert messages[2]["role"] == "user"
1859-
assert messages[2]["content"] == [{"text": "Second user message", "type": "text"}]
1788+
assert messages[0]["content"] == [{"text": "Second user message", "type": "text"}]
18601789

18611790

18621791
def test_generate_content_with_dict_inline_data(

tests/integrations/langchain/test_langchain.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,9 +1068,8 @@ def test_langchain_message_truncation(sentry_init, capture_events):
10681068

10691069
parsed_messages = json.loads(messages_data)
10701070
assert isinstance(parsed_messages, list)
1071-
assert len(parsed_messages) == 2
1072-
assert "small message 4" in str(parsed_messages[0])
1073-
assert "small message 5" in str(parsed_messages[1])
1071+
assert len(parsed_messages) == 1
1072+
assert "small message 5" in str(parsed_messages[0])
10741073
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
10751074

10761075

tests/integrations/langgraph/test_langgraph.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,8 @@ def original_invoke(self, *args, **kwargs):
270270
import json
271271

272272
request_messages = json.loads(request_messages)
273-
assert len(request_messages) == 2
274-
assert request_messages[0]["content"] == "Hello, can you help me?"
275-
assert request_messages[1]["content"] == "Of course! How can I assist you?"
273+
assert len(request_messages) == 1
274+
assert request_messages[0]["content"] == "Of course! How can I assist you?"
276275

277276
response_text = invoke_span["data"][SPANDATA.GEN_AI_RESPONSE_TEXT]
278277
assert response_text == expected_assistant_response
@@ -1383,7 +1382,6 @@ def original_invoke(self, *args, **kwargs):
13831382

13841383
parsed_messages = json.loads(messages_data)
13851384
assert isinstance(parsed_messages, list)
1386-
assert len(parsed_messages) == 2
1387-
assert "small message 4" in str(parsed_messages[0])
1388-
assert "small message 5" in str(parsed_messages[1])
1385+
assert len(parsed_messages) == 1
1386+
assert "small message 5" in str(parsed_messages[0])
13891387
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5

tests/integrations/litellm/test_litellm.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,9 +752,8 @@ def test_litellm_message_truncation(sentry_init, capture_events):
752752

753753
parsed_messages = json.loads(messages_data)
754754
assert isinstance(parsed_messages, list)
755-
assert len(parsed_messages) == 2
756-
assert "small message 4" in str(parsed_messages[0])
757-
assert "small message 5" in str(parsed_messages[1])
755+
assert len(parsed_messages) == 1
756+
assert "small message 5" in str(parsed_messages[0])
758757
assert tx["_meta"]["spans"]["0"]["data"]["gen_ai.request.messages"][""]["len"] == 5
759758

760759

0 commit comments

Comments
 (0)