Skip to content

Commit 141eaaa

Browse files
fix(integrations): Anthropic: add content transformation for images and documents (#5276)
### Description Handling of non-text request messages #### Issues Closes https://linear.app/getsentry/issue/TET-1636/redact-images-anthropic
1 parent 8a48ad2 commit 141eaaa

File tree

2 files changed

+763
-11
lines changed

2 files changed

+763
-11
lines changed

sentry_sdk/integrations/anthropic.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
normalize_message_roles,
1212
truncate_and_annotate_messages,
1313
get_start_span_function,
14+
transform_anthropic_content_part,
1415
)
1516
from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS
1617
from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
@@ -122,6 +123,27 @@ def _collect_ai_data(
122123
return model, input_tokens, output_tokens, content_blocks
123124

124125

126+
def _transform_anthropic_content_block(
127+
content_block: "dict[str, Any]",
128+
) -> "dict[str, Any]":
129+
"""
130+
Transform an Anthropic content block using the Anthropic-specific transformer,
131+
with special handling for Anthropic's text-type documents.
132+
"""
133+
# Handle Anthropic's text-type documents specially (not covered by shared function)
134+
if content_block.get("type") == "document":
135+
source = content_block.get("source")
136+
if isinstance(source, dict) and source.get("type") == "text":
137+
return {
138+
"type": "text",
139+
"text": source.get("data", ""),
140+
}
141+
142+
# Use Anthropic-specific transformation
143+
result = transform_anthropic_content_part(content_block)
144+
return result if result is not None else content_block
145+
146+
125147
def _set_input_data(
126148
span: "Span", kwargs: "dict[str, Any]", integration: "AnthropicIntegration"
127149
) -> None:
@@ -166,19 +188,41 @@ def _set_input_data(
166188
and "content" in message
167189
and isinstance(message["content"], (list, tuple))
168190
):
191+
transformed_content = []
169192
for item in message["content"]:
170-
if item.get("type") == "tool_result":
171-
normalized_messages.append(
172-
{
173-
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.TOOL,
174-
"content": { # type: ignore[dict-item]
175-
"tool_use_id": item.get("tool_use_id"),
176-
"output": item.get("content"),
177-
},
178-
}
179-
)
193+
# Skip tool_result items - they can contain images/documents
194+
# with nested structures that are difficult to redact properly
195+
if isinstance(item, dict) and item.get("type") == "tool_result":
196+
continue
197+
198+
# Transform content blocks (images, documents, etc.)
199+
transformed_content.append(
200+
_transform_anthropic_content_block(item)
201+
if isinstance(item, dict)
202+
else item
203+
)
204+
205+
# If there are non-tool-result items, add them as a message
206+
if transformed_content:
207+
normalized_messages.append(
208+
{
209+
"role": message.get("role"),
210+
"content": transformed_content,
211+
}
212+
)
180213
else:
181-
normalized_messages.append(message)
214+
# Transform content for non-list messages or assistant messages
215+
transformed_message = message.copy()
216+
if "content" in transformed_message:
217+
content = transformed_message["content"]
218+
if isinstance(content, (list, tuple)):
219+
transformed_message["content"] = [
220+
_transform_anthropic_content_block(item)
221+
if isinstance(item, dict)
222+
else item
223+
for item in content
224+
]
225+
normalized_messages.append(transformed_message)
182226

183227
role_normalized_messages = normalize_message_roles(normalized_messages)
184228
scope = sentry_sdk.get_current_scope()

0 commit comments

Comments
 (0)