-
Notifications
You must be signed in to change notification settings - Fork 572
fix(integrations): openai/openai-agents: convert input message format #5248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
1f32952
795bcea
a623e13
3d3ce5b
ce29e47
7074f0b
e8a1adc
c1a2239
bd46a6a
04b27f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| from sentry_sdk.ai.utils import ( | ||
| GEN_AI_ALLOWED_MESSAGE_ROLES, | ||
| normalize_message_roles, | ||
| parse_data_uri, | ||
| set_data_normalized, | ||
| normalize_message_role, | ||
| truncate_and_annotate_messages, | ||
|
|
@@ -27,6 +28,124 @@ | |
| raise DidNotEnable("OpenAI Agents not installed") | ||
|
|
||
|
|
||
| def _transform_openai_agents_content_part( | ||
| content_part: "dict[str, Any]", | ||
| ) -> "dict[str, Any]": | ||
| """ | ||
| Transform an OpenAI Agents content part to Sentry-compatible format. | ||
| Handles multimodal content (images, audio, files) by converting them | ||
| to the standardized format: | ||
| - base64 encoded data -> type: "blob" | ||
| - URL references -> type: "uri" | ||
| - file_id references -> type: "file" | ||
| """ | ||
| if not isinstance(content_part, dict): | ||
| return content_part | ||
|
|
||
| part_type = content_part.get("type") | ||
|
|
||
| # Handle input_text (OpenAI Agents SDK text format) -> normalize to standard text format | ||
| if part_type == "input_text": | ||
| return { | ||
| "type": "text", | ||
| "text": content_part.get("text", ""), | ||
| } | ||
|
|
||
| # Handle image_url (OpenAI vision format) and input_image (OpenAI Agents SDK format) | ||
| if part_type in ("image_url", "input_image"): | ||
| # Get URL from either format | ||
| if part_type == "image_url": | ||
| image_url = content_part.get("image_url", {}) | ||
| url = ( | ||
| image_url.get("url", "") | ||
| if isinstance(image_url, dict) | ||
| else str(image_url) | ||
| ) | ||
| else: | ||
| # input_image format has image_url directly | ||
| url = content_part.get("image_url", "") | ||
|
|
||
| if url.startswith("data:"): | ||
| try: | ||
| mime_type, content = parse_data_uri(url) | ||
| return { | ||
| "type": "blob", | ||
| "modality": "image", | ||
| "mime_type": mime_type, | ||
| "content": content, | ||
| } | ||
| except ValueError: | ||
| # If parsing fails, return as URI | ||
| return { | ||
| "type": "uri", | ||
| "modality": "image", | ||
| "mime_type": "", | ||
| "uri": url, | ||
| } | ||
| else: | ||
| return { | ||
| "type": "uri", | ||
| "modality": "image", | ||
| "mime_type": "", | ||
| "uri": url, | ||
| } | ||
|
|
||
| # Handle input_audio (OpenAI audio input format) | ||
| if part_type == "input_audio": | ||
| input_audio = content_part.get("input_audio", {}) | ||
| audio_format = input_audio.get("format", "") | ||
|
Comment on lines
+96
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The code assumes 🔍 Detailed AnalysisThe function 💡 Suggested FixAdd 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| mime_type = f"audio/{audio_format}" if audio_format else "" | ||
| return { | ||
| "type": "blob", | ||
| "modality": "audio", | ||
| "mime_type": mime_type, | ||
| "content": input_audio.get("data", ""), | ||
| } | ||
|
|
||
| # Handle image_file (Assistants API file-based images) | ||
| if part_type == "image_file": | ||
| image_file = content_part.get("image_file", {}) | ||
| return { | ||
| "type": "file", | ||
| "modality": "image", | ||
| "mime_type": "", | ||
| "file_id": image_file.get("file_id", ""), | ||
| } | ||
|
|
||
| # Handle file (document attachments) | ||
| if part_type == "file": | ||
| file_data = content_part.get("file", {}) | ||
| return { | ||
| "type": "file", | ||
| "modality": "document", | ||
| "mime_type": "", | ||
| "file_id": file_data.get("file_id", ""), | ||
| } | ||
|
|
||
| return content_part | ||
|
|
||
|
|
||
| def _transform_openai_agents_message_content(content: "Any") -> "Any": | ||
| """ | ||
| Transform OpenAI Agents message content, handling both string content and | ||
| list of content parts. | ||
| """ | ||
| if isinstance(content, str): | ||
| return content | ||
|
|
||
| if isinstance(content, (list, tuple)): | ||
| transformed = [] | ||
| for item in content: | ||
| if isinstance(item, dict): | ||
| transformed.append(_transform_openai_agents_content_part(item)) | ||
| else: | ||
| transformed.append(item) | ||
| return transformed | ||
|
|
||
| return content | ||
|
|
||
|
|
||
| def _capture_exception(exc: "Any") -> None: | ||
| set_span_errored() | ||
|
|
||
|
|
@@ -128,13 +247,15 @@ def _set_input_data( | |
| if "role" in message: | ||
| normalized_role = normalize_message_role(message.get("role")) | ||
| content = message.get("content") | ||
| # Transform content to handle multimodal data (images, audio, files) | ||
| transformed_content = _transform_openai_agents_message_content(content) | ||
| request_messages.append( | ||
| { | ||
| "role": normalized_role, | ||
| "content": ( | ||
| [{"type": "text", "text": content}] | ||
| if isinstance(content, str) | ||
| else content | ||
| [{"type": "text", "text": transformed_content}] | ||
| if isinstance(transformed_content, str) | ||
| else transformed_content | ||
| ), | ||
| } | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing type check for
image_urlcauses crash on string inputMedium Severity
The
_convert_message_partsfunction assumesimage_urlis always a dict, but if it's a non-empty string, calling.get("url", "")on it raises anAttributeError. The equivalent code inopenai_agents/utils.pyhandles this case withisinstance(image_url, dict)check and falls back tostr(image_url). Since_set_input_dataruns before the actual API call, this crash would prevent the user's OpenAI call from executing entirely.