diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md index 734171f6ab..daf8182038 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Add support for OpenAI Responses API instrumentation + ([#4166](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4166)) - Fix `StreamWrapper` missing `.headers` and other attributes when using `with_raw_response` streaming ([#4113](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4113)) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst index 1cd3a51b07..f10aa10f0a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/README.rst @@ -82,6 +82,23 @@ Make sure to configure OpenTelemetry tracing, logging, and events to capture all input="Generate vector embeddings for this text" ) + # Responses API example + response = client.responses.create( + model="gpt-4o-mini", + input="Write a short poem on OpenTelemetry.", + ) + + # Responses streaming example + with client.responses.stream( + model="gpt-4o-mini", + input="Write a short poem on OpenTelemetry.", + background=True, + ) as stream: + for event in stream: + if event.type == "response.completed": + response = event.response + break + Enabling message content ************************* @@ -109,4 +126,3 @@ References * `OpenTelemetry OpenAI Instrumentation `_ * `OpenTelemetry Project `_ * `OpenTelemetry Python Examples `_ - diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/pyproject.toml index 6190dd2175..1da3355b94 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/pyproject.toml @@ -27,7 +27,8 @@ classifiers = [ dependencies = [ "opentelemetry-api ~= 1.37", "opentelemetry-instrumentation ~= 0.58b0", - "opentelemetry-semantic-conventions ~= 0.58b0" + "opentelemetry-semantic-conventions ~= 0.58b0", + "opentelemetry-util-genai >= 0.2b0, <0.3b0", ] [project.optional-dependencies] diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py index 4bb06574ba..90f9cb235f 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/__init__.py @@ -52,6 +52,7 @@ from opentelemetry.metrics import get_meter from opentelemetry.semconv.schemas import Schemas from opentelemetry.trace import get_tracer +from opentelemetry.util.genai.handler import TelemetryHandler from .instruments import Instruments from .patch import ( @@ -60,6 +61,7 @@ chat_completions_create, embeddings_create, ) +from .patch_responses import responses_create class OpenAIInstrumentor(BaseInstrumentor): @@ -94,12 +96,13 @@ def _instrument(self, **kwargs): ) instruments = Instruments(self._meter) + capture_content = is_content_enabled() wrap_function_wrapper( module="openai.resources.chat.completions", name="Completions.create", wrapper=chat_completions_create( - tracer, logger, instruments, is_content_enabled() + tracer, logger, instruments, capture_content ), ) @@ -107,7 +110,7 @@ def _instrument(self, **kwargs): module="openai.resources.chat.completions", name="AsyncCompletions.create", wrapper=async_chat_completions_create( - tracer, logger, instruments, is_content_enabled() + tracer, logger, instruments, capture_content ), ) @@ -115,19 +118,40 @@ def _instrument(self, **kwargs): wrap_function_wrapper( module="openai.resources.embeddings", name="Embeddings.create", - wrapper=embeddings_create( - tracer, instruments, is_content_enabled() - ), + wrapper=embeddings_create(tracer, instruments, capture_content), ) wrap_function_wrapper( module="openai.resources.embeddings", name="AsyncEmbeddings.create", wrapper=async_embeddings_create( - tracer, instruments, is_content_enabled() + tracer, instruments, capture_content ), ) + # Responses API is only available in openai>=1.66.0 + # https://github.com/openai/openai-python/blob/main/CHANGELOG.md#1660-2025-03-11 + try: + if TelemetryHandler is None: + raise ModuleNotFoundError( + "opentelemetry.util.genai.handler is unavailable" + ) + + handler = TelemetryHandler( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + logger_provider=logger_provider, + ) + + wrap_function_wrapper( + module="openai.resources.responses.responses", + name="Responses.create", + wrapper=responses_create(handler, capture_content), + ) + except (AttributeError, ModuleNotFoundError): + # Responses API or TelemetryHandler not available + pass + def _uninstrument(self, **kwargs): import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415 @@ -135,3 +159,11 @@ def _uninstrument(self, **kwargs): unwrap(openai.resources.chat.completions.AsyncCompletions, "create") unwrap(openai.resources.embeddings.Embeddings, "create") unwrap(openai.resources.embeddings.AsyncEmbeddings, "create") + + # Responses API is only available in openai>=1.66.0 + # https://github.com/openai/openai-python/blob/main/CHANGELOG.md#1660-2025-03-11 + try: + unwrap(openai.resources.responses.responses.Responses, "create") + except (AttributeError, ModuleNotFoundError): + # Responses API not available in this version of openai + pass diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py index 1543d1ab79..5e63c3a7a5 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +# pylint: disable=too-many-lines from timeit import default_timer from typing import Any, Optional @@ -23,14 +24,12 @@ from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) -from opentelemetry.semconv._incubating.attributes import ( - server_attributes as ServerAttributes, -) from opentelemetry.trace import Span, SpanKind, Tracer from opentelemetry.trace.propagation import set_span_in_context from .instruments import Instruments from .utils import ( + _record_metrics, choice_to_event, get_llm_request_attributes, handle_span_exception, @@ -283,83 +282,6 @@ def _get_embeddings_span_name(span_attributes): return f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}" -def _record_metrics( - instruments: Instruments, - duration: float, - result, - request_attributes: dict, - error_type: Optional[str], - operation_name: str, -): - common_attributes = { - GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name, - GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value, - GenAIAttributes.GEN_AI_REQUEST_MODEL: request_attributes[ - GenAIAttributes.GEN_AI_REQUEST_MODEL - ], - } - - if "gen_ai.embeddings.dimension.count" in request_attributes: - common_attributes["gen_ai.embeddings.dimension.count"] = ( - request_attributes["gen_ai.embeddings.dimension.count"] - ) - - if error_type: - common_attributes["error.type"] = error_type - - if result and getattr(result, "model", None): - common_attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] = result.model - - if result and getattr(result, "service_tier", None): - common_attributes[ - GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER - ] = result.service_tier - - if result and getattr(result, "system_fingerprint", None): - common_attributes["gen_ai.openai.response.system_fingerprint"] = ( - result.system_fingerprint - ) - - if ServerAttributes.SERVER_ADDRESS in request_attributes: - common_attributes[ServerAttributes.SERVER_ADDRESS] = ( - request_attributes[ServerAttributes.SERVER_ADDRESS] - ) - - if ServerAttributes.SERVER_PORT in request_attributes: - common_attributes[ServerAttributes.SERVER_PORT] = request_attributes[ - ServerAttributes.SERVER_PORT - ] - - instruments.operation_duration_histogram.record( - duration, - attributes=common_attributes, - ) - - if result and getattr(result, "usage", None): - # Always record input tokens - input_attributes = { - **common_attributes, - GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.INPUT.value, - } - instruments.token_usage_histogram.record( - result.usage.prompt_tokens, - attributes=input_attributes, - ) - - # For embeddings, don't record output tokens as all tokens are input tokens - if ( - operation_name - != GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value - ): - output_attributes = { - **common_attributes, - GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value, - } - instruments.token_usage_histogram.record( - result.usage.completion_tokens, attributes=output_attributes - ) - - def _set_response_attributes( span, result, logger: Logger, capture_content: bool ): diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch_responses.py new file mode 100644 index 0000000000..c9e2910876 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch_responses.py @@ -0,0 +1,122 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING + +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.util.genai.types import Error, LLMInvocation + +from .response_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + OPENAI, + _extract_input_messages, + _extract_output_type, + _extract_system_instruction, + _set_invocation_response_attributes, +) +from .response_wrappers import ResponseStreamWrapper +from .utils import get_llm_request_attributes, is_streaming + +if TYPE_CHECKING: + from opentelemetry.util.genai.handler import TelemetryHandler + +__all__ = [ + "responses_create", + "_set_invocation_response_attributes", + "GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS", + "GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS", +] + +# --------------------------------------------------------------------------- +# Patch functions +# --------------------------------------------------------------------------- + + +def responses_create( + handler: "TelemetryHandler", + capture_content: bool, +): + """Wrap the `create` method of the `Responses` class to trace it.""" + # https://github.com/openai/openai-python/blob/dc68b90655912886bd7a6c7787f96005452ebfc9/src/openai/resources/responses/responses.py#L828 + + def traced_method(wrapped, instance, args, kwargs): + if Error is None or LLMInvocation is None: + raise ModuleNotFoundError( + "opentelemetry.util.genai.types is unavailable" + ) + + operation_name = GenAIAttributes.GenAiOperationNameValues.CHAT.value + span_attributes = get_llm_request_attributes( + kwargs, + instance, + operation_name, + ) + output_type = _extract_output_type(kwargs) + if output_type: + span_attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = output_type + request_model = str( + span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) + or "unknown" + ) + streaming = is_streaming(kwargs) + + invocation = handler.start_llm( + LLMInvocation( + request_model=request_model, + operation_name=operation_name, + provider=OPENAI, + input_messages=_extract_input_messages(kwargs) + if capture_content + else [], + system_instruction=_extract_system_instruction(kwargs) + if capture_content + else [], + attributes=span_attributes.copy(), + metric_attributes={ + GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name + }, + ) + ) + + try: + result = wrapped(*args, **kwargs) + if hasattr(result, "parse"): + parsed_result = result.parse() + else: + parsed_result = result + + if streaming: + return ResponseStreamWrapper( + parsed_result, + handler, + invocation, + capture_content, + ) + + _set_invocation_response_attributes( + invocation, parsed_result, capture_content + ) + handler.stop_llm(invocation) + return result + + except Exception as error: + handler.fail_llm( + invocation, Error(message=str(error), type=type(error)) + ) + raise + + return traced_method diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_extractors.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_extractors.py new file mode 100644 index 0000000000..40ec2758e6 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_extractors.py @@ -0,0 +1,256 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Mapping, Optional + +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.util.genai.types import ( + InputMessage, + LLMInvocation, + OutputMessage, + Text, +) + +OPENAI = GenAIAttributes.GenAiSystemValues.OPENAI.value +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" +GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = ( + "gen_ai.usage.cache_creation.input_tokens" +) + + +def _extract_system_instruction(kwargs: dict): + """Extract system instruction from the ``instructions`` parameter.""" + if Text is None: + return [] + instructions = kwargs.get("instructions") + if instructions is None: + return [] + if isinstance(instructions, str): + return [Text(content=instructions)] + return [] + + +def _extract_input_messages(kwargs: dict): + """Extract input messages from Responses API kwargs.""" + if InputMessage is None or Text is None: + return [] + raw_input = kwargs.get("input") + if raw_input is None: + return [] + + if isinstance(raw_input, str): + return [InputMessage(role="user", parts=[Text(content=raw_input)])] + + messages = [] + if isinstance(raw_input, list): + for item in raw_input: + role = getattr(item, "role", None) or ( + item.get("role") if isinstance(item, dict) else None + ) + if not role: + continue + content = getattr(item, "content", None) or ( + item.get("content") if isinstance(item, dict) else None + ) + if isinstance(content, str): + messages.append( + InputMessage(role=role, parts=[Text(content=content)]) + ) + elif isinstance(content, list): + parts = [] + for part in content: + text = getattr(part, "text", None) or ( + part.get("text") if isinstance(part, dict) else None + ) + if text: + parts.append(Text(content=text)) + if parts: + messages.append(InputMessage(role=role, parts=parts)) + return messages + + +def _extract_output_messages(result: Any): + """Extract output messages from a Responses API result.""" + if OutputMessage is None or Text is None: + return [] + if result is None: + return [] + + output_items = getattr(result, "output", None) + if not output_items: + return [] + + messages = [] + for item in output_items: + if getattr(item, "type", None) != "message": + continue + + role = getattr(item, "role", "assistant") + finish_reason = _finish_reason_from_status( + getattr(item, "status", None) + ) + parts = _extract_output_parts(getattr(item, "content", []), Text) + + messages.append( + OutputMessage(role=role, parts=parts, finish_reason=finish_reason) + ) + + return messages + + +def _finish_reason_from_status(status): + return "stop" if status == "completed" else (status or "stop") + + +def _extract_output_parts(content_blocks, text_type): + parts = [] + for block in content_blocks: + block_type = getattr(block, "type", None) + if block_type == "output_text": + text = getattr(block, "text", None) + if text: + parts.append(text_type(content=text)) + elif block_type == "refusal": + refusal = getattr(block, "refusal", None) + if refusal: + parts.append(text_type(content=refusal)) + return parts + + +def _extract_finish_reasons(result: Any) -> list[str]: + """Extract finish reasons from Responses API output items.""" + output_items = getattr(result, "output", None) + if not output_items: + return [] + + finish_reasons = [] + for item in output_items: + if getattr(item, "type", None) != "message": + continue + finish_reasons.append( + _finish_reason_from_status(getattr(item, "status", None)) + ) + return finish_reasons + + +def _extract_output_type(kwargs: dict) -> Optional[str]: + """Extract output type from Responses API request text.format.""" + text_config = kwargs.get("text") + if not isinstance(text_config, Mapping): + return None + + format_config = text_config.get("format") + if isinstance(format_config, Mapping): + format_type = format_config.get("type") + else: + format_type = None + + if format_type == "json_schema": + return "json" + return format_type + + +def _get_field(obj: Any, key: str): + if isinstance(obj, Mapping): + return obj.get(key) + return getattr(obj, key, None) + + +def _set_optional_attribute( + invocation: "LLMInvocation", + result: Any, + source_name: str, + target_name: str, +): + value = getattr(result, source_name, None) + if value is not None: + invocation.attributes[target_name] = value + + +def _set_invocation_usage_attributes(invocation: "LLMInvocation", usage: Any): + input_tokens = _get_field(usage, "input_tokens") + if input_tokens is None: + input_tokens = _get_field(usage, "prompt_tokens") + invocation.input_tokens = input_tokens + + output_tokens = _get_field(usage, "output_tokens") + if output_tokens is None: + output_tokens = _get_field(usage, "completion_tokens") + invocation.output_tokens = output_tokens + + input_token_details = _get_field(usage, "input_tokens_details") + if input_token_details is None: + input_token_details = _get_field(usage, "prompt_tokens_details") + + cache_read_tokens = _get_field(input_token_details, "cached_tokens") + if cache_read_tokens is not None: + invocation.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = ( + cache_read_tokens + ) + + cache_creation_tokens = _get_field( + input_token_details, "cache_creation_input_tokens" + ) + if cache_creation_tokens is not None: + invocation.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = ( + cache_creation_tokens + ) + + +def _set_invocation_response_attributes( + invocation: "LLMInvocation", + result: Any, + capture_content: bool, +): + if result is None: + return + + if getattr(result, "model", None) and ( + not invocation.request_model or invocation.request_model == "unknown" + ): + invocation.request_model = result.model + + if getattr(result, "model", None): + invocation.response_model_name = result.model + + if getattr(result, "id", None): + invocation.response_id = result.id + + _set_optional_attribute( + invocation, + result, + "service_tier", + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER, + ) + _set_optional_attribute( + invocation, + result, + "system_fingerprint", + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SYSTEM_FINGERPRINT, + ) + + usage = getattr(result, "usage", None) + if usage: + _set_invocation_usage_attributes(invocation, usage) + + finish_reasons = _extract_finish_reasons(result) + if finish_reasons: + invocation.finish_reasons = finish_reasons + + if capture_content: + output_messages = _extract_output_messages(result) + if output_messages: + invocation.output_messages = output_messages diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_wrappers.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_wrappers.py new file mode 100644 index 0000000000..fb51646dd9 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/response_wrappers.py @@ -0,0 +1,164 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import TYPE_CHECKING, Any + +from opentelemetry.util.genai.types import Error + +from .response_extractors import _set_invocation_response_attributes + +if TYPE_CHECKING: + from opentelemetry.util.genai.handler import TelemetryHandler + from opentelemetry.util.genai.types import LLMInvocation + + +class _ResponseProxy: + def __init__(self, response, finalize): + self._response = response + self._finalize = finalize + + def close(self): + try: + self._response.close() + finally: + self._finalize(None) + + def __getattr__(self, name): + return getattr(self._response, name) + + +class ResponseStreamWrapper: + """Wrapper for OpenAI Responses API streams using TelemetryHandler.""" + + def __init__( + self, + stream: Any, + handler: "TelemetryHandler", + invocation: "LLMInvocation", + capture_content: bool, + ): + self.stream = stream + self.handler = handler + self.invocation = invocation + self._capture_content = capture_content + self._finalized = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + if exc_type is not None: + self._fail( + str(exc_val), type(exc_val) if exc_val else Exception + ) + finally: + self.close() + return False + + def close(self): + if hasattr(self.stream, "close"): + self.stream.close() + self._stop(None) + + def __iter__(self): + return self + + def __next__(self): + try: + event = next(self.stream) + self.process_event(event) + return event + except StopIteration: + self._stop(None) + raise + except Exception as error: + self._fail(str(error), type(error)) + raise + + def get_final_response(self): + if not hasattr(self.stream, "get_final_response"): + raise AttributeError("get_final_response is not available") + self.until_done() + return self.stream.get_final_response() + + def until_done(self): + for _ in self: + pass + return self + + def parse(self): + """Called when using with_raw_response with stream=True.""" + return self + + def __getattr__(self, name): + return getattr(self.stream, name) + + @property + def response(self): + response = getattr(self.stream, "response", None) + if response is None: + return None + return _ResponseProxy(response, lambda *_: self._stop(None)) + + def _stop(self, result: Any): + if self._finalized: + return + _set_invocation_response_attributes( + self.invocation, result, self._capture_content + ) + self.handler.stop_llm(self.invocation) + self._finalized = True + + def _fail(self, message: str, error_type: type[BaseException]): + if self._finalized: + return + if Error is None: + raise ModuleNotFoundError( + "opentelemetry.util.genai.types is unavailable" + ) + + self.handler.fail_llm( + self.invocation, Error(message=message, type=error_type) + ) + self._finalized = True + + def process_event(self, event): + event_type = getattr(event, "type", None) + response = getattr(event, "response", None) + + if response and ( + not self.invocation.request_model + or self.invocation.request_model == "unknown" + ): + model = getattr(response, "model", None) + if model: + self.invocation.request_model = model + + if event_type == "response.completed": + self._stop(response) + return + + if event_type in {"response.failed", "response.incomplete"}: + _set_invocation_response_attributes( + self.invocation, response, self._capture_content + ) + self._fail(event_type, RuntimeError) + return + + if event_type == "error": + error_type = getattr(event, "code", None) or "response.error" + message = getattr(event, "message", None) or error_type + self._fail(message, RuntimeError) + return diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py index 6e3ebad2ed..ff52d120de 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py @@ -14,8 +14,8 @@ from __future__ import annotations -from os import environ -from typing import Mapping +import os +from typing import TYPE_CHECKING, Mapping, Optional from urllib.parse import urlparse from httpx import URL @@ -32,18 +32,25 @@ error_attributes as ErrorAttributes, ) from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util.genai.utils import should_capture_content OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = ( "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" ) +if TYPE_CHECKING: + from .instruments import Instruments + def is_content_enabled() -> bool: - capture_content = environ.get( + capture_content = os.environ.get( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false" ) + legacy_enabled = capture_content.lower() == "true" - return capture_content.lower() == "true" + if should_capture_content is None: + return legacy_enabled + return legacy_enabled or should_capture_content() def extract_tool_calls(item, capture_content): @@ -202,8 +209,10 @@ def get_llm_request_attributes( GenAIAttributes.GEN_AI_REQUEST_MODEL: kwargs.get("model"), } - # Add chat-specific attributes only for chat operations - if operation_name == GenAIAttributes.GenAiOperationNameValues.CHAT.value: + # Add chat-like attributes for chat operations + if operation_name in ( + GenAIAttributes.GenAiOperationNameValues.CHAT.value, + ): attributes.update( { GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: kwargs.get( @@ -213,7 +222,8 @@ def get_llm_request_attributes( or kwargs.get("top_p"), GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get( "max_tokens" - ), + ) + or kwargs.get("max_output_tokens"), GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get( "presence_penalty" ), @@ -284,6 +294,94 @@ def get_llm_request_attributes( return {k: v for k, v in attributes.items() if value_is_set(v)} +def _record_metrics( # pylint: disable=too-many-branches + instruments: Instruments, + duration: float, + result, + request_attributes: dict, + error_type: Optional[str], + operation_name: str, +): + common_attributes = { + GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name, + GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value, + } + if request_model := request_attributes.get( + GenAIAttributes.GEN_AI_REQUEST_MODEL + ): + common_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = request_model + + if "gen_ai.embeddings.dimension.count" in request_attributes: + common_attributes["gen_ai.embeddings.dimension.count"] = ( + request_attributes["gen_ai.embeddings.dimension.count"] + ) + + if error_type: + common_attributes["error.type"] = error_type + + if result and getattr(result, "model", None): + common_attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] = result.model + + if result and getattr(result, "service_tier", None): + common_attributes[ + GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER + ] = result.service_tier + + if result and getattr(result, "system_fingerprint", None): + common_attributes["gen_ai.openai.response.system_fingerprint"] = ( + result.system_fingerprint + ) + + if ServerAttributes.SERVER_ADDRESS in request_attributes: + common_attributes[ServerAttributes.SERVER_ADDRESS] = ( + request_attributes[ServerAttributes.SERVER_ADDRESS] + ) + + if ServerAttributes.SERVER_PORT in request_attributes: + common_attributes[ServerAttributes.SERVER_PORT] = request_attributes[ + ServerAttributes.SERVER_PORT + ] + + instruments.operation_duration_histogram.record( + duration, + attributes=common_attributes, + ) + + if result and getattr(result, "usage", None): + usage = result.usage + input_tokens = getattr(usage, "prompt_tokens", None) + if input_tokens is None: + input_tokens = getattr(usage, "input_tokens", None) + output_tokens = getattr(usage, "completion_tokens", None) + if output_tokens is None: + output_tokens = getattr(usage, "output_tokens", None) + + # Always record input tokens + input_attributes = { + **common_attributes, + GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.INPUT.value, + } + if input_tokens is not None: + instruments.token_usage_histogram.record( + input_tokens, + attributes=input_attributes, + ) + + # For embeddings, don't record output tokens as all tokens are input tokens + if ( + operation_name + != GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value + ): + output_attributes = { + **common_attributes, + GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value, + } + if output_tokens is not None: + instruments.token_usage_histogram.record( + output_tokens, attributes=output_attributes + ) + + def handle_span_exception(span, error): span.set_status(Status(StatusCode.ERROR, str(error))) if span.is_recording(): diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create.yaml new file mode 100644 index 0000000000..6aeee65254 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create.yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '72' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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: |- + { + "id": "resp_0ddadc2b6c99631e0069922bc4c8b881a1bb297b80eab5f2da", + "object": "response", + "created_at": 1771187140, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187141, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_0ddadc2b6c99631e0069922bc5bb7481a1bd2cf765e86f0324", + "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": null, + "reasoning": { + "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": 12, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9ce7892a1edf6e28-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:25:41 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1542' + openai-organization: test_openai_org_id + openai-processing-ms: + - '1117' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=EqzSyI6frjYjpe4KHsZwfOCC3u_Kwly3Sd3J5Zds8zI-1771187140.1743493-1.0.1.1-OmexI2NvBMMEXBpmchsnZHZYoTN49ITf88EX6vLlvPVe2omNldWnkBVDxPsaqC43T_5jYbgXJ8TWJOreyavfvAJJt5s6AT_6hiOPPN9GC8ZuS6nii1Buh8VcBBlsCibo; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:41 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '199969' + x-ratelimit-reset-requests: + - 8.64s + x-ratelimit-reset-tokens: + - 9ms + x-request-id: + - req_599094c097964d0d8609a82a7d64b624 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content.yaml new file mode 100644 index 0000000000..df7fec79a0 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content.yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '72' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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: |- + { + "id": "resp_0a692bbc4a9a738e0069922bd6dff081a0bf3b31e4574406b3", + "object": "response", + "created_at": 1771187158, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187159, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_0a692bbc4a9a738e0069922bd7639481a0a23616dd17799eee", + "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": null, + "reasoning": { + "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": 12, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9ce7899ecfb1c623-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:25:59 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1542' + openai-organization: test_openai_org_id + openai-processing-ms: + - '680' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=Lbl9DmPj4kySvF1eCafY9T.72od8gDdGIcR6nMvlxSM-1771187158.8475974-1.0.1.1-DH7BPaBw8nKQK0B5lfBSYvhHSTWUreFW2DrfErLPfwM_IpreByWYdyGv7NbNayCYFpXQ3tOrv3ZRWlSwxCsKNLWnRqgoAsZxV4U97QOuR93RJVlDpWEaPPf4b8RtxUe3; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:59 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9996' + x-ratelimit-remaining-tokens: + - '199969' + x-ratelimit-reset-requests: + - 34.205s + x-ratelimit-reset-tokens: + - 9ms + x-request-id: + - req_f41b7b1fa8bc4e4b8bdd55a243075b36 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_system_instruction.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_system_instruction.yaml new file mode 100644 index 0000000000..784b053ee8 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_system_instruction.yaml @@ -0,0 +1,173 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '120' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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: |- + { + "id": "resp_0233b438169682b40069922bd952e0819d81cec5cf35c84e81", + "object": "response", + "created_at": 1771187161, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187161, + "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", + "output": [ + { + "id": "msg_0233b438169682b40069922bd9a7e0819d86b88f3926660e99", + "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": null, + "reasoning": { + "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": {} + } + headers: + CF-RAY: + - 9ce789ae1dd261a4-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:26:01 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1568' + openai-organization: test_openai_org_id + openai-processing-ms: + - '536' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=3eCCvSommNHuhREd5dxYrLGSytg4GALQnYmyVXMfgXE-1771187161.2950814-1.0.1.1-o2mJ1h4Tt6HcRvGyo42zkOXIleax_EI9XYyJKq.yPxrIc3VV5GFCrCVrMo.Xg..KubEy67gunDANwHKllJOuXIQVmYDm4YUdYmfunqeuyrNpchY_XJ6FOOFhqtLddF6z; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:56:01 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9993' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 57.872s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_9674b98f2ad540d2ad8f83ae6bb085d8 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_no_content_in_experimental_mode.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_no_content_in_experimental_mode.yaml new file mode 100644 index 0000000000..b2eb3a150f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_no_content_in_experimental_mode.yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '72' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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: |- + { + "id": "resp_0d18b4a505bb3e7a0069922bd88954819f884008a1e164c94c", + "object": "response", + "created_at": 1771187160, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187161, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_0d18b4a505bb3e7a0069922bd8e5b4819f8e1086a823f00419", + "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": null, + "reasoning": { + "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": 12, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9ce789a9281d60e6-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:26:01 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1542' + openai-organization: test_openai_org_id + openai-processing-ms: + - '589' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=n4.5mxMWU4m5YLF1PpbKFO7tw4IRw1nmtvQzxo2rcB4-1771187160.510076-1.0.1.1-7uGrj0QFRCtsERMlJtLfzLI0fK_HaShDinmhpr1cpsoGs6We1zdnkAQqZhM2JlFB4aWnVCOOzs34A1meNF8BQjBHhPjPoWHYpjiCTHRno4JdFNvyhzuIperX.ZQkqIeV; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:56:01 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9995' + x-ratelimit-remaining-tokens: + - '199969' + x-ratelimit-reset-requests: + - 41.391s + x-ratelimit-reset-tokens: + - 9ms + x-request-id: + - req_6c5f965d80654f80bed9e7b7c3cf1067 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve.yaml new file mode 100644 index 0000000000..c65299ca3c --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve.yaml @@ -0,0 +1,319 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": false + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '72' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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: |- + { + "id": "resp_03af00866b03fea30069922bce690c8197af8c9605c28116a1", + "object": "response", + "created_at": 1771187150, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187150, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_03af00866b03fea30069922bcede2c81979727cb46b656fd79", + "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": null, + "reasoning": { + "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": 12, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9ce78969dcd319c7-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:25:51 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1542' + openai-organization: test_openai_org_id + openai-processing-ms: + - '639' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=5VGMYEsZeS.ABFbzaCXfWVvbW3NKuqMPba5PaXoLGUA-1771187150.371446-1.0.1.1-oVgVIOIJiMxLBuCd55yeZ7VthiZFnQlBj2ns9QXws_fYVWTAeka0T81qJ7c15C4ptJrbZa2cLE6QtHr_pFlO3LHvsS8lUVYJP34h0QACa0ODIbQ1sInYNh0OZlBRgFwV; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:51 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9997' + x-ratelimit-remaining-tokens: + - '199969' + x-ratelimit-reset-requests: + - 25.39s + x-ratelimit-reset-tokens: + - 9ms + x-request-id: + - req_9425bf3f0f79463996939395328f939f + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + cookie: + - test_cookie + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: GET + uri: https://api.openai.com/v1/responses/resp_03af00866b03fea30069922bce690c8197af8c9605c28116a1 + response: + body: + string: |- + { + "id": "resp_03af00866b03fea30069922bce690c8197af8c9605c28116a1", + "object": "response", + "created_at": 1771187150, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1771187150, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_03af00866b03fea30069922bcede2c81979727cb46b656fd79", + "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": null, + "reasoning": { + "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": 12, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 6, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 18 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9ce7896f4c98de92-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Sun, 15 Feb 2026 20:25:51 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + content-length: + - '1542' + openai-organization: test_openai_org_id + openai-processing-ms: + - '80' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + x-request-id: + - req_5155e47a313f4615911460d831e27cc6 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve_stream_existing_response.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve_stream_existing_response.yaml new file mode 100644 index 0000000000..e319a3ccb7 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_retrieve_stream_existing_response.yaml @@ -0,0 +1,285 @@ +interactions: +- request: + body: |- + { + "background": true, + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '91' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","object":"response","created_at":1771187151,"status":"queued","background":true,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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.queued + data: {"type":"response.queued","response":{"id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","object":"response","created_at":1771187151,"status":"queued","background":true,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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.in_progress + data: {"type":"response.in_progress","sequence_number":2,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","usage":null,"status":"in_progress","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187151,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"background":true,"completed_at":null}} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[],"role":"assistant","status":"in_progress","type":"message"},"output_index":0,"sequence_number":3} + + event: response.content_part.added + data: {"type":"response.content_part.added","item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","part":{"text":"","logprobs":[],"type":"output_text","annotations":[]},"content_index":0,"output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"This","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" is","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" a","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" test","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":".","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" How","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" can","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":11} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" I","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":12} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" assist","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":13} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" you","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":14} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" further","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":15} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"?","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":16} + + event: response.output_text.done + data: {"type":"response.output_text.done","text":"This is a test. How can I assist you further?","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":17} + + event: response.content_part.done + data: {"type":"response.content_part.done","item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","part":{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]},"content_index":0,"output_index":0,"sequence_number":18} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"},"output_index":0,"sequence_number":19} + + event: response.completed + data: {"type":"response.completed","sequence_number":20,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","usage":{"input_tokens":12,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0},"output_tokens":13,"total_tokens":25},"status":"completed","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187151,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"default","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"background":true,"completed_at":1771187153}} + + headers: + CF-RAY: + - 9ce78970efe10f3e-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:52 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '1126' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=mpzbaGS1KSEREmECIYifExDkv3qTFoZOseGXdjZSrQI-1771187151.5091498-1.0.1.1-hPZFu0axb6XudOZa3f5Dh_0QUIwlsoGtAUqgizvcsPuUaf9B5ufC1aIixdLTLrkhmAReszYvRqXy0f1d.AsBewSH3HhM22i80SPiJdhhELzJqcvDkWy1HLtI8kO.GQlw; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:52 GMT + x-request-id: + - req_1147ddbf62f4452291cbb827ca174e1e + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + cookie: + - test_cookie + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: GET + uri: https://api.openai.com/v1/responses/resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54?starting_after=0&stream=true + response: + body: + string: |+ + event: response.queued + data: {"type":"response.queued","sequence_number":1,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","usage":null,"status":"queued","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187151,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"background":true,"completed_at":null}} + + event: response.in_progress + data: {"type":"response.in_progress","sequence_number":2,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","usage":null,"status":"in_progress","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187151,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"background":true,"completed_at":null}} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[],"role":"assistant","status":"in_progress","type":"message"},"output_index":0,"sequence_number":3} + + event: response.content_part.added + data: {"type":"response.content_part.added","item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","part":{"text":"","logprobs":[],"type":"output_text","annotations":[]},"content_index":0,"output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"This","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" is","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" a","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" test","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":".","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" How","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" can","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":11} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" I","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":12} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" assist","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":13} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" you","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":14} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" further","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":15} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"?","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":16} + + event: response.output_text.done + data: {"type":"response.output_text.done","text":"This is a test. How can I assist you further?","logprobs":[],"item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content_index":0,"output_index":0,"sequence_number":17} + + event: response.content_part.done + data: {"type":"response.content_part.done","item_id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","part":{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]},"content_index":0,"output_index":0,"sequence_number":18} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"},"output_index":0,"sequence_number":19} + + event: response.completed + data: {"type":"response.completed","sequence_number":20,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_039be8aa35b2d1f30069922bcf8b04819db82d51010ae07a54","usage":{"input_tokens":12,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0},"output_tokens":13,"total_tokens":25},"status":"completed","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187151,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[{"id":"msg_039be8aa35b2d1f30069922bd19998819d801caf28bd4ac920","content":[{"text":"This is a test. How can I assist you further?","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"default","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"background":true,"completed_at":1771187153}} + + headers: + CF-RAY: + - 9ce78992ad67c475-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:57 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '317' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + x-request-id: + - req_42fa11cb44354271a2c09bc096ec10ed + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_captures_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_captures_content.yaml new file mode 100644 index 0000000000..c3417d2818 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_captures_content.yaml @@ -0,0 +1,126 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '71' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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_0385929b1a05c16b0069922bd7b2a881a2a79473a37f9f1864","object":"response","created_at":1771187159,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0385929b1a05c16b0069922bd7b2a881a2a79473a37f9f1864","object":"response","created_at":1771187159,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","logprobs":[],"obfuscation":"ExVRdLq1LHgA","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","logprobs":[],"obfuscation":"KZr63d8wBO3lQ","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","logprobs":[],"obfuscation":"P1y6AZReRdTUb8","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","logprobs":[],"obfuscation":"vkjDqdQhuUA","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","logprobs":[],"obfuscation":"EMX0XlHMAVzSTze","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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_0385929b1a05c16b0069922bd7b2a881a2a79473a37f9f1864","object":"response","created_at":1771187159,"status":"completed","background":false,"completed_at":1771187160,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0385929b1a05c16b0069922bd837a081a2901d0bf7eb088fde","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":null,"reasoning":{"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":12,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":18},"user":null,"metadata":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9ce789a3f862377d-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:59 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '41' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=qYxVKrWrZ9N1a0esR02WdIDekq4DeBv.e7rlf4ei8DY-1771187159.6724527-1.0.1.1-AQoBv_iuqoXmhxKBeVgT_AhKekwM_hrenQIZnV95ChQpNJsSiswSkjmrOrKWYj282k_SFJglHTIXJmtgOyXoUKU7RHmzinh_mZr7UxVXUzCsxsViuCt6wzWRss33oXA8; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:59 GMT + x-request-id: + - req_4c838c84665d45c5abc5a948f3eec262 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_existing_response.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_existing_response.yaml new file mode 100644 index 0000000000..f9d35003bd --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_existing_response.yaml @@ -0,0 +1,246 @@ +interactions: +- request: + body: |- + { + "background": true, + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '91' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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_0902a5af124240630069922bc771408192a122de125273821c","object":"response","created_at":1771187143,"status":"queued","background":true,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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.queued + data: {"type":"response.queued","response":{"id":"resp_0902a5af124240630069922bc771408192a122de125273821c","object":"response","created_at":1771187143,"status":"queued","background":true,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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.in_progress + data: {"type":"response.in_progress","sequence_number":2,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":null,"status":"in_progress","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":null}} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[],"role":"assistant","status":"in_progress","type":"message"},"output_index":0,"sequence_number":3} + + event: response.content_part.added + data: {"type":"response.content_part.added","item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"part":{"text":"","logprobs":[],"type":"output_text","annotations":[]},"output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"This","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" is","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" a","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" test","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":".","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":9} + + event: response.output_text.done + data: {"type":"response.output_text.done","text":"This is a test.","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":10} + + event: response.content_part.done + data: {"type":"response.content_part.done","item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"part":{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]},"output_index":0,"sequence_number":11} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"},"output_index":0,"sequence_number":12} + + event: response.completed + data: {"type":"response.completed","sequence_number":13,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":{"input_tokens":12,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0},"output_tokens":6,"total_tokens":18},"status":"completed","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"default","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":1771187145}} + + headers: + CF-RAY: + - 9ce7893e5983d954-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:44 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '825' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=wdqKFzmig00qL5iRqKdSV5B86Q4qwIxGAekZkL6MbIU-1771187143.4113734-1.0.1.1-FG980w2OZNpsIyCNY58BpMIlwNGYFung.RgZSVG1qRhLTzNkXeDGFRGVt87CIc3PD_AOao7uHZIjVYt90pKGHYHzAVMgqZ45QK6u3gEn0WmljmWATbHxQ_hikPcTxacB; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:44 GMT + x-request-id: + - req_16f6d65735b649ff886e473dcad3db8e + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.1 + X-Stainless-Runtime: + - CPython + X-Stainless-Runtime-Version: + - 3.12.12 + authorization: + - Bearer test_openai_api_key + cookie: + - test_cookie + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + method: GET + uri: https://api.openai.com/v1/responses/resp_0902a5af124240630069922bc771408192a122de125273821c?stream=true + response: + body: + string: |+ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":null,"status":"queued","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":null}} + + event: response.queued + data: {"type":"response.queued","sequence_number":1,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":null,"status":"queued","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":null}} + + event: response.in_progress + data: {"type":"response.in_progress","sequence_number":2,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":null,"status":"in_progress","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"auto","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":null}} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[],"role":"assistant","status":"in_progress","type":"message"},"output_index":0,"sequence_number":3} + + event: response.content_part.added + data: {"type":"response.content_part.added","item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"part":{"text":"","logprobs":[],"type":"output_text","annotations":[]},"output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":"This","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" is","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" a","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":" test","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","delta":".","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":9} + + event: response.output_text.done + data: {"type":"response.output_text.done","text":"This is a test.","logprobs":[],"item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"output_index":0,"sequence_number":10} + + event: response.content_part.done + data: {"type":"response.content_part.done","item_id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content_index":0,"part":{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]},"output_index":0,"sequence_number":11} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"},"output_index":0,"sequence_number":12} + + event: response.completed + data: {"type":"response.completed","sequence_number":13,"response":{"truncation":"disabled","tool_choice":"auto","id":"resp_0902a5af124240630069922bc771408192a122de125273821c","usage":{"input_tokens":12,"input_tokens_details":{"cached_tokens":0},"output_tokens_details":{"reasoning_tokens":0},"output_tokens":6,"total_tokens":18},"status":"completed","top_p":1.0,"temperature":1.0,"top_logprobs":0,"object":"response","created_at":1771187143,"prompt_cache_key":null,"text":{"verbosity":"medium","format":{"type":"text"}},"incomplete_details":null,"frequency_penalty":0.0,"previous_response_id":null,"model":"gpt-4o-mini-2024-07-18","safety_identifier":null,"metadata":{},"store":true,"output":[{"id":"msg_0902a5af124240630069922bc998188192a56c34bc519252a9","content":[{"text":"This is a test.","logprobs":[],"type":"output_text","annotations":[]}],"role":"assistant","status":"completed","type":"message"}],"parallel_tool_calls":true,"error":null,"instructions":null,"max_output_tokens":null,"service_tier":"default","max_tool_calls":null,"prompt_cache_retention":null,"tools":[],"user":null,"background":true,"presence_penalty":0.0,"reasoning":{"effort":null,"summary":null},"completed_at":1771187145}} + + headers: + CF-RAY: + - 9ce7895b9c838c8d-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:48 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '435' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + x-request-id: + - req_573eb07663324b1da146ed2ebdc51cd6 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_new_response.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_new_response.yaml new file mode 100644 index 0000000000..5eb7925500 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_new_response.yaml @@ -0,0 +1,126 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '71' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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_0ed89c7c7fe30b0f0069922bc62a788196baf96470de20bf1e","object":"response","created_at":1771187142,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0ed89c7c7fe30b0f0069922bc62a788196baf96470de20bf1e","object":"response","created_at":1771187142,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","logprobs":[],"obfuscation":"mP3OzBGcGwoN","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","logprobs":[],"obfuscation":"4oyXAkuroaPuJ","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","logprobs":[],"obfuscation":"CP2UMbr4diy5br","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","logprobs":[],"obfuscation":"gDnWyUINjvp","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","logprobs":[],"obfuscation":"MMCPwsJRyYHfWBw","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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_0ed89c7c7fe30b0f0069922bc62a788196baf96470de20bf1e","object":"response","created_at":1771187142,"status":"completed","background":false,"completed_at":1771187143,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0ed89c7c7fe30b0f0069922bc6fa5481969e5f6dfb46d7c872","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":null,"reasoning":{"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":12,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":18},"user":null,"metadata":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9ce789364873b1f3-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:25:42 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '67' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=gprbrDpkrav1W_jFKKXc.zxxSHTqomhuiS3uxx3AF1U-1771187142.126042-1.0.1.1-KqsoOqzgPHIsAlG2LgNktRyTgZOSngIzjbqTVcajVAapa3Oxqz342KE6dgKdIHi2d6zfu3Y47xT.tPEYFMEAmajdFqt.FzsZCknltAYnCTgvoH7so0HbWNmPq7gRoHCw; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:55:42 GMT + x-request-id: + - req_b564b82f23b64d0c9565ebabd1007055 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_no_content_in_experimental_mode.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_no_content_in_experimental_mode.yaml new file mode 100644 index 0000000000..fea07e3782 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_no_content_in_experimental_mode.yaml @@ -0,0 +1,126 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '71' + Content-Type: + - application/json + Host: + - api.openai.com + User-Agent: + - OpenAI/Python 1.109.1 + X-Stainless-Arch: + - arm64 + X-Stainless-Async: + - 'false' + X-Stainless-Lang: + - python + X-Stainless-OS: + - MacOS + X-Stainless-Package-Version: + - 1.109.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_0d0bf9ab3933c9150069922bda0e288195bbafeb5ce2ea8088","object":"response","created_at":1771187162,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0d0bf9ab3933c9150069922bda0e288195bbafeb5ce2ea8088","object":"response","created_at":1771187162,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":null,"reasoning":{"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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","logprobs":[],"obfuscation":"IyMsl8GsHkrL","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","logprobs":[],"obfuscation":"gyr754u1qsG2v","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","logprobs":[],"obfuscation":"C4yeYHqSnaRWlr","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","logprobs":[],"obfuscation":"Rn0pWLwVUtA","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","logprobs":[],"obfuscation":"ppTh2nPXTlzUNGU","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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_0d0bf9ab3933c9150069922bda0e288195bbafeb5ce2ea8088","object":"response","created_at":1771187162,"status":"completed","background":false,"completed_at":1771187162,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"msg_0d0bf9ab3933c9150069922bda71a08195b4df58b1e20fdc98","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":null,"reasoning":{"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":12,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":18},"user":null,"metadata":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9ce789b28811ff90-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Sun, 15 Feb 2026 20:26:02 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '60' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=svni1d7N3CcpY.MpZlrBKQn37WctprZzy83bIKNQlko-1771187162.0063605-1.0.1.1-csAUtbhSTxJhgnNgMj.9t.yPCxHiikbnbbtOlX73dpBAL75xAH.jOlTy9rnq7hG.IXzSq6kUlblOXHY_MTPfmNtr46EYs7YQU752tZfg8.tfAgwrUotYEMi92YWF35Zb; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sun, 15 Feb 2026 + 20:56:02 GMT + x-request-id: + - req_fba03afcdf48405db7eac80728b064f4 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py index af626d7990..16f9d76167 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py @@ -7,6 +7,10 @@ import yaml from openai import AsyncOpenAI, OpenAI +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.instrumentation.openai_v2.utils import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, @@ -170,6 +174,60 @@ def instrument_with_content_unsampled( instrumentor.uninstrument() +@pytest.fixture(scope="function") +def instrument_with_experimental_content( + tracer_provider, logger_provider, meter_provider +): + # Reset global state for experimental mode + _OpenTelemetrySemanticConventionStability._initialized = False + os.environ.update( + {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental"} + ) + os.environ.update( + {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_AND_EVENT"} + ) + + instrumentor = OpenAIInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + yield instrumentor + os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) + os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) + _OpenTelemetrySemanticConventionStability._initialized = False + instrumentor.uninstrument() + + +@pytest.fixture(scope="function") +def instrument_with_experimental_no_content( + tracer_provider, logger_provider, meter_provider +): + # Reset global state for experimental mode + _OpenTelemetrySemanticConventionStability._initialized = False + os.environ.update( + {OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental"} + ) + os.environ.update( + {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "NO_CONTENT"} + ) + + instrumentor = OpenAIInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + yield instrumentor + os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) + os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) + _OpenTelemetrySemanticConventionStability._initialized = False + instrumentor.uninstrument() + + class LiteralBlockScalar(str): """Formats the string as a literal block scalar, preserving whitespace and without interpreting escape characters""" diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt index 2e29be5222..d8be3b03c4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.latest.txt @@ -51,4 +51,5 @@ wrapt==1.16.0 # test with the latest version of opentelemetry-api, sdk, and semantic conventions -e opentelemetry-instrumentation +-e util/opentelemetry-util-genai -e instrumentation-genai/opentelemetry-instrumentation-openai-v2 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt index 979b7a5bae..00ba7b9ee4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/requirements.oldest.txt @@ -32,4 +32,5 @@ opentelemetry-api==1.37 # when updating, also update in pyproject.toml opentelemetry-sdk==1.37 # when updating, also update in pyproject.toml opentelemetry-semantic-conventions==0.58b0 # when updating, also update in pyproject.toml +-e util/opentelemetry-util-genai -e instrumentation-genai/opentelemetry-instrumentation-openai-v2 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py new file mode 100644 index 0000000000..f82c3de958 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -0,0 +1,403 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from types import SimpleNamespace + +import openai +import pytest +from packaging.version import Version + +from opentelemetry.instrumentation.openai_v2.patch_responses import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + _set_invocation_response_attributes, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.util.genai.types import LLMInvocation + +from .test_utils import assert_all_attributes + + +def _load_span_messages(span, attribute): + """Load and parse JSON message content from a span attribute.""" + value = span.attributes.get(attribute) + assert value is not None, f"Expected attribute {attribute} to be present" + assert isinstance(value, str), f"Expected {attribute} to be a JSON string" + parsed = json.loads(value) + assert isinstance(parsed, list), f"Expected {attribute} to be a JSON list" + return parsed + + +# The Responses API was introduced in openai>=1.66.0 +# https://github.com/openai/openai-python/blob/main/CHANGELOG.md#1660-2025-03-11 +OPENAI_VERSION = Version(openai.__version__) +RESPONSES_API_MIN_VERSION = Version("1.66.0") +skip_if_no_responses_api = pytest.mark.skipif( + OPENAI_VERSION < RESPONSES_API_MIN_VERSION, + reason=f"Responses API requires openai >= {RESPONSES_API_MIN_VERSION}, got {OPENAI_VERSION}", +) + + +@skip_if_no_responses_api +@pytest.mark.vcr() +def test_responses_create( + span_exporter, openai_client, instrument_with_content +): + response = openai_client.responses.create( + model="gpt-4o-mini", + input="Say this is a test", + stream=False, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + + input_tokens = response.usage.input_tokens if response.usage else None + output_tokens = response.usage.output_tokens if response.usage else None + + assert_all_attributes( + spans[0], + "gpt-4o-mini", + response.id, + response.model, + input_tokens, + output_tokens, + operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value, + response_service_tier=response.service_tier, + ) + + +@skip_if_no_responses_api +@pytest.mark.vcr() +def test_responses_stream_new_response( + span_exporter, openai_client, instrument_with_content +): + with openai_client.responses.stream( + model="gpt-4o-mini", + input="Say this is a test", + ) as stream: + final_response = None + for event in stream: + if event.type == "response.completed": + final_response = event.response + break + + assert final_response is not None + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + + input_tokens = ( + final_response.usage.input_tokens if final_response.usage else None + ) + output_tokens = ( + final_response.usage.output_tokens if final_response.usage else None + ) + + assert_all_attributes( + spans[0], + "gpt-4o-mini", + final_response.id, + final_response.model, + input_tokens, + output_tokens, + operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value, + response_service_tier=final_response.service_tier, + ) + + +@skip_if_no_responses_api +@pytest.mark.vcr() +def test_responses_stream_existing_response( + span_exporter, openai_client, instrument_with_content +): + response_id = None + starting_after = None + + with openai_client.responses.stream( + model="gpt-4o-mini", + input="Say this is a test", + background=True, + ) as stream: + for event in stream: + if event.type == "response.created": + response_id = event.response.id + starting_after = event.sequence_number + if response_id is not None and starting_after is not None: + break + + assert response_id is not None + assert starting_after is not None + span_count = len(span_exporter.get_finished_spans()) + + with openai_client.responses.stream( + response_id=response_id, + starting_after=starting_after, + ) as stream: + final_response = None + for event in stream: + if event.type == "response.completed": + final_response = event.response + break + + assert final_response is not None + + spans = span_exporter.get_finished_spans() + assert len(spans) == span_count + + +@skip_if_no_responses_api +@pytest.mark.vcr() +def test_responses_retrieve( + span_exporter, openai_client, instrument_with_content +): + create_response = openai_client.responses.create( + model="gpt-4o-mini", + input="Say this is a test", + stream=False, + ) + + span_count = len(span_exporter.get_finished_spans()) + + response = openai_client.responses.retrieve(create_response.id) + spans = span_exporter.get_finished_spans() + assert len(spans) == span_count + assert response.id == create_response.id + + +@skip_if_no_responses_api +@pytest.mark.vcr() +def test_responses_retrieve_stream_existing_response( + span_exporter, openai_client, instrument_with_content +): + response_id = None + starting_after = None + + with openai_client.responses.stream( + model="gpt-4o-mini", + input="Say this is a test", + background=True, + ) as stream: + for event in stream: + if event.type == "response.created": + response_id = event.response.id + starting_after = event.sequence_number + if response_id is not None and starting_after is not None: + break + + assert response_id is not None + assert starting_after is not None + span_count = len(span_exporter.get_finished_spans()) + + with openai_client.responses.retrieve( + response_id=response_id, + starting_after=starting_after, + stream=True, + ) as stream: + final_response = None + for event in stream: + if event.type == "response.completed": + final_response = event.response + break + + assert final_response is not None + + spans = span_exporter.get_finished_spans() + assert len(spans) == span_count + + +# ============================================================================= +# Content capture tests (experimental mode) +# ============================================================================= + + +@skip_if_no_responses_api +@pytest.mark.vcr("test_responses_create_captures_content.yaml") +def test_responses_create_captures_content( + span_exporter, openai_client, instrument_with_experimental_content +): + """Test that input and output content is captured in span attributes.""" + openai_client.responses.create( + model="gpt-4o-mini", + input="Say this is a test", + stream=False, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + input_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_INPUT_MESSAGES + ) + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + + # Input: string input should become a user message with text part + assert len(input_messages) == 1 + assert input_messages[0]["role"] == "user" + assert input_messages[0]["parts"][0]["type"] == "text" + assert "test" in input_messages[0]["parts"][0]["content"] + + # Output: should have an assistant message with text part + assert len(output_messages) >= 1 + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"][0]["type"] == "text" + assert len(output_messages[0]["parts"][0]["content"]) > 0 + + +@skip_if_no_responses_api +@pytest.mark.vcr("test_responses_stream_captures_content.yaml") +def test_responses_stream_captures_content( + span_exporter, openai_client, instrument_with_experimental_content +): + """Test that streaming responses capture content in span attributes.""" + with openai_client.responses.stream( + model="gpt-4o-mini", + input="Say this is a test", + ) as stream: + for event in stream: + if event.type == "response.completed": + break + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + input_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_INPUT_MESSAGES + ) + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + + assert input_messages[0]["role"] == "user" + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"] + assert output_messages[0]["parts"][0]["type"] == "text" + + +@skip_if_no_responses_api +@pytest.mark.vcr("test_responses_create_no_content_in_experimental_mode.yaml") +def test_responses_create_no_content_in_experimental_mode( + span_exporter, openai_client, instrument_with_experimental_no_content +): + """Test that NO_CONTENT mode does not capture messages in span attributes.""" + openai_client.responses.create( + model="gpt-4o-mini", + input="Say this is a test", + stream=False, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + # Content should NOT be in span attributes under NO_CONTENT + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + + # Basic span attributes should still be present + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini" + ) + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS in span.attributes + assert GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS in span.attributes + + +@skip_if_no_responses_api +@pytest.mark.vcr("test_responses_create_captures_system_instruction.yaml") +def test_responses_create_captures_system_instruction( + span_exporter, openai_client, instrument_with_experimental_content +): + """Test that system instructions are captured in span attributes.""" + openai_client.responses.create( + model="gpt-4o-mini", + input="Say this is a test", + instructions="You are a helpful assistant.", + stream=False, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + system_instructions = span.attributes.get( + GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS + ) + assert system_instructions is not None + parsed = json.loads(system_instructions) + assert isinstance(parsed, list) + assert len(parsed) >= 1 + assert parsed[0]["type"] == "text" + assert "helpful assistant" in parsed[0]["content"] + + +@skip_if_no_responses_api +@pytest.mark.vcr("test_responses_stream_no_content_in_experimental_mode.yaml") +def test_responses_stream_no_content_in_experimental_mode( + span_exporter, openai_client, instrument_with_experimental_no_content +): + """Test that streaming with NO_CONTENT mode does not capture messages.""" + with openai_client.responses.stream( + model="gpt-4o-mini", + input="Say this is a test", + ) as stream: + for event in stream: + if event.type == "response.completed": + break + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + # Content should NOT be in span attributes + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + + # Basic span attributes should still be present + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS in span.attributes + + +def test_set_invocation_response_attributes_sets_cache_token_attributes(): + invocation = LLMInvocation() + usage = SimpleNamespace( + input_tokens=12, + output_tokens=6, + input_tokens_details=SimpleNamespace( + cached_tokens=3, + cache_creation_input_tokens=2, + ), + ) + result = SimpleNamespace( + usage=usage, + output=[ + SimpleNamespace(type="message", status="completed"), + ], + ) + + _set_invocation_response_attributes( + invocation=invocation, + result=result, + capture_content=False, + ) + + assert invocation.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == 3 + assert invocation.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] == 2 diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 616d9ab471..3fb6417cee 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -111,6 +111,18 @@ def should_emit_event() -> bool: return False +def should_capture_content() -> bool: + """Return True when content conversion should be performed.""" + if not is_experimental_mode(): + return False + mode = get_content_capturing_mode() + if mode == ContentCapturingMode.NO_CONTENT: + return False + if mode == ContentCapturingMode.EVENT_ONLY and not should_emit_event(): + return False + return True + + class _GenAiJsonEncoder(json.JSONEncoder): def default(self, o: Any) -> Any: if isinstance(o, bytes): diff --git a/util/opentelemetry-util-genai/tests/test_events_options.py b/util/opentelemetry-util-genai/tests/test_events_options.py index 78476de880..f83fc9562e 100644 --- a/util/opentelemetry-util-genai/tests/test_events_options.py +++ b/util/opentelemetry-util-genai/tests/test_events_options.py @@ -25,6 +25,7 @@ OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.utils import ( + should_capture_content, should_emit_event, ) @@ -206,3 +207,45 @@ def test_should_emit_event_user_setting_overrides_default_for_no_content( ): # pylint: disable=no-self-use # User explicitly setting emit_event="true" should override the default (False for NO_CONTENT) assert should_emit_event() is True + + +class TestShouldCaptureContent(unittest.TestCase): + @patch_env_vars( + stability_mode="default", + content_capturing="SPAN_AND_EVENT", + emit_event="true", + ) + def test_should_capture_content_false_when_not_experimental( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="NO_CONTENT", + emit_event="true", + ) + def test_should_capture_content_false_when_no_content( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="EVENT_ONLY", + emit_event="false", + ) + def test_should_capture_content_false_when_event_only_but_event_disabled( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="SPAN_ONLY", + emit_event="", + ) + def test_should_capture_content_true_for_span_only( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is True diff --git a/uv.lock b/uv.lock index 6e0fca9fb9..fecdf10043 100644 --- a/uv.lock +++ b/uv.lock @@ -4403,6 +4403,7 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-genai" }, ] [package.optional-dependencies] @@ -4416,6 +4417,7 @@ requires-dist = [ { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" }, + { name = "opentelemetry-util-genai", editable = "util/opentelemetry-util-genai" }, ] provides-extras = ["instruments"]