Skip to content

Commit 6347fc7

Browse files
ericapisaniclaude
andcommitted
feat(pydantic-ai): Support ImageUrl content type in span instrumentation
Add handling for the pydantic-ai `ImageUrl` message content type in the pydantic-ai integration. For data URLs containing base64-encoded images, the content is redacted and replaced with a placeholder to avoid sending large binary payloads to Sentry. For regular HTTP URLs, the URL string is preserved as-is. Refactor binary content serialization into shared helper functions `_serialize_binary_content_item` and `_serialize_image_url_item` in `spans/utils.py` to remove duplication between `ai_client.py` and `invoke_agent.py`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 763b16a commit 6347fc7

5 files changed

Lines changed: 346 additions & 30 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
1+
import re
2+
13
SPAN_ORIGIN = "auto.ai.pydantic_ai"
4+
5+
# Matches data URLs with base64-encoded content, e.g. "data:image/png;base64,iVBORw0K..."
6+
# Group 1: MIME type (e.g. "image/png"), Group 2: base64 data
7+
DATA_URL_BASE64_REGEX = re.compile(
8+
r"^data:([a-zA-Z]+/[a-zA-Z]+);base64,([A-Za-z0-9+/\-_]+={0,2})$"
9+
)

‎sentry_sdk/integrations/pydantic_ai/spans/ai_client.py‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import json
22

33
import sentry_sdk
4-
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
54
from sentry_sdk.ai.utils import (
65
normalize_message_roles,
76
set_data_normalized,
87
truncate_and_annotate_messages,
9-
get_modality_from_mime_type,
108
)
119
from sentry_sdk.consts import OP, SPANDATA
1210
from sentry_sdk.utils import safe_serialize
@@ -21,7 +19,11 @@
2119
get_current_agent,
2220
get_is_streaming,
2321
)
24-
from .utils import _set_usage_data
22+
from .utils import (
23+
_serialize_binary_content_item,
24+
_serialize_image_url_item,
25+
_set_usage_data,
26+
)
2527

2628
from typing import TYPE_CHECKING
2729

@@ -40,6 +42,7 @@
4042
TextPart,
4143
ThinkingPart,
4244
BinaryContent,
45+
ImageUrl,
4346
)
4447
except ImportError:
4548
# Fallback if these classes are not available
@@ -50,6 +53,7 @@
5053
TextPart = None
5154
ThinkingPart = None
5255
BinaryContent = None
56+
ImageUrl = None
5357

5458

5559
def _transform_system_instructions(
@@ -158,22 +162,14 @@ def _set_input_messages(span: "sentry_sdk.tracing.Span", messages: "Any") -> Non
158162
for item in part.content:
159163
if isinstance(item, str):
160164
content.append({"type": "text", "text": item})
165+
elif ImageUrl and isinstance(item, ImageUrl):
166+
content.append(_serialize_image_url_item(item))
161167
elif BinaryContent and isinstance(item, BinaryContent):
162-
content.append(
163-
{
164-
"type": "blob",
165-
"modality": get_modality_from_mime_type(
166-
item.media_type
167-
),
168-
"mime_type": item.media_type,
169-
"content": BLOB_DATA_SUBSTITUTE,
170-
}
171-
)
168+
content.append(_serialize_binary_content_item(item))
172169
else:
173170
content.append(safe_serialize(item))
174171
else:
175172
content.append({"type": "text", "text": str(part.content)})
176-
177173
# Add message if we have content or tool calls
178174
if content or tool_calls:
179175
message: "Dict[str, Any]" = {"role": role}

‎sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import sentry_sdk
2-
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
32
from sentry_sdk.ai.utils import (
4-
get_modality_from_mime_type,
53
get_start_span_function,
64
normalize_message_roles,
75
set_data_normalized,
@@ -16,17 +14,22 @@
1614
_set_model_data,
1715
_should_send_prompts,
1816
)
19-
from .utils import _set_usage_data
17+
from .utils import (
18+
_serialize_binary_content_item,
19+
_serialize_image_url_item,
20+
_set_usage_data,
21+
)
2022

2123
from typing import TYPE_CHECKING
2224

2325
if TYPE_CHECKING:
2426
from typing import Any
2527

2628
try:
27-
from pydantic_ai.messages import BinaryContent # type: ignore
29+
from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore
2830
except ImportError:
2931
BinaryContent = None
32+
ImageUrl = None
3033

3134

3235
def invoke_agent_span(
@@ -105,17 +108,10 @@ def invoke_agent_span(
105108
for item in user_prompt:
106109
if isinstance(item, str):
107110
content.append({"text": item, "type": "text"})
111+
elif ImageUrl and isinstance(item, ImageUrl):
112+
content.append(_serialize_image_url_item(item))
108113
elif BinaryContent and isinstance(item, BinaryContent):
109-
content.append(
110-
{
111-
"type": "blob",
112-
"modality": get_modality_from_mime_type(
113-
item.media_type
114-
),
115-
"mime_type": item.media_type,
116-
"content": BLOB_DATA_SUBSTITUTE,
117-
}
118-
)
114+
content.append(_serialize_binary_content_item(item))
119115
if content:
120116
messages.append(
121117
{

‎sentry_sdk/integrations/pydantic_ai/spans/utils.py‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,56 @@
11
"""Utility functions for PydanticAI span instrumentation."""
22

33
import sentry_sdk
4+
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
5+
from sentry_sdk.ai.utils import get_modality_from_mime_type
46
from sentry_sdk.consts import SPANDATA
57

8+
from ..consts import DATA_URL_BASE64_REGEX
9+
610
from typing import TYPE_CHECKING
711

812
if TYPE_CHECKING:
9-
from typing import Union, Dict, Any, List
13+
from typing import Union, Dict, Any, List, Optional
1014
from pydantic_ai.usage import RequestUsage, RunUsage # type: ignore
1115

16+
try:
17+
from pydantic_ai.messages import BinaryContent, ImageUrl # type: ignore
18+
except ImportError:
19+
BinaryContent = None
20+
ImageUrl = None
21+
22+
23+
def _serialize_image_url_item(item: "Any") -> "Dict[str, Any]":
24+
"""Serialize an ImageUrl content item for span data.
25+
26+
For data URLs containing base64-encoded images, the content is redacted.
27+
For regular HTTP URLs, the URL string is preserved.
28+
"""
29+
data_url_matches = DATA_URL_BASE64_REGEX.match(item.url)
30+
31+
if data_url_matches:
32+
mime_type = data_url_matches[1] or "image"
33+
return {
34+
"type": "image",
35+
"mime_type": mime_type,
36+
"content": BLOB_DATA_SUBSTITUTE,
37+
}
38+
39+
return {
40+
"type": "image",
41+
"content": str(item.url),
42+
}
43+
44+
45+
def _serialize_binary_content_item(item: "Any") -> "Dict[str, Any]":
46+
"""Serialize a BinaryContent item for span data, redacting the blob data."""
47+
return {
48+
"type": "blob",
49+
"modality": get_modality_from_mime_type(item.media_type),
50+
"mime_type": item.media_type,
51+
"content": BLOB_DATA_SUBSTITUTE,
52+
}
53+
1254

1355
def _set_usage_data(
1456
span: "sentry_sdk.tracing.Span", usage: "Union[RequestUsage, RunUsage]"

0 commit comments

Comments
 (0)