diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7e7a25f65d..649b21a33b 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -593,6 +593,12 @@ class SPANDATA: Example: [{"role": "assistant", "parts": [{"type": "text", "content": "The weather in Paris is currently rainy with a temperature of 57°F."}], "finish_reason": "stop"}] """ + GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK = "gen_ai.response.time_to_first_chunk" + """ + Time in seconds when the first response content chunk arrived in streaming responses. + Example: 0.6853435 + """ + GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN = "gen_ai.response.time_to_first_token" """ The time it took to receive the first token from the model. diff --git a/sentry_sdk/integrations/openai.py b/sentry_sdk/integrations/openai.py index 48223e7041..5ba27fc840 100644 --- a/sentry_sdk/integrations/openai.py +++ b/sentry_sdk/integrations/openai.py @@ -978,7 +978,7 @@ def _wrap_synchronous_completions_chunk_iterator( with capture_internal_exceptions(): if ttft is not None: set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, ttft ) all_responses = None if len(data_buf) > 0: @@ -1046,7 +1046,7 @@ async def _wrap_asynchronous_completions_chunk_iterator( with capture_internal_exceptions(): if ttft is not None: set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, ttft ) all_responses = None if len(data_buf) > 0: @@ -1117,7 +1117,7 @@ def _wrap_synchronous_responses_event_iterator( with capture_internal_exceptions(): if ttft is not None: set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, ttft ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] @@ -1187,7 +1187,7 @@ async def _wrap_asynchronous_responses_event_iterator( with capture_internal_exceptions(): if ttft is not None: set_data_normalized( - span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + span, SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, ttft ) if len(data_buf) > 0: all_responses = ["".join(chunk) for chunk in data_buf] diff --git a/sentry_sdk/integrations/openai_agents/patches/models.py b/sentry_sdk/integrations/openai_agents/patches/models.py index 34f5ddf924..2fb72295dc 100644 --- a/sentry_sdk/integrations/openai_agents/patches/models.py +++ b/sentry_sdk/integrations/openai_agents/patches/models.py @@ -152,7 +152,7 @@ async def wrapped_stream_response(*args: "Any", **kwargs: "Any") -> "Any": if not ttft_recorded and hasattr(event, "delta"): ttft = time.perf_counter() - start_time span.set_attribute( - SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN, ttft + SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK, ttft ) ttft_recorded = True diff --git a/tests/integrations/openai/test_openai.py b/tests/integrations/openai/test_openai.py index ac90597d13..ba5b2ffb65 100644 --- a/tests/integrations/openai/test_openai.py +++ b/tests/integrations/openai/test_openai.py @@ -4991,7 +4991,7 @@ async def test_ai_client_span_streaming_responses_async_api( "gen_ai.response.model": "response-model-id", "gen_ai.response.streaming": True, SPANDATA.GEN_AI_PROVIDER_NAME: "openai", - "gen_ai.response.time_to_first_token": mock.ANY, + SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK: mock.ANY, "gen_ai.usage.input_tokens": 20, "gen_ai.usage.input_tokens.cached": 5, "gen_ai.usage.output_tokens": 10, @@ -5481,8 +5481,8 @@ def test_streaming_chat_completion_ttft( assert span["attributes"]["sentry.op"] == "gen_ai.chat" # Verify TTFT is captured - assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["attributes"] - ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["attributes"] + ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK] assert isinstance(ttft, float) assert ttft > 0 @@ -5564,8 +5564,8 @@ async def test_streaming_chat_completion_ttft_async( assert span["attributes"]["sentry.op"] == "gen_ai.chat" # Verify TTFT is captured - assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["attributes"] - ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["attributes"] + ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK] assert isinstance(ttft, float) assert ttft > 0 @@ -5614,8 +5614,8 @@ def test_streaming_responses_api_ttft( assert span["attributes"]["sentry.op"] == "gen_ai.responses" # Verify TTFT is captured - assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["attributes"] - ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["attributes"] + ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK] assert isinstance(ttft, float) assert ttft > 0 @@ -5666,8 +5666,8 @@ async def test_streaming_responses_api_ttft_async( assert span["attributes"]["sentry.op"] == "gen_ai.responses" # Verify TTFT is captured - assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in span["attributes"] - ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN] + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in span["attributes"] + ttft = span["attributes"][SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK] assert isinstance(ttft, float) assert ttft > 0 diff --git a/tests/integrations/openai_agents/test_openai_agents.py b/tests/integrations/openai_agents/test_openai_agents.py index 2bde3ee306..0789d3d2e2 100644 --- a/tests/integrations/openai_agents/test_openai_agents.py +++ b/tests/integrations/openai_agents/test_openai_agents.py @@ -4107,7 +4107,7 @@ async def test_streaming_ttft_on_chat_span( assert len(chat_spans) >= 1 chat_span = chat_spans[0] - assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_TOKEN in chat_span["attributes"] + assert SPANDATA.GEN_AI_RESPONSE_TIME_TO_FIRST_CHUNK in chat_span["attributes"] assert chat_span["attributes"].get(SPANDATA.GEN_AI_RESPONSE_STREAMING) is True