Skip to content

Commit 75f8a27

Browse files
feat(openai): Gate Chat Completions inputs behind data collection (#6965)
Apply `data_collection.gen_ai.inputs` to Chat Completions prompt messages, system instructions, and tool definitions, matching the Responses API. Preserve legacy `send_default_pii` behavior when data collection is not configured. Refs PY-2588 --------- Co-authored-by: Alexander Alderman Webb <alexander.webb@sentry.io>
1 parent 934bbd7 commit 75f8a27

2 files changed

Lines changed: 208 additions & 21 deletions

File tree

sentry_sdk/integrations/openai.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -483,19 +483,11 @@ def _set_completions_api_input_data(
483483
kwargs: "dict[str, Any]",
484484
integration: "OpenAIIntegration",
485485
) -> None:
486-
messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get(
487-
"messages"
488-
)
489-
490486
set_on_span = (
491487
span.set_attribute if isinstance(span, StreamedSpan) else span.set_data
492488
)
493-
tools = kwargs.get("tools")
494-
if tools is not None and _is_given(tools):
495-
set_on_span(
496-
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
497-
json.dumps(_transform_tool_definitions_completions(tools)),
498-
)
489+
490+
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
499491

500492
model = kwargs.get("model")
501493
if model is not None:
@@ -525,17 +517,48 @@ def _set_completions_api_input_data(
525517
if reasoning_level is not None and _is_given(reasoning_level):
526518
set_on_span(SPANDATA.GEN_AI_REQUEST_REASONING_LEVEL, reasoning_level)
527519

528-
if (
529-
not should_send_default_pii()
530-
or not integration.include_prompts
531-
or messages is None
532-
):
533-
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
520+
client = sentry_sdk.get_client()
521+
if has_data_collection_enabled(client.options):
522+
if (
523+
integration.include_prompts
524+
and client.options["data_collection"]["gen_ai"]["inputs"]
525+
):
526+
tools = kwargs.get("tools")
527+
if tools is not None and _is_given(tools):
528+
set_on_span(
529+
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
530+
json.dumps(_transform_tool_definitions_completions(tools)),
531+
)
532+
else:
533+
# Pre-data collection this was always set, so this needs to be left here for now until
534+
# we deprecate `send_default_pii`. Once we do, this 'else' branch should be removed,
535+
# and the above branch placed below the "if not should_send_default_pii() or not integration.include_prompts"
536+
# line below
537+
tools = kwargs.get("tools")
538+
if tools is not None and _is_given(tools):
539+
set_on_span(
540+
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
541+
json.dumps(_transform_tool_definitions_completions(tools)),
542+
)
543+
544+
messages: "Optional[Union[str, Iterable[ChatCompletionMessageParam]]]" = kwargs.get(
545+
"messages"
546+
)
547+
548+
if has_data_collection_enabled(client.options):
549+
# This takes precedence over the global data collection settings
550+
if not integration.include_prompts:
551+
return
552+
if not client.options["data_collection"]["gen_ai"]["inputs"]:
553+
return
554+
elif not should_send_default_pii() or not integration.include_prompts:
555+
return
556+
557+
if messages is None:
534558
return
535559

536560
if isinstance(messages, str):
537561
normalized_messages = normalize_message_roles([messages]) # type: ignore
538-
client = sentry_sdk.get_client()
539562
scope = sentry_sdk.get_current_scope()
540563
messages_data = (
541564
truncate_and_annotate_messages(normalized_messages, span, scope)
@@ -546,12 +569,10 @@ def _set_completions_api_input_data(
546569
set_data_normalized(
547570
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
548571
)
549-
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
550572
return
551573

552574
# dict special case following https://github.com/openai/openai-python/blob/3e0c05b84a2056870abf3bd6a5e7849020209cc3/src/openai/_utils/_transform.py#L194-L197
553575
if not isinstance(messages, Iterable) or isinstance(messages, dict):
554-
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
555576
return
556577

557578
messages = list(messages)
@@ -583,8 +604,6 @@ def _set_completions_api_input_data(
583604
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages_data, unpack=False
584605
)
585606

586-
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, "chat")
587-
588607

589608
def _set_embeddings_input_data(
590609
span: "Union[Span, StreamedSpan]",

tests/integrations/openai/test_openai.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,26 @@ async def __call__(self, *args, **kwargs):
137137
}
138138
]
139139

140+
EXAMPLE_COMPLETIONS_TOOLS = [
141+
{
142+
"type": "function",
143+
"function": {
144+
"name": "get_current_weather",
145+
"description": "Get the current weather in a given location",
146+
"parameters": {
147+
"type": "object",
148+
"properties": {
149+
"location": {
150+
"type": "string",
151+
"description": "The city and state, e.g. San Francisco, CA",
152+
},
153+
},
154+
"required": ["location"],
155+
},
156+
},
157+
}
158+
]
159+
140160

141161
@pytest.mark.skipif(
142162
OPENAI_VERSION <= (2, 10, 0),
@@ -639,6 +659,154 @@ def test_nonstreaming_chat_completion(
639659
assert span["data"]["gen_ai.usage.total_tokens"] == 30
640660

641661

662+
@pytest.mark.skipif(
663+
OPENAI_VERSION <= (1, 1, 0),
664+
reason="OpenAI versions <=1.1.0 do not support the tools parameter.",
665+
)
666+
@pytest.mark.parametrize("span_streaming", [True, False])
667+
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
668+
@pytest.mark.parametrize(
669+
"data_collection,include_prompts,expected_present,expected_absent",
670+
[
671+
pytest.param(
672+
{"gen_ai": {"inputs": True}},
673+
True,
674+
{
675+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS: json.dumps(
676+
[{"type": "text", "content": "You are a helpful assistant."}]
677+
),
678+
SPANDATA.GEN_AI_REQUEST_MESSAGES: safe_serialize(
679+
[{"role": "user", "content": "hello"}]
680+
),
681+
SPANDATA.GEN_AI_TOOL_DEFINITIONS: safe_serialize(EXAMPLE_TOOLS),
682+
},
683+
[],
684+
id="inputs-enabled",
685+
),
686+
pytest.param(
687+
{"gen_ai": {"inputs": False}},
688+
True,
689+
{},
690+
[
691+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
692+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
693+
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
694+
],
695+
id="inputs-disabled",
696+
),
697+
pytest.param(
698+
{},
699+
True,
700+
{
701+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS: json.dumps(
702+
[{"type": "text", "content": "You are a helpful assistant."}]
703+
),
704+
SPANDATA.GEN_AI_REQUEST_MESSAGES: safe_serialize(
705+
[{"role": "user", "content": "hello"}]
706+
),
707+
SPANDATA.GEN_AI_TOOL_DEFINITIONS: safe_serialize(EXAMPLE_TOOLS),
708+
},
709+
[],
710+
id="gen-ai-omitted-defaults-to-enabled",
711+
),
712+
pytest.param(
713+
{"gen_ai": {"inputs": True}},
714+
False,
715+
{},
716+
[
717+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
718+
SPANDATA.GEN_AI_REQUEST_MESSAGES,
719+
SPANDATA.GEN_AI_TOOL_DEFINITIONS,
720+
],
721+
id="include-prompts-disabled-overrides-inputs-enabled",
722+
),
723+
],
724+
)
725+
def test_completions_api_data_collection(
726+
sentry_init,
727+
capture_events,
728+
capture_items,
729+
data_collection,
730+
include_prompts,
731+
expected_present,
732+
expected_absent,
733+
nonstreaming_chat_completions_model_response,
734+
stream_gen_ai_spans,
735+
span_streaming,
736+
):
737+
sentry_init(
738+
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
739+
disabled_integrations=[StdlibIntegration],
740+
traces_sample_rate=1.0,
741+
_experiments={"data_collection": data_collection},
742+
stream_gen_ai_spans=stream_gen_ai_spans,
743+
trace_lifecycle="stream" if span_streaming else "static",
744+
)
745+
746+
client = OpenAI(api_key="z")
747+
client.chat.completions._post = mock.Mock(
748+
return_value=nonstreaming_chat_completions_model_response(
749+
response_id="chat-id",
750+
response_model="gpt-3.5-turbo",
751+
message_content="the model response",
752+
created=10000000,
753+
usage=CompletionUsage(
754+
prompt_tokens=20,
755+
completion_tokens=10,
756+
total_tokens=30,
757+
),
758+
)
759+
)
760+
761+
create_kwargs = {
762+
"model": "some-model",
763+
"messages": [
764+
{"role": "system", "content": "You are a helpful assistant."},
765+
{"role": "user", "content": "hello"},
766+
],
767+
"max_tokens": 100,
768+
"presence_penalty": 0.1,
769+
"frequency_penalty": 0.2,
770+
"temperature": 0.7,
771+
"top_p": 0.9,
772+
"tools": EXAMPLE_COMPLETIONS_TOOLS,
773+
}
774+
775+
if span_streaming or stream_gen_ai_spans:
776+
items = capture_items("span")
777+
778+
with start_transaction(name="openai tx"):
779+
client.chat.completions.create(**create_kwargs)
780+
781+
sentry_sdk.flush()
782+
(span,) = (item.payload for item in items)
783+
span_data = span["attributes"]
784+
else:
785+
events = capture_events()
786+
787+
with start_transaction(name="openai tx"):
788+
client.chat.completions.create(**create_kwargs)
789+
790+
(transaction,) = events
791+
(span,) = transaction["spans"]
792+
span_data = span["data"]
793+
794+
assert span_data[SPANDATA.GEN_AI_OPERATION_NAME] == "chat"
795+
assert span_data[SPANDATA.GEN_AI_REQUEST_MODEL] == "some-model"
796+
assert span_data[SPANDATA.GEN_AI_REQUEST_MAX_TOKENS] == 100
797+
assert span_data[SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY] == 0.1
798+
assert span_data[SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY] == 0.2
799+
assert span_data[SPANDATA.GEN_AI_REQUEST_TEMPERATURE] == 0.7
800+
assert span_data[SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9
801+
assert span_data[SPANDATA.GEN_AI_SYSTEM] == "openai"
802+
803+
for key, value in expected_present.items():
804+
assert span_data[key] == value
805+
806+
for key in expected_absent:
807+
assert key not in span_data
808+
809+
642810
@pytest.mark.parametrize("span_streaming", [True, False])
643811
@pytest.mark.parametrize("stream_gen_ai_spans", [True, False])
644812
@pytest.mark.asyncio

0 commit comments

Comments
 (0)