Skip to content
Open
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
4 changes: 4 additions & 0 deletions sentry_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,7 @@ class SDKInfo(TypedDict):
)

HttpStatusCodeRange = Union[int, Container[int]]

class TextPart(TypedDict):
type: Literal["text"]
content: str
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
178 changes: 164 additions & 14 deletions sentry_sdk/integrations/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@
if TYPE_CHECKING:
from typing import (
Any,
Iterable,
List,
Optional,
Callable,
AsyncIterator,
Iterator,
Union,
Iterable,
)
from sentry_sdk.tracing import Span
from sentry_sdk._types import TextPart

from openai.types.responses import ResponseInputParam
from openai.types.responses import ResponseInputParam, ResponseInputItemParam
from openai import Omit

try:
try:
Expand All @@ -52,7 +54,11 @@
from openai.resources import Embeddings, AsyncEmbeddings

if TYPE_CHECKING:
from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk
from openai.types.chat import (
ChatCompletionMessageParam,
ChatCompletionChunk,
ChatCompletionSystemMessageParam,
)
except ImportError:
raise DidNotEnable("OpenAI not installed")

Expand Down Expand Up @@ -193,6 +199,69 @@ def _calculate_token_usage(
)


def _is_system_instruction_completions(message: "ChatCompletionMessageParam") -> bool:
return isinstance(message, dict) and message.get("role") == "system"


def _get_system_instructions_completions(
messages: "Iterable[ChatCompletionMessageParam]",
) -> "list[ChatCompletionSystemMessageParam]":
system_instructions = []

for message in messages:
if _is_system_instruction_completions(message):
system_instructions.append(message)

return system_instructions


def _is_system_instruction_responses(message: "ResponseInputItemParam") -> bool:
return (
isinstance(message, dict)
and message.get("type") == "message"
and message.get("role") == "system"
)


def _get_system_instructions_responses(
messages: "Union[str, ResponseInputParam]",
) -> "list[ResponseInputItemParam]":
if isinstance(messages, str):
return []

system_instructions = []

for message in messages:
if _is_system_instruction_responses(message):
system_instructions.append(message)

return system_instructions


def _transform_system_instructions(
system_instructions: "list[ChatCompletionSystemMessageParam]",
) -> "list[TextPart]":
instruction_text_parts: "list[TextPart]" = []

for instruction in system_instructions:
if not isinstance(instruction, dict):
continue

content = instruction.get("content")

if isinstance(content, str):
instruction_text_parts.append({"type": "text", "content": content})

elif isinstance(content, list):
for part in content:
if isinstance(part, dict) and part.get("type") == "text":
text = part.get("text", "")
if text:
instruction_text_parts.append({"type": "text", "content": text})

return instruction_text_parts


def _get_input_messages(
kwargs: "dict[str, Any]",
) -> "Optional[Union[Iterable[Any], list[str]]]":
Expand Down Expand Up @@ -243,24 +312,72 @@ def _set_responses_api_input_data(
kwargs: "dict[str, Any]",
integration: "OpenAIIntegration",
) -> None:
messages: "Optional[Union[ResponseInputParam, list[str]]]" = _get_input_messages(
kwargs
)
messages: "Optional[Union[str, ResponseInputParam]]" = kwargs.get("input")

if messages is None:
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "responses")
_commmon_set_input_data(span, kwargs)
return

explicit_instructions: "Union[Optional[str], Omit]" = kwargs.get("instructions")
system_instructions = _get_system_instructions_responses(messages)
if (
messages is not None
and len(messages) > 0
(
(explicit_instructions is not None and _is_given(explicit_instructions))
or len(system_instructions) > 0
)
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages) # type: ignore
instructions_text_parts: "list[TextPart]" = []
if explicit_instructions is not None and _is_given(explicit_instructions):
instructions_text_parts.append(
{
"type": "text",
"content": explicit_instructions,
}
)
# Deliberate use of function accepting completions API type because
# of shared structure FOR THIS PURPOSE ONLY.
instructions_text_parts += _transform_system_instructions(system_instructions)

set_data_normalized(
span,
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
instructions_text_parts,
unpack=False,
)

if (
isinstance(messages, str)
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
)

elif should_send_default_pii() and integration.include_prompts:
non_system_messages = [
message
for message in messages
if not _is_system_instruction_responses(message)
]
if len(non_system_messages) > 0:
normalized_messages = normalize_message_roles(non_system_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)

Expand All @@ -270,23 +387,56 @@ def _set_completions_api_input_data(
kwargs: "dict[str, Any]",
integration: "OpenAIIntegration",
) -> None:
messages: "Optional[Union[Iterable[ChatCompletionMessageParam], list[str]]]" = (
_get_input_messages(kwargs)
messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get(
"messages"
)

if messages is None:
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
_commmon_set_input_data(span, kwargs)
return

system_instructions = _get_system_instructions_completions(messages)
if (
messages is not None
and len(messages) > 0 # type: ignore
len(system_instructions) > 0
and should_send_default_pii()
and integration.include_prompts
):
normalized_messages = normalize_message_roles(messages) # type: ignore
set_data_normalized(
span,
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
_transform_system_instructions(system_instructions),
unpack=False,
)

if (
isinstance(messages, str)
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
)
elif should_send_default_pii() and integration.include_prompts:
non_system_messages = [
message
for message in messages
if not _is_system_instruction_completions(message)
]
if len(non_system_messages) > 0:
normalized_messages = normalize_message_roles(non_system_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)
Expand Down
Loading
Loading