diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 04ae33daff..ee33ecf656 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -710,6 +710,21 @@ def _set_common_output_data( # Chat Completions API if hasattr(response, "choices") and response.choices is not None: + finish_reasons = [ + choice.finish_reason + for choice in response.choices + if hasattr(choice, "finish_reason") and choice.finish_reason is not None + ] + if len(finish_reasons) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + if has_data_collection_enabled(client.options): if client.options["data_collection"]["gen_ai"]["outputs"]: response_text = [ @@ -1046,6 +1061,7 @@ def _wrap_synchronous_completions_chunk_iterator( streaming_message_total_token_usage = None client = sentry_sdk.get_client() + finish_reasons: "list[str]" = [] for x in old_iterator: if isinstance(span, StreamedSpan): span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) @@ -1063,7 +1079,21 @@ def _wrap_synchronous_completions_chunk_iterator( if len(data_buf) <= choice_index: data_buf.append([]) data_buf[choice_index].append(content or "") + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reasons.append(choice.finish_reason) choice_index += 1 + if len(finish_reasons) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) if hasattr(x, "usage"): streaming_message_total_token_usage = x.usage @@ -1117,6 +1147,7 @@ async def _wrap_asynchronous_completions_chunk_iterator( streaming_message_total_token_usage = None client = sentry_sdk.get_client() + finish_reasons: "list[str]" = [] async for x in old_iterator: if isinstance(span, StreamedSpan): span.set_attribute(SPANDATA.GEN_AI_RESPONSE_MODEL, x.model) @@ -1134,7 +1165,21 @@ async def _wrap_asynchronous_completions_chunk_iterator( if len(data_buf) <= choice_index: data_buf.append([]) data_buf[choice_index].append(content or "") + if ( + hasattr(choice, "finish_reason") + and choice.finish_reason is not None + ): + finish_reasons.append(choice.finish_reason) choice_index += 1 + if len(finish_reasons) > 0: + if isinstance(span, StreamedSpan): + span.set_attribute( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) + else: + span.set_data( + SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons + ) if hasattr(x, "usage"): streaming_message_total_token_usage = x.usage diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index e578b03fdc..3c7fd78d22 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -911,6 +911,7 @@ def test_completions_api_data_collection_outputs( span_data = span["data"] assert span_data[SPANDATA.GEN_AI_RESPONSE_MODEL] == "gpt-3.5-turbo" + assert span_data[SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] if expect_output: assert "the model response" in span_data[SPANDATA.GEN_AI_RESPONSE_TEXT] @@ -1027,6 +1028,7 @@ async def test_completions_api_data_collection_outputs_async( span_data = span["data"] assert span_data[SPANDATA.GEN_AI_RESPONSE_MODEL] == "gpt-3.5-turbo" + assert span_data[SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] if expect_output: assert "the model response" in span_data[SPANDATA.GEN_AI_RESPONSE_TEXT] @@ -1790,6 +1792,7 @@ def test_streaming_chat_completion_no_prompts( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9 assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_MODEL] == "model-id" + assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] @@ -1844,6 +1847,7 @@ def test_streaming_chat_completion_no_prompts( assert span["data"][SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9 assert span["data"][SPANDATA.GEN_AI_RESPONSE_MODEL] == "model-id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -2702,6 +2706,7 @@ async def test_streaming_chat_completion_async_no_prompts( assert span["attributes"][SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9 assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_MODEL] == "model-id" + assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["attributes"] @@ -2758,6 +2763,7 @@ async def test_streaming_chat_completion_async_no_prompts( assert span["data"][SPANDATA.GEN_AI_REQUEST_TOP_P] == 0.9 assert span["data"][SPANDATA.GEN_AI_RESPONSE_MODEL] == "model-id" + assert span["data"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == ["stop"] assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -5017,6 +5023,11 @@ def test_completions_token_usage_manual_output_counting_choices( sentry_sdk.flush() (span,) = (item.payload for item in items) + assert span["attributes"][SPANDATA.GEN_AI_RESPONSE_FINISH_REASONS] == [ + "stop", + "stop", + "stop", + ] assert span["attributes"][SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 20 assert span["attributes"][SPANDATA.GEN_AI_USAGE_TOTAL_TOKENS] == 20 if tiktoken_encoding_if_installed():