diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/131.added b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/131.added new file mode 100644 index 00000000..dc1eff22 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/.changelog/131.added @@ -0,0 +1 @@ +Add OpenAI Responses API stream instrumentation for sync and async clients. diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/__init__.py index 55d74e10..6e9853e0 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/__init__.py @@ -80,7 +80,9 @@ ) from .patch_responses import ( async_responses_create, + async_responses_stream, responses_create, + responses_stream, ) @@ -171,11 +173,21 @@ def _instrument(self, **kwargs): "Responses.create", responses_create(handler), ) + wrap_function_wrapper( + "openai.resources.responses.responses", + "Responses.stream", + responses_stream(handler), + ) wrap_function_wrapper( "openai.resources.responses.responses", "AsyncResponses.create", async_responses_create(handler), ) + wrap_function_wrapper( + "openai.resources.responses.responses", + "AsyncResponses.stream", + async_responses_stream(handler), + ) def _uninstrument(self, **kwargs): import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415 @@ -190,8 +202,9 @@ def _uninstrument(self, **kwargs): responses_module = _get_responses_module() if responses_module is not None: unwrap(responses_module.Responses, "create") - if hasattr(responses_module, "AsyncResponses"): - unwrap(responses_module.AsyncResponses, "create") + unwrap(responses_module.Responses, "stream") + unwrap(responses_module.AsyncResponses, "create") + unwrap(responses_module.AsyncResponses, "stream") def _get_responses_module(): diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py index 9f4f3ee0..3eb4c443 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch_responses.py @@ -3,6 +3,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Union, cast + from opentelemetry.util.genai.handler import TelemetryHandler from .response_extractors import ( @@ -12,18 +14,84 @@ set_invocation_response_attributes, ) from .response_wrappers import ( + AsyncResponseStreamManagerWrapper, AsyncResponseStreamWrapper, + ResponseStreamManagerWrapper, ResponseStreamWrapper, + responses_stream_context, ) -from .utils import is_streaming + +if TYPE_CHECKING: + from openai import AsyncStream as OpenAIAsyncStream + from openai import Stream as OpenAIStream + from openai.lib.streaming.responses._responses import ( # pylint: disable=no-name-in-module + AsyncResponseStream, + AsyncResponseStreamManager, + ResponseStream, + ResponseStreamManager, + ) + from openai.resources.responses.responses import AsyncResponses, Responses + from openai.types.responses import ( # pylint: disable=no-name-in-module + ParsedResponse, + Response, + ) + +try: + from openai import AsyncStream as _OpenAIAsyncStream + from openai import Stream as _OpenAIStream + from openai.lib.streaming.responses._responses import ( # pylint: disable=no-name-in-module + AsyncResponseStream as _AsyncResponseStream, + ) + from openai.lib.streaming.responses._responses import ( # pylint: disable=no-name-in-module + ResponseStream as _ResponseStream, + ) +except ImportError: + _AsyncResponseStream = None + _OpenAIAsyncStream = None + _OpenAIStream = None + _ResponseStream = None + +ResponseResult = Union["ParsedResponse[Any]", "Response"] +ResponseStreamResult = Union["OpenAIStream[Any]", "ResponseStream[Any]"] +AsyncResponseStreamResult = Union[ + "OpenAIAsyncStream[Any]", "AsyncResponseStream[Any]" +] -def responses_create(handler: TelemetryHandler): - """Wrap the `create` method of the `Responses` class to trace it.""" +def responses_create( + handler: TelemetryHandler, +) -> Callable[ + ..., + Union[ + ResponseResult, + ResponseStreamResult, + ResponseStreamWrapper[Any], + ], +]: + """Wrap ``Responses.create`` to trace Responses API calls. + + Traces :meth:`openai.resources.responses.responses.Responses.create`. + OpenAI SDK source: + https://github.com/openai/openai-python/blob/main/src/openai/resources/responses/responses.py#L914 + """ capture_content = handler.should_capture_content() - def traced_method(wrapped, instance, args, kwargs): + def traced_method( + wrapped: Callable[..., Union[ResponseResult, ResponseStreamResult]], + instance: "Responses", + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Union[ + ResponseResult, + ResponseStreamResult, + ResponseStreamWrapper[Any], + ]: + stream_context = responses_stream_context.get() + if stream_context is not None: + result = wrapped(*args, **kwargs) + return _get_response_stream_result(result) + params = extract_params(**kwargs) invocation = handler.inference( **get_inference_creation_kwargs(params, instance) @@ -34,15 +102,23 @@ def traced_method(wrapped, instance, args, kwargs): result = wrapped(*args, **kwargs) parsed_result = _get_response_stream_result(result) - if is_streaming(kwargs): + if ( + _ResponseStream is not None + and isinstance(parsed_result, _ResponseStream) + ) or ( + _OpenAIStream is not None + and isinstance(parsed_result, _OpenAIStream) + ): return ResponseStreamWrapper( - parsed_result, + cast("ResponseStreamResult", parsed_result), invocation, capture_content, ) set_invocation_response_attributes( - invocation, parsed_result, capture_content + invocation, + cast("ResponseResult", parsed_result), + capture_content, ) invocation.stop() return result @@ -50,17 +126,53 @@ def traced_method(wrapped, instance, args, kwargs): invocation.fail(error) raise - return traced_method + return cast( + 'Callable[..., Union["ResponseResult", "ResponseStreamResult", ResponseStreamWrapper[Any]]]', + traced_method, + ) + +def async_responses_create( + handler: TelemetryHandler, +) -> Callable[ + ..., + Awaitable[ + Union[ + ResponseResult, + AsyncResponseStreamResult, + AsyncResponseStreamWrapper[Any], + ] + ], +]: + """Wrap ``AsyncResponses.create`` to trace async Responses API calls. -def async_responses_create(handler: TelemetryHandler): - """Wrap the `create` method of the `AsyncResponses` class to trace it.""" + Traces :meth:`openai.resources.responses.responses.AsyncResponses.create`. + OpenAI SDK source: + https://github.com/openai/openai-python/blob/main/src/openai/resources/responses/responses.py#L2661 + """ capture_content = handler.should_capture_content() - async def traced_method(wrapped, instance, args, kwargs): + async def traced_method( + wrapped: Callable[ + ..., + Awaitable[Union[ResponseResult, AsyncResponseStreamResult]], + ], + instance: "AsyncResponses", + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Union[ + ResponseResult, + AsyncResponseStreamResult, + AsyncResponseStreamWrapper[Any], + ]: + stream_context = responses_stream_context.get() + if stream_context is not None: + result = await wrapped(*args, **kwargs) + return _get_response_stream_result(result) + params = extract_params(**kwargs) - invocation = handler.start_inference( + invocation = handler.inference( **get_inference_creation_kwargs(params, instance) ) apply_request_attributes(invocation, params, capture_content) @@ -69,15 +181,23 @@ async def traced_method(wrapped, instance, args, kwargs): result = await wrapped(*args, **kwargs) parsed_result = _get_response_stream_result(result) - if is_streaming(kwargs): + if ( + _AsyncResponseStream is not None + and isinstance(parsed_result, _AsyncResponseStream) + ) or ( + _OpenAIAsyncStream is not None + and isinstance(parsed_result, _OpenAIAsyncStream) + ): return AsyncResponseStreamWrapper( - parsed_result, + cast("AsyncResponseStreamResult", parsed_result), invocation, capture_content, ) set_invocation_response_attributes( - invocation, parsed_result, capture_content + invocation, + cast("ResponseResult", parsed_result), + capture_content, ) invocation.stop() return result @@ -85,7 +205,84 @@ async def traced_method(wrapped, instance, args, kwargs): invocation.fail(error) raise - return traced_method + return cast( + 'Callable[..., Awaitable[Union["ResponseResult", "AsyncResponseStreamResult", AsyncResponseStreamWrapper[Any]]]]', + traced_method, + ) + + +def responses_stream( + handler: TelemetryHandler, +) -> Callable[..., ResponseStreamManagerWrapper[Any]]: + """Wrap ``Responses.stream`` to trace sync stream manager calls. + + Traces :meth:`openai.resources.responses.responses.Responses.stream`. + OpenAI SDK source: + https://github.com/openai/openai-python/blob/main/src/openai/resources/responses/responses.py#L1062 + """ + + capture_content = handler.should_capture_content() + + def traced_method( + wrapped: Callable[..., "ResponseStreamManager[Any]"], + instance: "Responses", + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> ResponseStreamManagerWrapper[Any]: + def invocation_factory() -> Any: + params = extract_params(**kwargs) + invocation = handler.inference( + **get_inference_creation_kwargs(params, instance) + ) + apply_request_attributes(invocation, params, capture_content) + return invocation + + return ResponseStreamManagerWrapper( + wrapped(*args, **kwargs), + invocation_factory, + capture_content, + ) + + return cast( + "Callable[..., ResponseStreamManagerWrapper[Any]]", traced_method + ) + + +def async_responses_stream( + handler: TelemetryHandler, +) -> Callable[..., AsyncResponseStreamManagerWrapper[Any]]: + """Wrap ``AsyncResponses.stream`` to trace async stream manager calls. + + Traces :meth:`openai.resources.responses.responses.AsyncResponses.stream`. + OpenAI SDK source: + https://github.com/openai/openai-python/blob/main/src/openai/resources/responses/responses.py#L2809 + """ + + capture_content = handler.should_capture_content() + + def traced_method( + wrapped: Callable[..., "AsyncResponseStreamManager[Any]"], + instance: "AsyncResponses", + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> AsyncResponseStreamManagerWrapper[Any]: + def invocation_factory() -> Any: + params = extract_params(**kwargs) + invocation = handler.inference( + **get_inference_creation_kwargs(params, instance) + ) + apply_request_attributes(invocation, params, capture_content) + return invocation + + return AsyncResponseStreamManagerWrapper( + wrapped(*args, **kwargs), + invocation_factory, + capture_content, + ) + + return cast( + "Callable[..., AsyncResponseStreamManagerWrapper[Any]]", traced_method + ) def _get_response_stream_result(result): diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_wrappers.py index 019e8223..8a3f0f11 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/response_wrappers.py @@ -5,7 +5,7 @@ from __future__ import annotations -from contextlib import AsyncExitStack, ExitStack +from contextvars import ContextVar from types import TracebackType from typing import TYPE_CHECKING, Callable, Generic, TypeVar @@ -41,6 +41,28 @@ TextFormatT = TypeVar("TextFormatT") ResponseT = TypeVar("ResponseT") +ResponsesStreamContext = tuple["GenAIInvocation", bool] + +responses_stream_context: ContextVar[ResponsesStreamContext | None] = ( + ContextVar("responses_stream_context", default=None) +) + + +def _set_responses_stream_context( + invocation: "GenAIInvocation", capture_content: bool +): + """Mark the current ``Responses.stream`` manager entry. + + OpenAI's ``Responses.stream`` manager enters by calling the patched + ``Responses.create(..., stream=True)`` method internally. This context + marker lets the inner ``create`` wrapper return the SDK stream without + creating a second telemetry invocation; the outer stream manager wrapper + owns the span lifecycle. + + :returns: The ``ContextVar`` reset handle for restoring the previous + context after the SDK manager has entered. + """ + return responses_stream_context.set((invocation, capture_content)) def _set_response_attributes( @@ -203,6 +225,9 @@ def __init__( capture_content: bool, ): SyncStreamWrapper.__init__(self, stream) + # Marks streams already wrapped by the inner Responses.create path so + # Responses.stream manager entry can return them without wrapping twice. + self._self_is_response_stream_wrapper = True _ResponseStreamMixin.__init__(self, invocation, capture_content) @property @@ -226,19 +251,34 @@ class ResponseStreamManagerWrapper(Generic[TextFormatT]): def __init__( self, manager: "ResponseStreamManager[TextFormatT]", - invocation, + invocation_factory: Callable[[], "GenAIInvocation"], capture_content: bool, ): self._manager = manager - self._invocation = invocation + self._invocation_factory = invocation_factory + self._invocation: "GenAIInvocation | None" = None self._capture_content = capture_content self._stream_wrapper: ResponseStreamWrapper[TextFormatT] | None = None def __enter__(self) -> ResponseStreamWrapper[TextFormatT]: - stream = self._manager.__enter__() + invocation = self._invocation_factory() + self._invocation = invocation + stream_context_reset = _set_responses_stream_context( + invocation, self._capture_content + ) + try: + stream = self._manager.__enter__() + except Exception as error: + invocation.fail(error) + raise + finally: + responses_stream_context.reset(stream_context_reset) + if getattr(stream, "_self_is_response_stream_wrapper", False): + self._stream_wrapper = stream + return stream self._stream_wrapper = ResponseStreamWrapper( stream, - self._invocation, + invocation, self._capture_content, ) return self._stream_wrapper @@ -248,22 +288,25 @@ def __exit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: - suppressed = False + ) -> bool | None: stream_wrapper = self._stream_wrapper self._stream_wrapper = None - with ExitStack() as cleanup: - if stream_wrapper is not None: - - def finalize_stream_wrapper() -> None: - if suppressed: - stream_wrapper.__exit__(None, None, None) - else: - stream_wrapper.__exit__(exc_type, exc_val, exc_tb) - - cleanup.callback(finalize_stream_wrapper) + try: suppressed = self._manager.__exit__(exc_type, exc_val, exc_tb) - return suppressed + except Exception as error: + if stream_wrapper is not None: + stream_wrapper.__exit__( + type(error), error, error.__traceback__ + ) + elif self._invocation is not None: + self._invocation.fail(error) + raise + if stream_wrapper is not None: + if suppressed: + stream_wrapper.__exit__(None, None, None) + else: + stream_wrapper.__exit__(exc_type, exc_val, exc_tb) + return suppressed def parse(self) -> "ResponseStreamManagerWrapper[TextFormatT]": raise NotImplementedError( @@ -292,6 +335,9 @@ def __init__( capture_content: bool, ): AsyncStreamWrapper.__init__(self, stream) + # Marks streams already wrapped by the inner AsyncResponses.create path + # so AsyncResponses.stream manager entry avoids wrapping twice. + self._self_is_response_stream_wrapper = True _ResponseStreamMixin.__init__(self, invocation, capture_content) async def __aenter__(self) -> "AsyncResponseStreamWrapper[TextFormatT]": @@ -344,21 +390,36 @@ class AsyncResponseStreamManagerWrapper(Generic[TextFormatT]): def __init__( self, manager: "AsyncResponseStreamManager[TextFormatT]", - invocation, + invocation_factory: Callable[[], "GenAIInvocation"], capture_content: bool, ): self._manager = manager - self._invocation = invocation + self._invocation_factory = invocation_factory + self._invocation: "GenAIInvocation | None" = None self._capture_content = capture_content self._stream_wrapper: ( AsyncResponseStreamWrapper[TextFormatT] | None ) = None async def __aenter__(self) -> AsyncResponseStreamWrapper[TextFormatT]: - stream = await self._manager.__aenter__() + invocation = self._invocation_factory() + self._invocation = invocation + stream_context_reset = _set_responses_stream_context( + invocation, self._capture_content + ) + try: + stream = await self._manager.__aenter__() + except Exception as error: + invocation.fail(error) + raise + finally: + responses_stream_context.reset(stream_context_reset) + if getattr(stream, "_self_is_response_stream_wrapper", False): + self._stream_wrapper = stream + return stream self._stream_wrapper = AsyncResponseStreamWrapper( stream, - self._invocation, + invocation, self._capture_content, ) return self._stream_wrapper @@ -368,26 +429,27 @@ async def __aexit__( exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: - suppressed = False + ) -> bool | None: stream_wrapper = self._stream_wrapper self._stream_wrapper = None - async with AsyncExitStack() as cleanup: - if stream_wrapper is not None: - - async def finalize_stream_wrapper() -> None: - if suppressed: - await stream_wrapper.__aexit__(None, None, None) - else: - await stream_wrapper.__aexit__( - exc_type, exc_val, exc_tb - ) - - cleanup.push_async_callback(finalize_stream_wrapper) + try: suppressed = await self._manager.__aexit__( exc_type, exc_val, exc_tb ) - return suppressed + except Exception as error: + if stream_wrapper is not None: + await stream_wrapper.__aexit__( + type(error), error, error.__traceback__ + ) + elif self._invocation is not None: + self._invocation.fail(error) + raise + if stream_wrapper is not None: + if suppressed: + await stream_wrapper.__aexit__(None, None, None) + else: + await stream_wrapper.__aexit__(exc_type, exc_val, exc_tb) + return suppressed def parse(self) -> "AsyncResponseStreamManagerWrapper[TextFormatT]": raise NotImplementedError( diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/responses_stream_conformance.yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/responses_stream_conformance.yaml new file mode 100644 index 00000000..d0e8816b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/responses_stream_conformance.yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_048385de343c399f006a2e1ac3c6e881a0b3e7d86be0a3aa51","object":"response","created_at":1781406403,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_048385de343c399f006a2e1ac3c6e881a0b3e7d86be0a3aa51","object":"response","created_at":1781406403,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"obfuscation":"gBrLMx2P9OMd","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"obfuscation":"4Ho2vUDEh14HC","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"obfuscation":"zfcaC9b1zJVA9Z","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"obfuscation":"1FQO0u7pPmZ","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"obfuscation":"oa5qFp78eHQ12jE","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_048385de343c399f006a2e1ac3c6e881a0b3e7d86be0a3aa51","object":"response","created_at":1781406403,"status":"completed","background":false,"completed_at":1781406404,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_048385de343c399f006a2e1ac461c481a0a58babcab9df7ade","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b61ee66c31e5e2-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 03:06:43 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '160' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_6c9a5a50109f48ca978a1ac6e6c788ce + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_captures_content[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_captures_content[content_mode0].yaml new file mode 100644 index 00000000..458949c3 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_captures_content[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - AsyncOpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_0c3a5222f7e0f181006a2e19227ba881a2a5033eb2812d1bac","object":"response","created_at":1781405986,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_0c3a5222f7e0f181006a2e19227ba881a2a5033eb2812d1bac","object":"response","created_at":1781405986,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"obfuscation":"zraeXRFHlxab","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"obfuscation":"AZzDRVKLb3wKz","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"obfuscation":"4RM7QhQZDpmjRv","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"obfuscation":"UDOYsksJr91","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"obfuscation":"llav5xEgX44bSED","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_0c3a5222f7e0f181006a2e19227ba881a2a5033eb2812d1bac","object":"response","created_at":1781405986,"status":"completed","background":false,"completed_at":1781405986,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_0c3a5222f7e0f181006a2e1922ecf881a2888e9761e8d8ba26","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b614b74a1c1906-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:46 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '235' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_8f0fe0f8eb2a4751af48849e1cfa12cc + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_until_done[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_until_done[content_mode0].yaml new file mode 100644 index 00000000..d6534e96 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_until_done[content_mode0].yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "service_tier": "default", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '137' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - AsyncOpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_09dd7746310fbbeb006a2e19235cb4819d89c8fa3575582a4a","object":"response","created_at":1781405987,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_09dd7746310fbbeb006a2e19235cb4819d89c8fa3575582a4a","object":"response","created_at":1781405987,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"obfuscation":"VEIx1erAauZi","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"obfuscation":"C2Cf1MNMHewNe","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"obfuscation":"Yf1gvOXlqYANtD","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"obfuscation":"inJMS3zPg98","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"obfuscation":"1hFXEmYqhUkLtuX","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_09dd7746310fbbeb006a2e19235cb4819d89c8fa3575582a4a","object":"response","created_at":1781405987,"status":"completed","background":false,"completed_at":1781405988,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_09dd7746310fbbeb006a2e19248af0819d847dd8fd820780fe","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b614bc9bb926df-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:47 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '134' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_45072f14690b4675844dcec7b29850ca + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_user_exception[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_user_exception[content_mode0].yaml new file mode 100644 index 00000000..87d37e30 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_async_responses_stream_user_exception[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - AsyncOpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - async:asyncio + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_0e84228666c8e919006a2e1924fe60819d9573b04fe131a2fb","object":"response","created_at":1781405989,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_0e84228666c8e919006a2e1924fe60819d9573b04fe131a2fb","object":"response","created_at":1781405989,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"obfuscation":"8NEf3PdbUfoi","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"obfuscation":"rGdZ2fOjKjPmT","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"obfuscation":"pf0E8BP7radCD6","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"obfuscation":"stkIwYTvMM8","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"obfuscation":"N14Vx6hofoFKiHS","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_0e84228666c8e919006a2e1924fe60819d9573b04fe131a2fb","object":"response","created_at":1781405989,"status":"completed","background":false,"completed_at":1781405989,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_0e84228666c8e919006a2e19255d78819dab8c41d0db15d997","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b614c6eac0ff90-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:49 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '188' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_b20ee4f919fe411fbc84ef688021b7b6 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_captures_content[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_captures_content[content_mode0].yaml new file mode 100644 index 00000000..00427369 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_captures_content[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_07ca2df9e5d13dc2006a2e19064f40819f9e75f01a5a436052","object":"response","created_at":1781405958,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_07ca2df9e5d13dc2006a2e19064f40819f9e75f01a5a436052","object":"response","created_at":1781405958,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"obfuscation":"yB9DkO2QuP7c","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"obfuscation":"m8irG0PdQzufG","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"obfuscation":"YO2LnIqx16CP2n","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"obfuscation":"MkNK5SQhyWo","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"obfuscation":"k6vbHcUw0I4nGhu","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_07ca2df9e5d13dc2006a2e19064f40819f9e75f01a5a436052","object":"response","created_at":1781405958,"status":"completed","background":false,"completed_at":1781405958,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_07ca2df9e5d13dc2006a2e1906adb4819fba490feb3c851473","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b6140728f48acc-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:18 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '125' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_2a2c1cbae8cf46cb8364f242481e5027 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_until_done[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_until_done[content_mode0].yaml new file mode 100644 index 00000000..eae5a700 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_until_done[content_mode0].yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "service_tier": "default", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '137' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_046a977ee055660e006a2e19072aec819191ec15f6cfba4c8a","object":"response","created_at":1781405959,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_046a977ee055660e006a2e19072aec819191ec15f6cfba4c8a","object":"response","created_at":1781405959,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"obfuscation":"ys4bSkmgI1B3","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"obfuscation":"mq9YUMT5XZCzr","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"obfuscation":"TKltgFURqJOFJm","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"obfuscation":"uZvet8BMaqQ","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"obfuscation":"r0usjDkQmXoND60","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_046a977ee055660e006a2e19072aec819191ec15f6cfba4c8a","object":"response","created_at":1781405959,"status":"completed","background":false,"completed_at":1781405959,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_046a977ee055660e006a2e1907cd208191b153e79ebb4a5296","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b6140c7f2e0c23-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:19 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '329' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_a470087cf0d64b8b95e34d02b88f543e + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_user_exception[content_mode0].yaml b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_user_exception[content_mode0].yaml new file mode 100644 index 00000000..efc91224 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/cassettes/test_responses_stream_user_exception[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '112' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 2.41.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 2.41.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","response":{"id":"resp_0191dd7e73268599006a2e19082a8c819cbb1398ee7964e84d","object":"response","created_at":1781405960,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_0191dd7e73268599006a2e19082a8c819cbb1398ee7964e84d","object":"response","created_at":1781405960,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + event: response.content_part.added + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"obfuscation":"HF7jfvS3q136","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"obfuscation":"VI5EMPOeUwRz7","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"obfuscation":"Hpye6vTG73Uszt","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"obfuscation":"BCfOA3Ga3xI","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"obfuscation":"G9NnpJDF2JXkBZI","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_0191dd7e73268599006a2e19082a8c819cbb1398ee7964e84d","object":"response","created_at":1781405960,"status":"completed","background":false,"completed_at":1781405960,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","moderation":null,"output":[{"id":"msg_0191dd7e73268599006a2e19088c58819c9c4458b8e6331980","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"context":null,"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} + + headers: + Access-Control-Expose-Headers: + - CF-Ray + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - a0b61412cf02adca-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 14 Jun 2026 02:59:20 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + openai-organization: test_openai_org_id + openai-processing-ms: + - '129' + openai-project: test_openai_project_id + openai-version: + - '2020-10-01' + set-cookie: test_set_cookie + x-request-id: + - req_178ac6e347ec4150bb792a6d31d4730f + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/conformance/responses_stream.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/conformance/responses_stream.py new file mode 100644 index 00000000..c6813c41 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/conformance/responses_stream.py @@ -0,0 +1,53 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Conformance scenario: OpenAI Responses API stream helper.""" + +from __future__ import annotations + +from typing import Any + +from openai import OpenAI + +from opentelemetry.instrumentation.genai.openai import OpenAIInstrumentor +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test_util_genai.conformance import Scenario +from opentelemetry.test_util_genai.instrumentor import instrument + +DEFAULT_MODEL = "gpt-4o-mini" +SYSTEM_INSTRUCTIONS = "You are a helpful assistant." +USER_PROMPT = "Say this is a test" + + +class ResponsesStreamScenario(Scenario): + expected_spans = ("chat",) + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + ) + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + OpenAIInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + semconv="gen_ai_latest_experimental", + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette("responses_stream_conformance.yaml"): + with OpenAI().responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_PROMPT, + ) as stream: + stream.until_done() diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_async_responses.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_async_responses.py index 86d41ae1..4409048a 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_async_responses.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_async_responses.py @@ -14,6 +14,9 @@ ) from opentelemetry.instrumentation.genai.openai import OpenAIInstrumentor +from opentelemetry.instrumentation.genai.openai.response_wrappers import ( + AsyncResponseStreamManagerWrapper, +) from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) @@ -73,6 +76,13 @@ def _skip_if_not_latest(): + """Skip Responses API tests outside the latest experimental semconv path. + + Responses instrumentation is implemented with the GenAI latest + experimental semantic conventions only. The regular test matrix can still + exercise older or non-experimental semconv paths, so those runs should not + assert telemetry this instrumentation does not emit. + """ if not is_experimental_mode(): pytest.skip( "Responses create instrumentation only supports the latest experimental semconv path" @@ -407,6 +417,114 @@ async def test_async_responses_create_streaming( ) +@pytest.mark.asyncio() +async def test_async_responses_stream_connection_error( + span_exporter, instrument_no_content +): + _skip_if_not_latest() + + client = AsyncOpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + async with client.responses.stream( + model=DEFAULT_MODEL, + input="Hello", + timeout=0.1, + ): + pass + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_stream_captures_content( + span_exporter, + log_exporter, + async_openai_client, + instrument_with_content, +): + _skip_if_not_latest() + + manager = async_openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) + assert isinstance(manager, AsyncResponseStreamManagerWrapper) + async with manager as stream: + response = await _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_stream_until_done( + span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + + async with async_openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + service_tier="default", + ) as stream: + response = await stream.get_final_response() + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_stream_user_exception( + span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + + with pytest.raises(ValueError, match="User raised exception"): + async with async_openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) as stream: + async for _ in stream: + raise ValueError("User raised exception") + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" + + @pytest.mark.asyncio() async def test_async_responses_create_streaming_aggregates_cache_tokens( span_exporter, async_openai_client, instrument_no_content, vcr diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py index 675d6cfd..37a7c8f6 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_conformance.py @@ -26,6 +26,7 @@ from .conformance.embedding import EmbeddingScenario from .conformance.inference import InferenceScenario from .conformance.responses_conversation import ResponsesConversationScenario +from .conformance.responses_stream import ResponsesStreamScenario from .conformance.tool_calling import ToolCallingScenario @@ -41,6 +42,7 @@ ), ToolCallingScenario(), ResponsesConversationScenario(), + ResponsesStreamScenario(), ], ids=lambda s: type(s).__name__, ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_wrappers.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_wrappers.py index c46ca2ba..b85da731 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_wrappers.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_response_wrappers.py @@ -14,13 +14,18 @@ class _FakeManager: - def __init__(self, stream, suppressed=False, exit_error=None): + def __init__( + self, stream, suppressed=False, enter_error=None, exit_error=None + ): self._stream = stream self._suppressed = suppressed + self._enter_error = enter_error self._exit_error = exit_error self.exit_args = None def __enter__(self): + if self._enter_error is not None: + raise self._enter_error return self._stream def __exit__(self, exc_type, exc_val, exc_tb): @@ -31,13 +36,18 @@ def __exit__(self, exc_type, exc_val, exc_tb): class _FakeAsyncManager: - def __init__(self, stream, suppressed=False, exit_error=None): + def __init__( + self, stream, suppressed=False, enter_error=None, exit_error=None + ): self._stream = stream self._suppressed = suppressed + self._enter_error = enter_error self._exit_error = exit_error self.exit_args = None async def __aenter__(self): + if self._enter_error is not None: + raise self._enter_error return self._stream async def __aexit__(self, exc_type, exc_val, exc_tb): @@ -63,7 +73,7 @@ def _make_wrapper(manager): ) return ResponseStreamManagerWrapper( manager=manager, - invocation=invocation, + invocation_factory=lambda: invocation, capture_content=False, ) @@ -90,7 +100,7 @@ def _make_async_manager_wrapper(manager): ) return AsyncResponseStreamManagerWrapper( manager=manager, - invocation=invocation, + invocation_factory=lambda: invocation, capture_content=False, ) @@ -206,6 +216,27 @@ def test_manager_exit_forwards_exception_to_stream_wrapper(): assert wrapper._stream_wrapper is None +def test_manager_enter_failure_fails_invocation_and_reraises(): + error = RuntimeError("enter failure") + manager = _FakeManager(stream=SimpleNamespace(), enter_error=error) + failures = [] + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=failures.append, + ) + wrapper = ResponseStreamManagerWrapper( + manager=manager, + invocation_factory=lambda: invocation, + capture_content=False, + ) + + with pytest.raises(RuntimeError, match="enter failure"): + wrapper.__enter__() + + assert failures == [error] + + def test_manager_exit_uses_none_exception_when_manager_suppresses(): manager = _FakeManager(stream=SimpleNamespace(), suppressed=True) wrapper = _make_wrapper(manager) @@ -235,7 +266,8 @@ def test_manager_exit_still_finalizes_stream_wrapper_when_manager_raises(): wrapper.__exit__(ValueError, error, None) assert manager.exit_args == (ValueError, error, None) - assert stream_wrapper.exit_args == (ValueError, error, None) + assert stream_wrapper.exit_args[:2] == (RuntimeError, manager_error) + assert stream_wrapper.exit_args[2] is not None assert wrapper._stream_wrapper is None @@ -281,6 +313,28 @@ async def test_async_manager_enter_constructs_async_stream_wrapper(): assert wrapper._stream_wrapper is result +@pytest.mark.asyncio +async def test_async_manager_enter_failure_fails_invocation_and_reraises(): + error = RuntimeError("enter failure") + manager = _FakeAsyncManager(stream=SimpleNamespace(), enter_error=error) + failures = [] + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=failures.append, + ) + wrapper = AsyncResponseStreamManagerWrapper( + manager=manager, + invocation_factory=lambda: invocation, + capture_content=False, + ) + + with pytest.raises(RuntimeError, match="enter failure"): + await wrapper.__aenter__() + + assert failures == [error] + + @pytest.mark.asyncio async def test_async_manager_exit_uses_none_exception_when_manager_suppresses(): manager = _FakeAsyncManager(stream=SimpleNamespace(), suppressed=True) @@ -312,7 +366,8 @@ async def test_async_manager_exit_still_finalizes_stream_wrapper_when_manager_ra await wrapper.__aexit__(ValueError, error, None) assert manager.exit_args == (ValueError, error, None) - assert stream_wrapper.exit_args == (ValueError, error, None) + assert stream_wrapper.exit_args[:2] == (RuntimeError, manager_error) + assert stream_wrapper.exit_args[2] is not None assert wrapper._stream_wrapper is None diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_responses.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_responses.py index 07c0e2ec..d28ed973 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_responses.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_responses.py @@ -8,6 +8,9 @@ from openai import APIConnectionError, BadRequestError, NotFoundError, OpenAI from opentelemetry.instrumentation.genai.openai import OpenAIInstrumentor +from opentelemetry.instrumentation.genai.openai.response_wrappers import ( + ResponseStreamManagerWrapper, +) from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) @@ -67,6 +70,13 @@ def _skip_if_not_latest(): + """Skip Responses API tests outside the latest experimental semconv path. + + Responses instrumentation is implemented with the GenAI latest + experimental semantic conventions only. The regular test matrix can still + exercise older or non-experimental semconv paths, so those runs should not + assert telemetry this instrumentation does not emit. + """ if not is_experimental_mode(): pytest.skip( "Responses create instrumentation only supports the latest experimental semconv path" @@ -403,6 +413,122 @@ def test_responses_create_streaming( ) +def test_responses_stream_returns_wrapped_manager( + openai_client, instrument_no_content +): + _skip_if_not_latest() + + manager = openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) + + assert isinstance(manager, ResponseStreamManagerWrapper) + + +def test_responses_stream_connection_error( + span_exporter, instrument_no_content +): + _skip_if_not_latest() + + client = OpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + with client.responses.stream( + model=DEFAULT_MODEL, + input="Hello", + timeout=0.1, + ): + pass + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.vcr() +def test_responses_stream_captures_content( + span_exporter, + log_exporter, + openai_client, + instrument_with_content, +): + _skip_if_not_latest() + + with openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) as stream: + response = _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.vcr() +def test_responses_stream_until_done( + span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + + with openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + service_tier="default", + ) as stream: + response = stream.get_final_response() + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.vcr() +def test_responses_stream_user_exception( + span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + + with pytest.raises(ValueError, match="User raised exception"): + with openai_client.responses.stream( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) as stream: + for _ in stream: + raise ValueError("User raised exception") + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" + + @pytest.mark.vcr() def test_responses_create_streaming_aggregates_cache_tokens( request, span_exporter, openai_client, instrument_no_content