Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions sentry_sdk/ai/_openai_completions_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from openai.types.chat import (
ChatCompletionMessageParam,
ChatCompletionSystemMessageParam,
)
from typing import Iterable, Union


def _get_system_instructions(
messages: "Iterable[Union[ChatCompletionMessageParam, str]]",
) -> "list[ChatCompletionSystemMessageParam]":
system_messages = []

for message in messages:
if isinstance(message, dict) and message.get("role") == "system":
system_messages.append(message)

return system_messages
6 changes: 6 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,12 @@ class SPANDATA:
Example: 2048
"""

GEN_AI_SYSTEM_INSTRUCTIONS = "gen_ai.system_instructions"
"""
The system instructions passed to the model.
Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}]
"""

GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages"
"""
The messages passed to the model. The "content" can be a string or an array of objects.
Expand Down
10 changes: 10 additions & 0 deletions sentry_sdk/integrations/litellm.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
truncate_and_annotate_messages,
transform_openai_content_part,
)
from sentry_sdk.ai._openai_completions_api import _get_system_instructions
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.scope import should_send_default_pii
Expand Down Expand Up @@ -129,6 +130,15 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None:
else:
# For chat, look for the 'messages' parameter
messages = kwargs.get("messages", [])

system_instructions = _get_system_instructions(messages)
set_data_normalized(
span,
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
system_instructions,
unpack=False,
)

if messages:
scope = sentry_sdk.get_current_scope()
messages = _convert_message_parts(messages)
Expand Down
180 changes: 145 additions & 35 deletions sentry_sdk/integrations/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
normalize_message_roles,
truncate_and_annotate_messages,
)
from sentry_sdk.ai._openai_completions_api import (
_get_system_instructions as _get_system_instructions_completions,
)
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations import DidNotEnable, Integration
from sentry_sdk.scope import should_send_default_pii
Expand All @@ -23,9 +26,20 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any, Iterable, List, Optional, Callable, AsyncIterator, Iterator
from typing import (
Any,
Iterable,
List,
Optional,
Callable,
AsyncIterator,
Iterator,
Union,
)
from sentry_sdk.tracing import Span

from openai.types.responses import ResponseInputParam, ResponseInputItemParam

try:
try:
from openai import NotGiven
Expand Down Expand Up @@ -182,12 +196,28 @@ def _calculate_token_usage(
)


def _set_input_data(
span: "Span",
def _get_system_instructions_responses(
input_items: "Union[ResponseInputParam, list[str]]",
) -> "list[ResponseInputItemParam]":
if isinstance(input_items, str):
return []

system_messages = []

for item in input_items:
if (
isinstance(item, dict)
and item.get("type") == "message"
and item.get("role") == "system"
):
system_messages.append(item)

return system_messages


def _get_input_messages(
kwargs: "dict[str, Any]",
operation: str,
integration: "OpenAIIntegration",
) -> None:
) -> "Optional[Union[Iterable[Any], list[str]]]":
# Input messages (the prompt or data sent to the model)
messages = kwargs.get("messages")
if messages is None:
Expand All @@ -196,29 +226,15 @@ def _set_input_data(
if isinstance(messages, str):
messages = [messages]

if (
messages is not None
and len(messages) > 0
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages)
scope = sentry_sdk.get_current_scope()
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
if messages_data is not None:
# Use appropriate field based on operation type
if operation == "embeddings":
set_data_normalized(
span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False
)
else:
set_data_normalized(
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
)
return messages


def _commmon_set_input_data(
span: "Span",
kwargs: "dict[str, Any]",
) -> None:
# Input attributes: Common
set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, "openai")
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation)

# Input attributes: Optional
kwargs_keys_to_attributes = {
Expand All @@ -244,6 +260,103 @@ def _set_input_data(
)


def _set_responses_api_input_data(
span: "Span",
kwargs: "dict[str, Any]",
integration: "OpenAIIntegration",
) -> None:
messages: "Optional[Union[ResponseInputParam, list[str]]]" = _get_input_messages(
kwargs
)

if messages is not None:
system_instructions = _get_system_instructions_responses(messages)
set_data_normalized(
span,
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
system_instructions,
unpack=False,
)

if (
messages is not None
and len(messages) > 0
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages) # type: ignore
scope = sentry_sdk.get_current_scope()
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
if messages_data is not None:
set_data_normalized(
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
)

set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses")
_commmon_set_input_data(span, kwargs)


def _set_completions_api_input_data(
span: "Span",
kwargs: "dict[str, Any]",
integration: "OpenAIIntegration",
) -> None:
messages: "Optional[Union[Iterable[ChatCompletionMessageParam], list[str]]]" = (
_get_input_messages(kwargs)
)

if messages is not None:
system_instructions = _get_system_instructions_completions(messages)
set_data_normalized(
span,
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
system_instructions,
unpack=False,
)

if (
messages is not None
and len(messages) > 0 # type: ignore
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages) # type: ignore
scope = sentry_sdk.get_current_scope()
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
if messages_data is not None:
set_data_normalized(
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
)

set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
_commmon_set_input_data(span, kwargs)


def _set_embeddings_input_data(
span: "Span",
kwargs: "dict[str, Any]",
integration: "OpenAIIntegration",
) -> None:
messages = _get_input_messages(kwargs)

if (
messages is not None
and len(messages) > 0 # type: ignore
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages) # type: ignore
scope = sentry_sdk.get_current_scope()
messages_data = truncate_and_annotate_messages(normalized_messages, span, scope)
if messages_data is not None:
set_data_normalized(
span, SPANDATA.GEN_AI_EMBEDDINGS_INPUT, messages_data, unpack=False
)

set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "embeddings")
_commmon_set_input_data(span, kwargs)


def _set_output_data(
span: "Span",
response: "Any",
Expand Down Expand Up @@ -454,16 +567,15 @@ def _new_chat_completion_common(f: "Any", *args: "Any", **kwargs: "Any") -> "Any
return f(*args, **kwargs)

model = kwargs.get("model")
operation = "chat"

span = sentry_sdk.start_span(
op=consts.OP.GEN_AI_CHAT,
name=f"{operation} {model}",
name=f"chat {model}",
origin=OpenAIIntegration.origin,
)
span.__enter__()

_set_input_data(span, kwargs, operation, integration)
_set_completions_api_input_data(span, kwargs, integration)

response = yield f, args, kwargs

Expand Down Expand Up @@ -546,14 +658,13 @@ def _new_embeddings_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "A
return f(*args, **kwargs)

model = kwargs.get("model")
operation = "embeddings"

with sentry_sdk.start_span(
op=consts.OP.GEN_AI_EMBEDDINGS,
name=f"{operation} {model}",
name=f"embeddings {model}",
origin=OpenAIIntegration.origin,
) as span:
_set_input_data(span, kwargs, operation, integration)
_set_embeddings_input_data(span, kwargs, integration)

response = yield f, args, kwargs

Expand Down Expand Up @@ -634,16 +745,15 @@ def _new_responses_create_common(f: "Any", *args: "Any", **kwargs: "Any") -> "An
return f(*args, **kwargs)

model = kwargs.get("model")
operation = "responses"

span = sentry_sdk.start_span(
op=consts.OP.GEN_AI_RESPONSES,
name=f"{operation} {model}",
name=f"responses {model}",
origin=OpenAIIntegration.origin,
)
span.__enter__()

_set_input_data(span, kwargs, operation, integration)
_set_responses_api_input_data(span, kwargs, integration)

response = yield f, args, kwargs

Expand Down
Loading