From 799e80e790b6e4b21af7603a3ee455bbfb5ec676 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Sat, 11 Apr 2026 20:15:49 -0400 Subject: [PATCH 01/10] wip: first draft of openai responses instrumentation. --- .../instrumentation/openai_v2/__init__.py | 30 + .../openai_v2/patch_responses.py | 196 ++++ .../openai_v2/response_extractors.py | 235 ++++- .../openai_v2/response_wrappers.py | 10 +- ...ggregates_cache_tokens[content_mode0].yaml | 172 ++++ ...onses_create_api_error[content_mode0].yaml | 94 ++ ...responses_create_basic[content_mode0].yaml | 173 ++++ ...reate_captures_content[content_mode0].yaml | 178 ++++ ...ures_reasoning_content[content_mode0].yaml | 179 ++++ ...ures_tool_call_content[content_mode0].yaml | 213 +++++ ..._create_event_only_no_content_in_span.yaml | 173 ++++ ...tation_error_swallowed[content_mode0].yaml | 127 +++ ...ses_create_stop_reason[content_mode0].yaml | 172 ++++ ...ream_propagation_error[content_mode0].yaml | 127 +++ ...onses_create_streaming[content_mode0].yaml | 128 +++ ...ggregates_cache_tokens[content_mode0].yaml | 127 +++ ...aming_captures_content[content_mode0].yaml | 127 +++ ...tes_response_attribute[content_mode0].yaml | 142 +++ ...te_streaming_iteration[content_mode0].yaml | 142 +++ ...reaming_user_exception[content_mode0].yaml | 127 +++ ...ses_create_token_usage[content_mode0].yaml | 172 ++++ ...create_with_all_params[content_mode0].yaml | 181 ++++ ...te_with_content_shapes[content_mode0].yaml | 173 ++++ ...content_span_unsampled[content_mode0].yaml | 173 ++++ ...er_finalize_idempotent[content_mode0].yaml | 127 +++ .../tests/conftest.py | 26 + .../tests/test_async_responses.py | 857 ++++++++++++++++++ .../tests/test_response_extractors.py | 42 +- .../tests/test_responses.py | 798 ++++++++++++++++ .../tests/test_utils.py | 36 + 30 files changed, 5432 insertions(+), 25 deletions(-) create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch_responses.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stop_reason[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stream_propagation_error[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_aggregates_cache_tokens[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_captures_content[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_delegates_response_attribute[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_iteration[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_user_exception[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_token_usage[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_all_params[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_shapes[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_span_unsampled[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_wrapper_finalize_idempotent[content_mode0].yaml create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py create mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py 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 e959083751..82405e4498 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 @@ -40,6 +40,7 @@ --- """ +from importlib import import_module from typing import Collection from wrapt import wrap_function_wrapper @@ -70,6 +71,10 @@ chat_completions_create_v_old, embeddings_create, ) +from .patch_responses import ( + async_responses_create, + responses_create, +) class OpenAIInstrumentor(BaseInstrumentor): @@ -159,6 +164,20 @@ def _instrument(self, **kwargs): ), ) + responses_module = _get_responses_module() + if responses_module is not None and latest_experimental_enabled: + wrap_function_wrapper( + module="openai.resources.responses.responses", + name="Responses.create", + wrapper=responses_create(handler, content_mode), + ) + + wrap_function_wrapper( + module="openai.resources.responses.responses", + name="AsyncResponses.create", + wrapper=async_responses_create(handler, content_mode), + ) + def _uninstrument(self, **kwargs): import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415 @@ -166,3 +185,14 @@ def _uninstrument(self, **kwargs): unwrap(openai.resources.chat.completions.AsyncCompletions, "create") unwrap(openai.resources.embeddings.Embeddings, "create") unwrap(openai.resources.embeddings.AsyncEmbeddings, "create") + responses_module = _get_responses_module() + if responses_module is not None: + unwrap(responses_module.Responses, "create") + unwrap(responses_module.AsyncResponses, "create") + + +def _get_responses_module(): + try: + return import_module("openai.resources.responses.responses") + except ImportError: + return None 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..dc0905b33a --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch_responses.py @@ -0,0 +1,196 @@ +# 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 __future__ import annotations + +from typing import Optional + +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.util.genai.handler import TelemetryHandler +from opentelemetry.util.genai.types import ContentCapturingMode, Error + +from .instruments import Instruments +from .response_extractors import ( + _create_invocation as create_response_invocation, + _set_invocation_response_attributes, +) +from .response_wrappers import AsyncResponseStreamWrapper, ResponseStreamWrapper +from .utils import is_streaming + + +def responses_create( + handler: TelemetryHandler, + content_capturing_mode: ContentCapturingMode, +): + """Wrap the `create` method of the `Responses` class to trace it.""" + + capture_content = content_capturing_mode != ContentCapturingMode.NO_CONTENT + + def traced_method(wrapped, instance, args, kwargs): + invocation = handler.start_llm( + create_response_invocation( + kwargs, instance, capture_content=capture_content + ) + ) + + try: + result = wrapped(*args, **kwargs) + parsed_result = _get_response_stream_result(result) + + if is_streaming(kwargs): + 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(type=type(error), message=str(error)) + ) + raise + + return traced_method + + +def async_responses_create( + handler: TelemetryHandler, + content_capturing_mode: ContentCapturingMode, +): + """Wrap the `create` method of the `AsyncResponses` class to trace it.""" + + capture_content = content_capturing_mode != ContentCapturingMode.NO_CONTENT + + async def traced_method(wrapped, instance, args, kwargs): + invocation = handler.start_llm( + create_response_invocation( + kwargs, instance, capture_content=capture_content + ) + ) + + try: + result = await wrapped(*args, **kwargs) + parsed_result = _get_response_stream_result(result) + + if is_streaming(kwargs): + return AsyncResponseStreamWrapper( + 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(type=type(error), message=str(error)) + ) + raise + + return traced_method + + +def _get_response_stream_result(result): + if hasattr(result, "parse"): + return result.parse() + return result + + +def _record_metrics( + instruments: Instruments, + duration: float, + result, + request_attributes: dict, + error_type: Optional[str], +): + common_attributes = { + GenAIAttributes.GEN_AI_OPERATION_NAME: ( + GenAIAttributes.GenAiOperationNameValues.CHAT.value + ), + GenAIAttributes.GEN_AI_SYSTEM: ( + GenAIAttributes.GenAiSystemValues.OPENAI.value + ), + GenAIAttributes.GEN_AI_REQUEST_MODEL: request_attributes[ + GenAIAttributes.GEN_AI_REQUEST_MODEL + ], + } + + 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 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): + input_tokens = getattr(result.usage, "input_tokens", None) + output_tokens = getattr(result.usage, "output_tokens", None) + + if input_tokens is not None: + input_attributes = { + **common_attributes, + GenAIAttributes.GEN_AI_TOKEN_TYPE: ( + GenAIAttributes.GenAiTokenTypeValues.INPUT.value + ), + } + instruments.token_usage_histogram.record( + input_tokens, + attributes=input_attributes, + ) + + if output_tokens is not None: + output_attributes = { + **common_attributes, + GenAIAttributes.GEN_AI_TOKEN_TYPE: ( + GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value + ), + } + instruments.token_usage_histogram.record( + output_tokens, + attributes=output_attributes, + ) 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 index 2d5e8702b1..de244a3634 100644 --- 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 @@ -14,15 +14,29 @@ from __future__ import annotations +import json import logging from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, List, Optional, TypeVar, Union -from pydantic import BaseModel, Field, StrictInt, StrictStr, ValidationError +from pydantic import ( + BaseModel, + Field, + StrictFloat, + StrictInt, + StrictStr, + ValidationError, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) +from opentelemetry.semconv._incubating.attributes import ( + server_attributes as ServerAttributes, +) _PYDANTIC_V2 = hasattr(BaseModel, "model_validate") @@ -42,13 +56,21 @@ try: from opentelemetry.util.genai.types import ( InputMessage, + LLMInvocation, OutputMessage, + Reasoning, Text, + ToolCall, ) except ImportError: InputMessage = None + LLMInvocation = None OutputMessage = None + Reasoning = None Text = None + ToolCall = None + +from .utils import get_server_address_and_port, value_is_set GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = ( @@ -91,7 +113,12 @@ class _ResponseInputItemModel(_ExtractorModel): class _ResponsesRequestModel(_ExtractorModel): instructions: Optional[StrictStr] = None input: Optional[Union[StrictStr, List[_ResponseInputItemModel]]] = None + max_output_tokens: Optional[StrictInt] = None + model: Optional[StrictStr] = None + service_tier: Optional[StrictStr] = None + temperature: Optional[StrictFloat] = None text: Optional[_ResponseTextConfigModel] = None + top_p: Optional[StrictFloat] = None class _ResponseOutputContentModel(_ExtractorModel): @@ -104,7 +131,12 @@ class _ResponseOutputItemModel(_ExtractorModel): type: Optional[StrictStr] = None role: Optional[StrictStr] = None status: Optional[StrictStr] = None + id: Optional[StrictStr] = None + call_id: Optional[StrictStr] = None + name: Optional[StrictStr] = None + arguments: Optional[StrictStr] = None content: List[_ResponseOutputContentModel] = Field(default_factory=list) + summary: List[_ResponseOutputContentModel] = Field(default_factory=list) class _UsageDetailsModel(_ExtractorModel): @@ -264,6 +296,32 @@ def _extract_output_parts( return parts +def _parse_tool_call_arguments(arguments: str | None) -> object: + if arguments is None: + return None + + try: + return json.loads(arguments) + except (TypeError, ValueError): + return arguments + + +def _extract_reasoning_parts( + item: _ResponseOutputItemModel, +) -> list["Reasoning"]: + if Reasoning is None: + return [] + + parts: list[Reasoning] = [] + for block in item.summary: + if block.text is not None: + parts.append(Reasoning(content=block.text)) + for block in item.content: + if block.type == "reasoning_text" and block.text is not None: + parts.append(Reasoning(content=block.text)) + return parts + + def _finish_reason_from_status(status: str | None) -> str | None: # Responses API output items expose lifecycle statuses rather than finish # reasons. We map the normal terminal state to the GenAI "stop" reason, @@ -284,19 +342,57 @@ def _extract_output_messages_from_model( messages: list[OutputMessage] = [] for item in result.output: - if item.type != "message": - continue - finish_reason = _finish_reason_from_status(item.status) - if finish_reason is None: + if item.type == "message": + finish_reason = _finish_reason_from_status(item.status) + if finish_reason is None: + continue + + messages.append( + OutputMessage( + role=item.role if item.role is not None else "assistant", + parts=_extract_output_parts(item.content), + finish_reason=finish_reason, + ) + ) continue - messages.append( - OutputMessage( - role=item.role if item.role is not None else "assistant", - parts=_extract_output_parts(item.content), - finish_reason=finish_reason, + if item.type == "function_call": + if ToolCall is None or item.name is None: + continue + if item.status not in {"completed", "incomplete"}: + continue + + messages.append( + OutputMessage( + role="assistant", + parts=[ + ToolCall( + id=item.call_id if item.call_id else item.id, + name=item.name, + arguments=_parse_tool_call_arguments( + item.arguments + ), + ) + ], + finish_reason="tool_calls", + ) ) - ) + continue + + if item.type == "reasoning": + finish_reason = _finish_reason_from_status(item.status) + if finish_reason is None: + continue + + parts = _extract_reasoning_parts(item) + if parts: + messages.append( + OutputMessage( + role="assistant", + parts=parts, + finish_reason=finish_reason, + ) + ) return messages @@ -317,12 +413,19 @@ def _extract_finish_reasons_from_model( ) -> list[str]: finish_reasons: list[str] = [] for item in result.output: + if item.type == "function_call" and item.status in { + "completed", + "incomplete", + }: + finish_reasons.append("tool_calls") + continue + if item.type != "message": continue finish_reason = _finish_reason_from_status(item.status) if finish_reason is not None: finish_reasons.append(finish_reason) - return finish_reasons + return list(dict.fromkeys(finish_reasons)) def _extract_finish_reasons( @@ -347,6 +450,114 @@ def _extract_output_type(kwargs: Mapping[str, object]) -> str | None: return request.text.format.type +def _extract_request_service_tier( + kwargs: Mapping[str, object], +) -> str | None: + request = _validate_request_kwargs(kwargs) + if request is None: + return None + + service_tier = request.service_tier + if service_tier in (None, "auto"): + return None + + return service_tier + + +def _get_request_attributes( + kwargs: Mapping[str, object], + client_instance: object, + latest_experimental_enabled: bool, +) -> dict[str, object]: + request = _validate_request_kwargs(kwargs) + request_model = request.model if request is not None else None + attributes: dict[str, object] = { + GenAIAttributes.GEN_AI_OPERATION_NAME: ( + GenAIAttributes.GenAiOperationNameValues.CHAT.value + ), + GenAIAttributes.GEN_AI_REQUEST_MODEL: request_model, + } + + if latest_experimental_enabled: + attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] = ( + GenAIAttributes.GenAiProviderNameValues.OPENAI.value + ) + else: + attributes[GenAIAttributes.GEN_AI_SYSTEM] = ( + GenAIAttributes.GenAiSystemValues.OPENAI.value + ) + + output_type = _extract_output_type(kwargs) + if output_type is not None: + output_type_key = ( + GenAIAttributes.GEN_AI_OUTPUT_TYPE + if latest_experimental_enabled + else GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT + ) + attributes[output_type_key] = output_type + + service_tier = _extract_request_service_tier(kwargs) + if service_tier is not None: + service_tier_key = ( + OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER + if latest_experimental_enabled + else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER + ) + attributes[service_tier_key] = service_tier + + address, port = get_server_address_and_port(client_instance) + if address is not None: + attributes[ServerAttributes.SERVER_ADDRESS] = address + if port is not None: + attributes[ServerAttributes.SERVER_PORT] = port + + return {key: value for key, value in attributes.items() if value_is_set(value)} + + +def _create_invocation( + kwargs: Mapping[str, object], + client_instance: object, + capture_content: bool, +) -> "LLMInvocation": + if LLMInvocation is None: + raise RuntimeError("GenAI LLMInvocation type is unavailable") + + request = _validate_request_kwargs(kwargs) + request_model = request.model if request is not None else None + + invocation = LLMInvocation( + request_model=request_model, + provider=GenAIAttributes.GenAiProviderNameValues.OPENAI.value, + ) + + if request is not None: + invocation.temperature = request.temperature + invocation.top_p = request.top_p + invocation.max_tokens = request.max_output_tokens + + request_service_tier = _extract_request_service_tier(kwargs) + if request_service_tier is not None: + invocation.attributes[OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER] = ( + request_service_tier + ) + + output_type = _extract_output_type(kwargs) + if output_type is not None: + invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = ( + output_type + ) + + address, port = get_server_address_and_port(client_instance) + invocation.server_address = address + invocation.server_port = port + + if capture_content: + invocation.system_instruction = _extract_system_instruction(kwargs) + invocation.input_messages = _extract_input_messages(kwargs) + + return invocation + + def _set_invocation_usage_attributes( invocation: "LLMInvocation", usage: _UsageModel ) -> None: 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 index ce3375b8d5..f4d0d1f713 100644 --- 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 @@ -188,9 +188,8 @@ def until_done(self) -> "ResponseStreamWrapper": return self def parse(self) -> "ResponseStreamWrapper": - raise NotImplementedError( - "ResponseStreamWrapper.parse() is not implemented" - ) + """Called when using with_raw_response with stream=True.""" + return self # TODO: Replace __getattr__ passthrough with wrapt.ObjectProxy in a future # cleanup once wrapt 2 typing support is available (wrapt PR #3903). @@ -387,9 +386,8 @@ async def until_done(self) -> "AsyncResponseStreamWrapper[TextFormatT]": return self def parse(self) -> "AsyncResponseStreamWrapper[TextFormatT]": - raise NotImplementedError( - "AsyncResponseStreamWrapper.parse() is not implemented" - ) + """Called when using with_raw_response with stream=True.""" + return self @property def response(self): diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml new file mode 100644 index 0000000000..ec42e9a4c8 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini" + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '103' + 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_010703fa494eff1b0069d85fdc796481a28f22f0d52e98cb86", + "object": "response", + "created_at": 1775787996, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775787997, + "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_010703fa494eff1b0069d85fddb59481a2b130ddbea671fff4", + "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: + - 9e9e4ec1bd558c6d-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:37 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: + - '1397' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=UKb371H39UgnWj6PgjEmWx.OBnfq9XxNpaLdTC6fFhA-1775787996.4343424-1.0.1.1-oUJCBytz8nRgzMoToAAArAixTo3VhLjSwMR9D.gyYqYQV6EIcYFljEj4pgV0vcEJaclwmxGpzQ1NwUa5N5_Wbt0wfqk.xhZRgOkYyB7Z2STM4qfew0YZXSgx69U0yztK; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:37 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9996' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 33.459s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_463bfcd591944e8b92af0b183b9ad8f2 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml new file mode 100644 index 0000000000..0abea51d06 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml @@ -0,0 +1,94 @@ +interactions: +- request: + body: |- + { + "input": "Hello", + "model": "this-model-does-not-exist" + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '56' + 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: |- + { + "error": { + "message": "The requested model 'this-model-does-not-exist' does not exist.", + "type": "invalid_request_error", + "param": "model", + "code": "model_not_found" + } + } + headers: + CF-RAY: + - 9e9e4ee6c98f780c-EWR + Connection: + - keep-alive + Content-Length: + - '191' + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:42 GMT + Server: + - cloudflare + Set-Cookie: test_set_cookie + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: test_openai_org_id + openai-processing-ms: + - '78' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=CsRql7dORYmp6jh3htZAm_m0RErYeE1U_PlETalT3QQ-1775788002.3668075-1.0.1.1-aur_w.sUFC8IJwwMM6gXZkWGPoxg7q_jyVIbIkxGi5IqIrU3.CMkvupgsED_7sTuqhp81oRru4_SDZh_J1d6vmJInETx3alIpHCrR5UWJ0vvkhAkJwxLGnZ3AsfrKIoF; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:42 GMT + x-request-id: + - req_f337a313a905474498b8d6f35de72475 + status: + code: 400 + message: Bad Request +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml new file mode 100644 index 0000000000..f77651fcca --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].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_04caba33425205fa0069d85fd2ab088197960cb41a20e67147", + "object": "response", + "created_at": 1775787986, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775787987, + "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_04caba33425205fa0069d85fd3e67481979fda2421b1fba85e", + "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: + - 9e9e4e7ed9ef1f47-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:28 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: + - '1403' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=kg4qtbr35BLD5Mbw.m8kinlLNbYtS5wdwYcXz490hG4-1775787985.7323968-1.0.1.1-HOHGUhKSe8jI.TJjOKssznv1hyW7k6XG3QVyKQ0pROkpfCo.kp.LVJmzFXw6ZZKQzj2z9PYPWTsY9MopqcEOgG4hqe.xx_L0i8u_CLSHHaimvq_4hcS5uJNCd56oxN59; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:28 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 8.64s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_838fd6c357d14955aecc16fbafcd81d9 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml new file mode 100644 index 0000000000..4f857dcec0 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml @@ -0,0 +1,178 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": false, + "text": { + "format": { + "type": "text" + } + } + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '158' + 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_01ebc5c056a2e1aa0069d85fd620e8819c9e300d5b06537da2", + "object": "response", + "created_at": 1775787990, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775787991, + "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_01ebc5c056a2e1aa0069d85fd772c0819c91006372be0ccfa1", + "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: + - 9e9e4e900b63acc5-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:31 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: + - '1483' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=NnpQnSwjhbSJqk0O6xoIzBRYf8JfdyiD1_LCi1De5Cg-1775787988.488219-1.0.1.1-f57TtdxSDxGhM6_a46bjUxdogyVqJHoU5WHgDmAF2pBs3vzla2D3BLKlV1C0cpPByaaRZP9Zii3E7emk9zrkQLSgPEbM3jiGIxzuW2pXVQO.PdHugSY7PNK9eIq89TL3; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:31 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9998' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 13.802s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_08136e915de245d3a318e4604db8aae5 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml new file mode 100644 index 0000000000..f31cc34a46 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml @@ -0,0 +1,179 @@ +interactions: +- request: + body: |- + { + "input": "What is 17*19? Think first.", + "model": "gpt-5-mini", + "reasoning": { + "summary": "concise" + } + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '100' + 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_00ffaee8970a201e0069d85ff21eec8191b6400bb158fb42bf", + "object": "response", + "created_at": 1775788019, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788028, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-mini-2025-08-07", + "output": [ + { + "id": "rs_00ffaee8970a201e0069d85ff850688191b0eacdc13b1f4752", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_00ffaee8970a201e0069d85ffc9fec819199bbeefde8316d62", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "17 \u00d7 19 = 323.\n\n(short method: 17\u00d719 = 17\u00d7(20\u22121) = 340 \u2212 17 = 323)" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0.0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": "concise" + }, + "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": 16, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 202, + "output_tokens_details": { + "reasoning_tokens": 128 + }, + "total_tokens": 218 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9e9e4f3e2b12f5f7-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:27:09 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: + - '1762' + openai-organization: test_openai_org_id + openai-processing-ms: + - '12496' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=VF0yBu1fFpOJzFTLDniavq9V3LVRsxdEm2CuC2oou5Q-1775788016.3419962-1.0.1.1-88h4uvoyPeRYvQfG.rhNuzzxbhnusBvt2rhdn67fmhLtcc15U8MnzW0ICpyElmisnBs_WFgYn_kdL39z59JRgleQ18.J6DoshssrHnwmYIhdOmbsMTew4Ggx3JwqD9pH; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:57:09 GMT + x-ratelimit-limit-requests: + - '500' + x-ratelimit-limit-tokens: + - '500000' + x-ratelimit-remaining-requests: + - '499' + x-ratelimit-remaining-tokens: + - '500000' + x-ratelimit-reset-requests: + - 120ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_4313bb2494264db6b5bd8bd3a9fe8462 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml new file mode 100644 index 0000000000..00f22123c6 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml @@ -0,0 +1,213 @@ +interactions: +- request: + body: |- + { + "input": "What's the weather in Seattle right now?", + "model": "gpt-4o-mini", + "tool_choice": { + "type": "function", + "name": "get_current_weather" + }, + "tools": [ + { + "type": "function", + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. Boston, MA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ] + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '480' + 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_0cb296f9e5607fd60069d85fef5a9c8190b9a1023b905723b8", + "object": "response", + "created_at": 1775788015, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788015, + "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": "fc_0cb296f9e5607fd60069d85fefc01c8190b80847c9f8782620", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Seattle, WA\"}", + "call_id": "call_9JGnHg3Qns7ZqDfSVNhAEArE", + "name": "get_current_weather" + } + ], + "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": { + "type": "function", + "name": "get_current_weather" + }, + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_current_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. Boston, MA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 72, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 8, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 80 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9e9e4f379c69ccb6-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:56 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: + - '2027' + openai-organization: test_openai_org_id + openai-processing-ms: + - '674' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=br01RxGo5SXW00_woYY5L7MyEcOiE8dmdgiNFie.oTo-1775788015.2997324-1.0.1.1-RbN016RUGisBdZ1wBbRYnN_LIzdDghSFytfdtUSHT7hKp09BZqJYvJKB_EN.HojlHyCtS25A2c9KRzLF2X1nk7yQHndMvs2VjtElBF_G5TaBgehj6B4Hu.uWXleelGGa; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:56 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9987' + x-ratelimit-remaining-tokens: + - '199711' + x-ratelimit-reset-requests: + - 1m50.441s + x-ratelimit-reset-tokens: + - 86ms + x-request-id: + - req_c096da9861fc4615bae077d9124c49ac + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml new file mode 100644 index 0000000000..b408e0a79e --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.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_07cbd7936ceac8510069d85fff0f6c8192b4b33eb68b420ce2", + "object": "response", + "created_at": 1775788031, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788032, + "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_07cbd7936ceac8510069d860005f348192b6be28a796292e6d", + "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: + - 9e9e4f99989843c2-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:27:12 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: + - '1505' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=o3UfucpdW2pEMxXq8x.tv492pSBhzEof3VOOyh4sc4Y-1775788030.9800544-1.0.1.1-u.YezuLAmHWQYIfnfWzLaHz.ZejG1RddVeKxwoukneeNkQhpS1t_NGLmlOeDV..l1AA4Y2rmPjUc2vxSjMBkQvAqbxompYQIT0AzG9jMN2pmovSrVkqWH4RXXf861BxL; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:57:12 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9986' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 1m59.732s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_1d6a9bc8820a4d2998bfbb41ad108b2d + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml new file mode 100644 index 0000000000..36dab78d58 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"SIvcY5ssmJxE","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"KSmXWZskD03gS","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"FZ3nNox6Jl28SC","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"0vqja7Hignb","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"n5dD8N0lDkoXOVT","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"completed","background":false,"completed_at":1775788014,"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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4f30ef8e8465-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:54 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=G50wZW7_TYVatvs1P15l3XRevsXZA5l7Y6UT8cWKpD8-1775788014.228369-1.0.1.1-.KSdzyylckBVofZyP.ALr55Hk0kPWHo6_3ni1ADFkUmIbaIlOEPS89W1OEHCGeXmslq8UQT2XSaGFvqaF80kFrkvN9jwD2WRFElVtBDJsfSMj8wggSx1neTrIqo9pgGs; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:54 GMT + x-request-id: + - req_6b47baa659dd45859686e511d074943a + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stop_reason[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stop_reason[content_mode0].yaml new file mode 100644 index 0000000000..5e50fe6d5a --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stop_reason[content_mode0].yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Say hi.", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini" + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '92' + 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_04c32bcdfd321da00069d85fdef458819683fe22e49919d4ff", + "object": "response", + "created_at": 1775787998, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788000, + "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_04c32bcdfd321da00069d85fe0454c81969387fedf3b0c30a4", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Hi there! How can I assist you today?" + } + ], + "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": 20, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 11, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 31 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9e9e4ecc3e49557d-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:40 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: + - '1591' + openai-organization: test_openai_org_id + openai-processing-ms: + - '1792' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=iqdcCSZ72UHwh_DaS2PKtBHKnaeJfDB3XXdnKBq_lYk-1775787998.110874-1.0.1.1-wgs5IujlsaERWAmX.f9kpDASYhkM3vUuOU7r4us7Ax271tjt.MXyyfx7UB0dc11p2d4y0Uu_sMJ0t2UBZ4gSXWt2Qg.FZ_Z8tUXF3dScNg6LAwA2kW3y9GsjWleQoEZh; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:40 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9995' + x-ratelimit-remaining-tokens: + - '199961' + x-ratelimit-reset-requests: + - 39.705s + x-ratelimit-reset-tokens: + - 11ms + x-request-id: + - req_7cd7eca858d7445890071c20ac85a5cb + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stream_propagation_error[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stream_propagation_error[content_mode0].yaml new file mode 100644 index 0000000000..b5e2bf3a23 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_stream_propagation_error[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_0b3096e2fc9c5f6e0069d85feb83648191a94bce278e791c8a","object":"response","created_at":1775788011,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0b3096e2fc9c5f6e0069d85feb83648191a94bce278e791c8a","object":"response","created_at":1775788011,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","logprobs":[],"obfuscation":"nc51O0nSiibE","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","logprobs":[],"obfuscation":"yoU6Uk79W0bxW","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","logprobs":[],"obfuscation":"jk7OGoqTWxCtqG","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","logprobs":[],"obfuscation":"UaZwgqyfBoF","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","logprobs":[],"obfuscation":"J3Cumq2wt0fyyXf","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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_0b3096e2fc9c5f6e0069d85feb83648191a94bce278e791c8a","object":"response","created_at":1775788011,"status":"completed","background":false,"completed_at":1775788013,"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_0b3096e2fc9c5f6e0069d85fece7b081919d4af661be995578","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4f1fca41f3e6-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26: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 + openai-organization: test_openai_org_id + openai-processing-ms: + - '64' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=R9QDas1fwU74VVgYqXd9TzsgAAPjykYGa.uDNQOtyUQ-1775788011.4835322-1.0.1.1-Is_GJHdm27wVuSZurGZKTbEQvyR8shC8z38xW1_CxQDFDdcJ_76ueq_goV2rxpyfz4vUe20uE1wnFZAawQ9gWgb58b0A3oXa2z0szEweG91RtieIL6eqAcPT6IspYFwC; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:51 GMT + x-request-id: + - req_db2d6aa9ea824b8588325b27b9a2f316 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming[content_mode0].yaml new file mode 100644 index 0000000000..1ccfec9df7 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming[content_mode0].yaml @@ -0,0 +1,128 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "service_tier": "default", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '146' + 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_0fbd9e3d1111001c0069d85fe2f11c819fb11676e58b5b7bff","object":"response","created_at":1775788002,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + event: response.in_progress + data: {"type":"response.in_progress","response":{"id":"resp_0fbd9e3d1111001c0069d85fe2f11c819fb11676e58b5b7bff","object":"response","created_at":1775788002,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + event: response.output_item.added + data: {"type":"response.output_item.added","item":{"id":"msg_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","logprobs":[],"obfuscation":"ZJTX0RKOR8C9","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","logprobs":[],"obfuscation":"zNkPAbNDs1sKK","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","logprobs":[],"obfuscation":"iv29RcbkAEj1vo","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","logprobs":[],"obfuscation":"z1cUAVHy30c","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","logprobs":[],"obfuscation":"C9As8FWqCPe5syf","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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_0fbd9e3d1111001c0069d85fe2f11c819fb11676e58b5b7bff","object":"response","created_at":1775788002,"status":"completed","background":false,"completed_at":1775788004,"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_0fbd9e3d1111001c0069d85fe435b0819f875602d96de7c6c0","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4eea18757539-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:43 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: + - '72' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=XmXMjxSDN5Yl6wEHoV2SMTCjpvdSyAhJANf._fmq6sI-1775788002.899318-1.0.1.1-t.ldWKmmkD2u_TiFdtnnnrwpeocTWOjAr.MJiw8AWur12XWCCKkKra9SHtlZqxfZ1x4iNiHRXZpVxtodW.IElfS4yAGG6upoxjeb2AKuNQWg95Ey0gAlS7bSnlmlz85L; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:43 GMT + x-request-id: + - req_0120d3ea46ff4b78b2dc58332dcd0730 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_aggregates_cache_tokens[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_aggregates_cache_tokens[content_mode0].yaml new file mode 100644 index 0000000000..c88c65c350 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_aggregates_cache_tokens[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_0f40f2ba541795700069d85fe4a800819f91ee3604774a214f","object":"response","created_at":1775788004,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0f40f2ba541795700069d85fe4a800819f91ee3604774a214f","object":"response","created_at":1775788004,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","logprobs":[],"obfuscation":"A26ZUlifFMrV","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","logprobs":[],"obfuscation":"PaStGK4n2ofBO","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","logprobs":[],"obfuscation":"f0UrMgxTV3QDVt","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","logprobs":[],"obfuscation":"JKHj13n4dZo","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","logprobs":[],"obfuscation":"OideIUAGhkizVWG","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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_0f40f2ba541795700069d85fe4a800819f91ee3604774a214f","object":"response","created_at":1775788004,"status":"completed","background":false,"completed_at":1775788005,"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_0f40f2ba541795700069d85fe51148819fa242ab4079fd997a","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4ef4ebe91016-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26: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: + - '57' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=v2PvXxvn7B5pUI_66b_aJrUWLx1yku3nRaRnoQCxags-1775788004.6269343-1.0.1.1-h_C05XX2Gr73zG5LLbWUlMpLOoDADIpjIdtlAXIlY8Y_JzfyU8I_t0UwrNFmHJq1F.ErWkS0DPgiBeQo2aFs5nksUHMXh6eQDRkBqFUHhhQdpXtp1EIMkIUiEv912pKa; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:44 GMT + x-request-id: + - req_889de9d5dde540da9a09091f95a4d6e5 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_captures_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_captures_content[content_mode0].yaml new file mode 100644 index 0000000000..a1a65f5816 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_captures_content[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_063c349de8c8f7460069d85fe58d508190b219b004f0b5ed3f","object":"response","created_at":1775788005,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_063c349de8c8f7460069d85fe58d508190b219b004f0b5ed3f","object":"response","created_at":1775788005,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","logprobs":[],"obfuscation":"eXA2LtUhvjY7","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","logprobs":[],"obfuscation":"ieIX8dxQGVxUt","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","logprobs":[],"obfuscation":"eTPvr4Ka28Z1GC","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","logprobs":[],"obfuscation":"gB10KXHqE3J","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","logprobs":[],"obfuscation":"GP1e7cha9eBaT2t","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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_063c349de8c8f7460069d85fe58d508190b219b004f0b5ed3f","object":"response","created_at":1775788005,"status":"completed","background":false,"completed_at":1775788006,"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_063c349de8c8f7460069d85fe5f32081908ab66b2a8a735153","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4efa6dc80f39-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:45 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: + - '74' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=15Qp8jkmAN8tGUQKoU.UPo48G0olk6nQR9nTJGNFFIA-1775788005.5067973-1.0.1.1-C.ieExM1f5SYK5rjZ10_UUwMUlL55g1x7omZa6Nprrp1HNG93ZTzqUZgadX3fZAE8T3atYUpUtMQbSsVrJDZndV.XFlItd.TVW2JjbzMp7lRV787.maAwwZ_ucfwHcgx; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:45 GMT + x-request-id: + - req_68551ade97f44e2393fc29c44efa4d6a + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_delegates_response_attribute[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_delegates_response_attribute[content_mode0].yaml new file mode 100644 index 0000000000..ba543c8359 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_delegates_response_attribute[content_mode0].yaml @@ -0,0 +1,142 @@ +interactions: +- request: + body: |- + { + "input": "Say hi.", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '108' + 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_095a4f6718867a2c0069d85fe8733c81a39c8e56d486672e2c","object":"response","created_at":1775788008,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_095a4f6718867a2c0069d85fe8733c81a39c8e56d486672e2c","object":"response","created_at":1775788008,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","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_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","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":"Hi","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"Km63kONUDJtKGX","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" there","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"23TDWXiU5b","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"FMLBoVgLJQfOsgD","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" How","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"dHkKPrqHyWwu","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"B0Zqh9vuiJfd","output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"rn9xXIOUVE61CA","output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" assist","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"5W2iukTtm","output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"tDfFIxMf07Nb","output_index":0,"sequence_number":11} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" today","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"DrTEj4Tqw9","output_index":0,"sequence_number":12} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"?","item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"obfuscation":"mlwPcO7d3LJZgQp","output_index":0,"sequence_number":13} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","logprobs":[],"output_index":0,"sequence_number":14,"text":"Hi there! How can I assist you today?"} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"},"sequence_number":15} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"}],"role":"assistant"},"output_index":0,"sequence_number":16} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_095a4f6718867a2c0069d85fe8733c81a39c8e56d486672e2c","object":"response","created_at":1775788008,"status":"completed","background":false,"completed_at":1775788009,"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_095a4f6718867a2c0069d85fe8ef5881a3ae4580b17e82fe85","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"}],"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":20,"input_tokens_details":{"cached_tokens":0},"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":31},"user":null,"metadata":{}},"sequence_number":17} + + headers: + CF-RAY: + - 9e9e4f0c892fcd7f-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26: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: + - '76' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=Vw7u3PaGtXVJ7g8L8AwOIbF69FKtwSyocKnNlWjTVwE-1775788008.411057-1.0.1.1-HEH2PUukscqhdnOB0JQRj6znSMmELu.Nnro4gjMKFfv6ElF35K.zz09OGX.o6TUAmCJteBVnR9fgZoMGiqXIbJYczCa46qgEg08DuL3EJPwDY08Bn09mWG50HgJRnauO; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:48 GMT + x-request-id: + - req_33346beabe1c4377aa06ac5b2b91fe14 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_iteration[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_iteration[content_mode0].yaml new file mode 100644 index 0000000000..54b728cd40 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_iteration[content_mode0].yaml @@ -0,0 +1,142 @@ +interactions: +- request: + body: |- + { + "input": "Say hi.", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '108' + 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_044ac187c9c304610069d85fe67de48193ace31859e09c522c","object":"response","created_at":1775788006,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_044ac187c9c304610069d85fe67de48193ace31859e09c522c","object":"response","created_at":1775788006,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","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_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","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":"Hi","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"1vbuA4HW3O0NSW","output_index":0,"sequence_number":4} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" there","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"n7CozaVcW7","output_index":0,"sequence_number":5} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"!","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"ZhXp5ceoVpiimYd","output_index":0,"sequence_number":6} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" How","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"hjUe6ZVVAUjs","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" can","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"4AnQTUheLsGR","output_index":0,"sequence_number":8} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" I","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"mK4sCZEN6rWPwG","output_index":0,"sequence_number":9} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" assist","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"ZQrugdYX9","output_index":0,"sequence_number":10} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" you","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"gNWgFpWNHSHq","output_index":0,"sequence_number":11} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":" today","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"5fsK2qJw0W","output_index":0,"sequence_number":12} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":"?","item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"obfuscation":"bgO18mk8SNJVujk","output_index":0,"sequence_number":13} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","logprobs":[],"output_index":0,"sequence_number":14,"text":"Hi there! How can I assist you today?"} + + event: response.content_part.done + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"},"sequence_number":15} + + event: response.output_item.done + data: {"type":"response.output_item.done","item":{"id":"msg_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"}],"role":"assistant"},"output_index":0,"sequence_number":16} + + event: response.completed + data: {"type":"response.completed","response":{"id":"resp_044ac187c9c304610069d85fe67de48193ace31859e09c522c","object":"response","created_at":1775788006,"status":"completed","background":false,"completed_at":1775788008,"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_044ac187c9c304610069d85fe7e3d8819392c3e3d70d448484","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"Hi there! How can I assist you today?"}],"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":20,"input_tokens_details":{"cached_tokens":0},"output_tokens":11,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":31},"user":null,"metadata":{}},"sequence_number":17} + + headers: + CF-RAY: + - 9e9e4f00288db35a-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:46 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: + - '81' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=nEyVlmmch9p8yXoX4cJH2XUxHgpelNa9WqmmByGcTyw-1775788006.4231112-1.0.1.1-OLhlKGn3ZpxAP8RKjDBl5v2SgrdffCfbKgJkNb0q4WmXDVQ68I2UlFL0w7sjEzu4LqrBNkqG7NWz2EB9KcLBkJvHM3iviMxnHiUCN8Ymx.HBMeuVo6FySpxm8VOiF2.I; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:46 GMT + x-request-id: + - req_5543c717c2f94ac8b2d625fd7a43a992 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_user_exception[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_user_exception[content_mode0].yaml new file mode 100644 index 0000000000..40a53fe9f3 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_streaming_user_exception[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_0ab5399c40b248980069d85fed5b908196b3c405a2e406528e","object":"response","created_at":1775788013,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0ab5399c40b248980069d85fed5b908196b3c405a2e406528e","object":"response","created_at":1775788013,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","logprobs":[],"obfuscation":"rWBr12xNhvj4","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","logprobs":[],"obfuscation":"jyr78tGfT3Ei3","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","logprobs":[],"obfuscation":"nletSvz2ofh0xQ","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","logprobs":[],"obfuscation":"0eVmSRu396A","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","logprobs":[],"obfuscation":"bwRrFTM7d6KGw37","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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_0ab5399c40b248980069d85fed5b908196b3c405a2e406528e","object":"response","created_at":1775788013,"status":"completed","background":false,"completed_at":1775788013,"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_0ab5399c40b248980069d85fedd82c8196bf21533dd91174a9","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4f2b38d149aa-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:53 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: + - '72' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=s8W4W.hBzGUeEJkHQeIG1_v86xJWwZSRBd9MwV9lWys-1775788013.319579-1.0.1.1-o74Xn7xSK1ngXDXxuL08yfKZJCaHHbaHHdCkIhtlbLOM2uuS_NbjAEJDK3JxVEaVuHIz9YlenuptnE56C2Mj892B6nYfh97gBebtbgi6.yd.MChTm8roBOKjvCM2BrvX; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:53 GMT + x-request-id: + - req_a006676e9fc5495d9a5c70c6a5432cd4 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_token_usage[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_token_usage[content_mode0].yaml new file mode 100644 index 0000000000..2f4f5c6c39 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_token_usage[content_mode0].yaml @@ -0,0 +1,172 @@ +interactions: +- request: + body: |- + { + "input": "Count to 5.", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini" + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '96' + 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_06cb37a988a448460069d85fda2a54819ea8505197e05be2fe", + "object": "response", + "created_at": 1775787994, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775787995, + "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_06cb37a988a448460069d85fdbc210819eb48d85855ab91c85", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "1, 2, 3, 4, 5." + } + ], + "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": 15, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 37 + }, + "user": null, + "metadata": {} + } + headers: + CF-RAY: + - 9e9e4eb34f7a7611-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:36 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: + - '1903' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=2nx.Vg_ezpIxFRjTFu2N4ewyxvCWAXrujgxwdjjJ5Ws-1775787994.1245146-1.0.1.1-r2ixTVtyYhx.3mQarHXRAbLyg6rVSCPprgpHq3hVySWzzHkDcF0JjOka2npimkyhSL6ogDSrYuni0frjgyP2YKrFdL4g.MhSFIpAW6rzDU3wDuFhlY71vYHpmxi36xD3; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:36 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9996' + x-ratelimit-remaining-tokens: + - '199958' + x-ratelimit-reset-requests: + - 27.061s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_c459e8be9d064ef68883e3eafbbe842f + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_all_params[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_all_params[content_mode0].yaml new file mode 100644 index 0000000000..18e97b9a69 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_all_params[content_mode0].yaml @@ -0,0 +1,181 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "max_output_tokens": 50, + "model": "gpt-4o-mini", + "service_tier": "default", + "temperature": 0.7, + "text": { + "format": { + "type": "text" + } + }, + "top_p": 0.9 + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '227' + 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_0148a489a29da95a0069d85fd7eff88196b30251cee68ee213", + "object": "response", + "created_at": 1775787991, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775787993, + "error": null, + "frequency_penalty": 0.0, + "incomplete_details": null, + "instructions": "You are a helpful assistant.", + "max_output_tokens": 50, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "msg_0148a489a29da95a0069d85fd9921881969419506db2fe8e5b", + "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": 0.7, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 0.9, + "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: + - 9e9e4ea54b19c598-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:26:34 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: + - '1566' + openai-organization: test_openai_org_id + openai-processing-ms: + - '1925' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=T7DzvwG02M4..DzelJiLphbvg3mRnam3BVFWwORluIU-1775787991.883274-1.0.1.1-g.AU4BYwgiFGi.wkDWBRKrVFlQ_txmR7gyL6f8jaPCJ_eeodANpk6HihVMUrw7TkwlVYKXKU14RHNOeXAN.sqR9mX0_B_OPXqp5Ua.nYmzYgoo5FL0pkil5MPAuGs6cF; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:34 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9997' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 20.554s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_b51ddb8279004aebacec15082044c55f + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_shapes[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_shapes[content_mode0].yaml new file mode 100644 index 0000000000..f06e10165d --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_shapes[content_mode0].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_098a0b1ff5a64ee20069d85ffe52c88194893322bd491c639f", + "object": "response", + "created_at": 1775788030, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788030, + "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_098a0b1ff5a64ee20069d85ffe914c8194be619807a7563617", + "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: + - 9e9e4f954876f580-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:27:10 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: + - '419' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=Juk22lx7il5.8Dj6Qe5r0zTzUIfPoLJZNurFR7m_GYU-1775788030.281736-1.0.1.1-uLuVFdyZdmquFgAnQfRAwqbKloj_E0.QfRNnNrZfHeG59Fwx.niIYpSuDPXtMJHvegKV2z5LOlUcLEd9qhLIfkDAcBP.1pARVA.z4fX2tY5hLz07O_7xN53nP6KKkWtz; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:57:10 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9987' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 1m44.133s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_cd01f60cdb89459fa65289f9ea499362 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_span_unsampled[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_span_unsampled[content_mode0].yaml new file mode 100644 index 0000000000..173512043d --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_with_content_span_unsampled[content_mode0].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_09c37a42cd5e29e50069d85ffd598c8196b06c38f3ec2c0c5d", + "object": "response", + "created_at": 1775788029, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1775788029, + "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_09c37a42cd5e29e50069d85ffdbf9c81968e9f52a59aad09cf", + "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: + - 9e9e4f8f29da3d85-EWR + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Fri, 10 Apr 2026 02:27:10 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: + - '612' + openai-project: + - proj_s74VWObPgWXRchv2sHdrOTPY + openai-version: + - '2020-10-01' + set-cookie: + - __cf_bm=RtJbfcfhWVu17IMRV15gn9SXfEDyvtifoeF2qbh26Tg-1775788029.305633-1.0.1.1-WG2IIGcb9kgWa9M70u7nXtA5_0quSngDCF0HTN6ZSpoQqregNtfkoGlraTGfFZ3L68A4tHgrwpe6vREPC5EdlAYWdQeR.aPoYEYriVvrm_XVjLL5lbWWouUJsBzbiCD3; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:57:10 GMT + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '200000' + x-ratelimit-remaining-requests: + - '9987' + x-ratelimit-remaining-tokens: + - '199959' + x-ratelimit-reset-requests: + - 1m45.102s + x-ratelimit-reset-tokens: + - 12ms + x-request-id: + - req_e000ce12927d4535aff19d007e59cac6 + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_wrapper_finalize_idempotent[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_wrapper_finalize_idempotent[content_mode0].yaml new file mode 100644 index 0000000000..aa35670299 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_stream_wrapper_finalize_idempotent[content_mode0].yaml @@ -0,0 +1,127 @@ +interactions: +- request: + body: |- + { + "input": "Say this is a test", + "instructions": "You are a helpful assistant.", + "model": "gpt-4o-mini", + "stream": true + } + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '119' + 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_028ed7d276fa81e10069d85feabfa4819692fcc8e1fa076ade","object":"response","created_at":1775788010,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_028ed7d276fa81e10069d85feabfa4819692fcc8e1fa076ade","object":"response","created_at":1775788010,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","logprobs":[],"obfuscation":"CG4sNhiLWuGC","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","logprobs":[],"obfuscation":"jHBObK7z99E0W","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","logprobs":[],"obfuscation":"B9pwQYVQPWUxbG","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","logprobs":[],"obfuscation":"MIBApfdNMvK","output_index":0,"sequence_number":7} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","logprobs":[],"obfuscation":"WOq8y7NOSXRXIA5","output_index":0,"sequence_number":8} + + event: response.output_text.done + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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_028ed7d276fa81e10069d85feabfa4819692fcc8e1fa076ade","object":"response","created_at":1775788010,"status":"completed","background":false,"completed_at":1775788011,"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_028ed7d276fa81e10069d85feb0a388196b97671aa3c1abaae","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":{}},"sequence_number":12} + + headers: + CF-RAY: + - 9e9e4f1aee770c72-EWR + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 10 Apr 2026 02:26:50 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=Nc2me5RSu_.UofRpZc1zajbJH7n7UT_uYnzi7A0Mb.k-1775788010.7046952-1.0.1.1-xr9f9UONQBKigYPBY4UOc9F7V8PWBU297MyLMi9o8.Aii1Vn5snvdURdniSVGlSZbhVp.uZ0mc3.MpFXuG6GSbAXLakGHYnnm7PK2xnhpe1ePt6tF11H3yEFaXjBFy7t; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 + 02:56:50 GMT + x-request-id: + - req_80f85c43461b443ea83413f6257db647 + 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 9c8f87c943..cc6dd55796 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py @@ -226,6 +226,32 @@ def instrument_with_content_unsampled( instrumentor.uninstrument() +@pytest.fixture(scope="function") +def instrument_event_only( + tracer_provider, logger_provider, meter_provider +): + _OpenTelemetrySemanticConventionStability._initialized = False + + os.environ.update( + { + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "event_only", + OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", + } + ) + + 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) + 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/test_async_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py new file mode 100644 index 0000000000..8240e85caf --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py @@ -0,0 +1,857 @@ +# 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 inspect +import json + +import pytest +from openai import APIConnectionError, AsyncOpenAI, NotFoundError + +from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor +from opentelemetry.instrumentation.openai_v2.response_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, +) +from opentelemetry.instrumentation.openai_v2.response_wrappers import ( + AsyncResponseStreamWrapper, +) +from opentelemetry.semconv._incubating.attributes import ( + error_attributes as ErrorAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.util.genai.utils import is_experimental_mode + +from .test_utils import ( + DEFAULT_MODEL, + USER_ONLY_EXPECTED_INPUT_MESSAGES, + USER_ONLY_PROMPT, + assert_all_attributes, + assert_messages_attribute, + format_simple_expected_output_message, + get_responses_weather_tool_definition, + skip_if_cassette_missing_and_no_real_key, +) + +try: + from openai.resources.responses.responses import AsyncResponses as _AsyncResponses + + HAS_RESPONSES_API = True + _create_params = set(inspect.signature(_AsyncResponses.create).parameters) + _has_tools_param = "tools" in _create_params + _has_reasoning_param = "reasoning" in _create_params +except ImportError: + HAS_RESPONSES_API = False + _has_tools_param = False + _has_reasoning_param = False + + +pytestmark = pytest.mark.skipif( + not HAS_RESPONSES_API, reason="Responses API requires a newer openai SDK" +) + +SYSTEM_INSTRUCTIONS = "You are a helpful assistant." +EXPECTED_SYSTEM_INSTRUCTIONS = [ + { + "type": "text", + "content": SYSTEM_INSTRUCTIONS, + } +] +INVALID_MODEL = "this-model-does-not-exist" +REASONING_MODEL = "gpt-5-mini" + + +def _skip_if_not_latest(): + if not is_experimental_mode(): + pytest.skip( + "Responses create instrumentation only supports the latest experimental semconv path" + ) + + +def _load_span_messages(span, attribute): + value = span.attributes.get(attribute) + assert value is not None + return json.loads(value) + + +def _assert_response_content(span, response, log_exporter): + assert_messages_attribute( + span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES], + USER_ONLY_EXPECTED_INPUT_MESSAGES, + ) + assert json.loads( + span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS] + ) == EXPECTED_SYSTEM_INSTRUCTIONS + assert_messages_attribute( + span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES], + format_simple_expected_output_message(response.output_text), + ) + assert len(log_exporter.get_finished_logs()) == 0 + + +def _assert_request_attrs( + span, + *, + temperature=None, + top_p=None, + max_tokens=None, + output_type=None, +): + if temperature is not None: + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] + == temperature + ) + if top_p is not None: + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == top_p + if max_tokens is not None: + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] + == max_tokens + ) + if output_type is not None: + assert span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type + + +async def _collect_completed_response(stream): + response = None + async for event in stream: + if event.type == "response.completed": + response = event.response + assert response is not None + return response + + +def _get_usage_details(usage): + return getattr(usage, "input_tokens_details", None) or getattr( + usage, "prompt_tokens_details", None + ) + + +def _assert_cache_attributes(span, usage): + details = _get_usage_details(usage) + assert details is not None + + cached_tokens = getattr(details, "cached_tokens", None) + if cached_tokens is None: + assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] + == cached_tokens + ) + + cache_creation = getattr(details, "cache_creation_input_tokens", None) + if cache_creation is None: + assert ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS + not in span.attributes + ) + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] + == cache_creation + ) + + +def test_async_responses_uninstrument_removes_patching( + span_exporter, tracer_provider, logger_provider, meter_provider +): + instrumentor = OpenAIInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + assert len(span_exporter.get_finished_spans()) == 0 + + +def test_async_responses_multiple_instrument_uninstrument_cycles( + tracer_provider, logger_provider, meter_provider +): + instrumentor = OpenAIInstrumentor() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_basic( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + == ("stop",) + ) + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_captures_content( + request, + span_exporter, + log_exporter, + async_openai_client, + instrument_with_content, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + text={"format": {"type": "text"}}, + ) + + (span,) = span_exporter.get_finished_spans() + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_with_all_params( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + max_output_tokens=50, + temperature=0.7, + top_p=0.9, + service_tier="default", + text={"format": {"type": "text"}}, + ) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + _assert_request_attrs( + span, + temperature=0.7, + top_p=0.9, + max_tokens=50, + output_type="text", + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_token_usage( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Count to 5.", + ) + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] + == response.usage.input_tokens + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] + == response.usage.output_tokens + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_aggregates_cache_tokens( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) + + (span,) = span_exporter.get_finished_spans() + _assert_cache_attributes(span, response.usage) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_stop_reason( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "stop", + ) + + +@pytest.mark.asyncio() +async def test_async_responses_create_connection_error( + span_exporter, instrument_no_content +): + _skip_if_not_latest() + + client = AsyncOpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + await client.responses.create( + model=DEFAULT_MODEL, + input="Hello", + timeout=0.1, + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost" + assert span.attributes[ServerAttributes.SERVER_PORT] == 4242 + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_api_error( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with pytest.raises(NotFoundError): + await async_openai_client.responses.create( + model=INVALID_MODEL, + input="Hello", + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "NotFoundError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + service_tier="default", + stream=True, + ) + async with stream: + response = await _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming_aggregates_cache_tokens( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + async with stream: + response = await _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + _assert_cache_attributes(span, response.usage) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming_captures_content( + request, + span_exporter, + log_exporter, + async_openai_client, + instrument_with_content, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + async with stream: + response = await _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming_iteration( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + stream=True, + ) + events = [] + async for event in stream: + events.append(event) + + assert len(events) > 0 + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming_delegates_response_attribute( + request, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + stream=True, + ) + + assert stream.response is not None + assert stream.response.status_code == 200 + assert stream.response.headers.get("x-request-id") is not None + await stream.close() + + +@pytest.mark.asyncio() +async def test_async_responses_create_streaming_connection_error( + span_exporter, instrument_no_content +): + _skip_if_not_latest() + + client = AsyncOpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + await client.responses.create( + model=DEFAULT_MODEL, + input="Hello", + stream=True, + timeout=0.1, + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_stream_wrapper_finalize_idempotent( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + + response = await _collect_completed_response(stream) + await stream.close() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert_all_attributes( + spans[0], + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_stream_propagation_error( + request, + span_exporter, + async_openai_client, + instrument_no_content, + monkeypatch, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + + class ErrorInjectingStreamDelegate: + def __init__(self, inner): + self._inner = inner + self._count = 0 + + def __aiter__(self): + return self + + async def __anext__(self): + if self._count == 1: + raise ConnectionError("connection reset during stream") + self._count += 1 + return await self._inner.__anext__() + + async def close(self): + return await self._inner.close() + + def __getattr__(self, name): + return getattr(self._inner, name) + + monkeypatch.setattr( + stream, "stream", ErrorInjectingStreamDelegate(stream.stream) + ) + + with pytest.raises(ConnectionError, match="connection reset during stream"): + async with stream: + async for _ in stream: + pass + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_streaming_user_exception( + request, span_exporter, async_openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + + with pytest.raises(ValueError, match="User raised exception"): + async with stream: + async for _ in stream: + raise ValueError("User raised exception") + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_instrumentation_error_swallowed( + request, + span_exporter, + async_openai_client, + instrument_no_content, + monkeypatch, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + def exploding_process_event(self, event): + del self + del event + raise RuntimeError("instrumentation bug") + + monkeypatch.setattr( + AsyncResponseStreamWrapper, "process_event", exploding_process_event + ) + + stream = await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + async with stream: + events = [event async for event in stream] + + assert len(events) > 0 + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ErrorAttributes.ERROR_TYPE not in span.attributes + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_tools_param, + reason=( + "openai SDK too old to support 'tools' parameter on AsyncResponses.create" + ), +) +async def test_async_responses_create_captures_tool_call_content( + request, span_exporter, async_openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=DEFAULT_MODEL, + input="What's the weather in Seattle right now?", + tools=[get_responses_weather_tool_definition()], + tool_choice={"type": "function", "name": "get_current_weather"}, + ) + + (span,) = span_exporter.get_finished_spans() + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + assert any( + part.get("type") == "tool_call" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_reasoning_param, + reason=( + "openai SDK too old to support 'reasoning' parameter on AsyncResponses.create" + ), +) +async def test_async_responses_create_captures_reasoning_content( + request, span_exporter, async_openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=REASONING_MODEL, + input="What is 17*19? Think first.", + reasoning={"summary": "concise"}, + ) + + (span,) = span_exporter.get_finished_spans() + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + assert any( + part.get("type") == "reasoning" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_with_content_span_unsampled( + request, + span_exporter, + log_exporter, + async_openai_client, + instrument_with_content_unsampled, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + assert len(span_exporter.get_finished_spans()) == 0 + assert len(log_exporter.get_finished_logs()) == 0 + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_with_content_shapes( + request, + span_exporter, + log_exporter, + async_openai_client, + instrument_with_content, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + 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 input_messages[0]["parts"][0]["type"] == "text" + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"][0]["type"] == "text" + assert len(log_exporter.get_finished_logs()) == 0 + + +@pytest.mark.asyncio() +@pytest.mark.vcr() +async def test_async_responses_create_event_only_no_content_in_span( + request, + span_exporter, + log_exporter, + async_openai_client, + instrument_event_only, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + await async_openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS not in span.attributes + + logs = log_exporter.get_finished_logs() + assert len(logs) == 1 + assert ( + logs[0].log_record.event_name + == "gen_ai.client.inference.operation.details" + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_extractors.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_extractors.py index 1cf9a1a70e..eb20b44a42 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_extractors.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_extractors.py @@ -109,7 +109,22 @@ def test_extract_output_messages_maps_parts_and_finish_reasons(loaded_module): status="queued", content=[SimpleNamespace(type="output_text", text="Pending")], ), - SimpleNamespace(type="tool_call", status="completed", content=[]), + SimpleNamespace( + type="function_call", + status="completed", + name="get_weather", + call_id="call_123", + arguments='{"city":"SF"}', + content=[], + ), + SimpleNamespace( + type="reasoning", + status="completed", + summary=[ + SimpleNamespace(type="summary_text", text="Thought step") + ], + content=[], + ), ] ) @@ -118,24 +133,37 @@ def test_extract_output_messages_maps_parts_and_finish_reasons(loaded_module): assert [(msg.role, msg.finish_reason) for msg in messages] == [ ("assistant", "stop"), ("assistant", "incomplete"), + ("assistant", "tool_calls"), + ("assistant", "stop"), ] - assert [[part.content for part in msg.parts] for msg in messages] == [ - ["Done", "Cannot comply"], - ["Partial"], + assert [part.content for part in messages[0].parts] == [ + "Done", + "Cannot comply", ] + assert [part.content for part in messages[1].parts] == ["Partial"] + assert messages[2].parts[0].type == "tool_call" + assert messages[2].parts[0].name == "get_weather" + assert messages[2].parts[0].arguments == {"city": "SF"} + assert messages[3].parts[0].type == "reasoning" + assert messages[3].parts[0].content == "Thought step" -def test_extract_finish_reasons_only_reads_message_items(loaded_module): +def test_extract_finish_reasons_maps_terminal_message_and_tool_items( + loaded_module, +): result = SimpleNamespace( output=[ SimpleNamespace(type="message", status="completed"), SimpleNamespace(type="message", status=None), SimpleNamespace(type="message", status="in_progress"), - SimpleNamespace(type="tool_call", status="incomplete"), + SimpleNamespace(type="function_call", status="incomplete"), ] ) - assert loaded_module._extract_finish_reasons(result) == ["stop"] + assert loaded_module._extract_finish_reasons(result) == [ + "stop", + "tool_calls", + ] def test_extract_output_type_handles_text_format_mapping(loaded_module): 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..be2ab1d90f --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -0,0 +1,798 @@ +# 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 inspect +import json + +import pytest +from openai import APIConnectionError, NotFoundError, OpenAI + +from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor +from opentelemetry.instrumentation.openai_v2.response_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, +) +from opentelemetry.instrumentation.openai_v2.response_wrappers import ( + ResponseStreamWrapper, +) +from opentelemetry.semconv._incubating.attributes import ( + error_attributes as ErrorAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.util.genai.utils import is_experimental_mode + +from .test_utils import ( + DEFAULT_MODEL, + USER_ONLY_EXPECTED_INPUT_MESSAGES, + USER_ONLY_PROMPT, + assert_all_attributes, + assert_messages_attribute, + format_simple_expected_output_message, + get_responses_weather_tool_definition, + skip_if_cassette_missing_and_no_real_key, +) + +try: + from openai.resources.responses.responses import Responses as _Responses + + HAS_RESPONSES_API = True + _create_params = set(inspect.signature(_Responses.create).parameters) + _has_tools_param = "tools" in _create_params + _has_reasoning_param = "reasoning" in _create_params +except ImportError: + HAS_RESPONSES_API = False + _has_tools_param = False + _has_reasoning_param = False + + +pytestmark = pytest.mark.skipif( + not HAS_RESPONSES_API, reason="Responses API requires a newer openai SDK" +) + +SYSTEM_INSTRUCTIONS = "You are a helpful assistant." +EXPECTED_SYSTEM_INSTRUCTIONS = [ + { + "type": "text", + "content": SYSTEM_INSTRUCTIONS, + } +] +INVALID_MODEL = "this-model-does-not-exist" +REASONING_MODEL = "gpt-5-mini" + + +def _skip_if_not_latest(): + if not is_experimental_mode(): + pytest.skip( + "Responses create instrumentation only supports the latest experimental semconv path" + ) + + +def _load_span_messages(span, attribute): + value = span.attributes.get(attribute) + assert value is not None + return json.loads(value) + + +def _assert_response_content(span, response, log_exporter): + assert_messages_attribute( + span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES], + USER_ONLY_EXPECTED_INPUT_MESSAGES, + ) + assert json.loads( + span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS] + ) == EXPECTED_SYSTEM_INSTRUCTIONS + assert_messages_attribute( + span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES], + format_simple_expected_output_message(response.output_text), + ) + assert len(log_exporter.get_finished_logs()) == 0 + + +def _assert_request_attrs( + span, + *, + temperature=None, + top_p=None, + max_tokens=None, + output_type=None, +): + if temperature is not None: + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] + == temperature + ) + if top_p is not None: + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == top_p + if max_tokens is not None: + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] + == max_tokens + ) + if output_type is not None: + assert span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type + + +def _collect_completed_response(stream): + response = None + for event in stream: + if event.type == "response.completed": + response = event.response + assert response is not None + return response + + +def _get_usage_details(usage): + return getattr(usage, "input_tokens_details", None) or getattr( + usage, "prompt_tokens_details", None + ) + + +def _assert_cache_attributes(span, usage): + details = _get_usage_details(usage) + assert details is not None + + cached_tokens = getattr(details, "cached_tokens", None) + if cached_tokens is None: + assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] + == cached_tokens + ) + + cache_creation = getattr(details, "cache_creation_input_tokens", None) + if cache_creation is None: + assert ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS + not in span.attributes + ) + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] + == cache_creation + ) + + +def test_responses_uninstrument_removes_patching( + span_exporter, tracer_provider, logger_provider, meter_provider +): + instrumentor = OpenAIInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + assert len(span_exporter.get_finished_spans()) == 0 + + +def test_responses_multiple_instrument_uninstrument_cycles( + tracer_provider, logger_provider, meter_provider +): + instrumentor = OpenAIInstrumentor() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + +@pytest.mark.vcr() +def test_responses_create_basic( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + == ("stop",) + ) + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + + +@pytest.mark.vcr() +def test_responses_create_captures_content( + request, span_exporter, log_exporter, openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + text={"format": {"type": "text"}}, + ) + + (span,) = span_exporter.get_finished_spans() + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.vcr() +def test_responses_create_with_all_params( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + max_output_tokens=50, + temperature=0.7, + top_p=0.9, + service_tier="default", + text={"format": {"type": "text"}}, + ) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + _assert_request_attrs( + span, + temperature=0.7, + top_p=0.9, + max_tokens=50, + output_type="text", + ) + + +@pytest.mark.vcr() +def test_responses_create_token_usage( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Count to 5.", + ) + + (span,) = span_exporter.get_finished_spans() + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] + == response.usage.input_tokens + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] + == response.usage.output_tokens + ) + + +@pytest.mark.vcr() +def test_responses_create_aggregates_cache_tokens( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + response = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + ) + + (span,) = span_exporter.get_finished_spans() + _assert_cache_attributes(span, response.usage) + + +@pytest.mark.vcr() +def test_responses_create_stop_reason( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "stop", + ) + + +def test_responses_create_connection_error(span_exporter, instrument_no_content): + _skip_if_not_latest() + + client = OpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + client.responses.create( + model=DEFAULT_MODEL, + input="Hello", + timeout=0.1, + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost" + assert span.attributes[ServerAttributes.SERVER_PORT] == 4242 + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.vcr() +def test_responses_create_api_error( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with pytest.raises(NotFoundError): + openai_client.responses.create( + model=INVALID_MODEL, + input="Hello", + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "NotFoundError" + + +@pytest.mark.vcr() +def test_responses_create_streaming( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + service_tier="default", + stream=True, + ) as stream: + response = _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + assert_all_attributes( + span, + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + request_service_tier="default", + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.vcr() +def test_responses_create_streaming_aggregates_cache_tokens( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) as stream: + response = _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + _assert_cache_attributes(span, response.usage) + + +@pytest.mark.vcr() +def test_responses_create_streaming_captures_content( + request, span_exporter, log_exporter, openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) as stream: + response = _collect_completed_response(stream) + + (span,) = span_exporter.get_finished_spans() + _assert_response_content(span, response, log_exporter) + + +@pytest.mark.vcr() +def test_responses_create_streaming_iteration( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + stream=True, + ) + events = list(stream) + + assert len(events) > 0 + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + + +@pytest.mark.vcr() +def test_responses_create_streaming_delegates_response_attribute( + request, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input="Say hi.", + stream=True, + ) + + assert stream.response is not None + assert stream.response.status_code == 200 + assert stream.response.headers.get("x-request-id") is not None + stream.close() + + +def test_responses_create_streaming_connection_error( + span_exporter, instrument_no_content +): + _skip_if_not_latest() + + client = OpenAI(base_url="http://localhost:4242") + + with pytest.raises(APIConnectionError): + client.responses.create( + model=DEFAULT_MODEL, + input="Hello", + stream=True, + timeout=0.1, + ) + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" + + +@pytest.mark.vcr() +def test_responses_stream_wrapper_finalize_idempotent( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + + response = _collect_completed_response(stream) + stream.close() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert_all_attributes( + spans[0], + DEFAULT_MODEL, + True, + response.id, + response.model, + response.usage.input_tokens, + response.usage.output_tokens, + response_service_tier=getattr(response, "service_tier", None), + ) + + +@pytest.mark.vcr() +def test_responses_create_stream_propagation_error( + request, span_exporter, openai_client, instrument_no_content, monkeypatch +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + stream = openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) + + class ErrorInjectingStreamDelegate: + def __init__(self, inner): + self._inner = inner + self._count = 0 + + def __iter__(self): + return self + + def __next__(self): + if self._count == 1: + raise ConnectionError("connection reset during stream") + self._count += 1 + return next(self._inner) + + def close(self): + return self._inner.close() + + def __getattr__(self, name): + return getattr(self._inner, name) + + monkeypatch.setattr( + stream, "stream", ErrorInjectingStreamDelegate(stream.stream) + ) + + with pytest.raises(ConnectionError, match="connection reset during stream"): + with stream: + for _ in stream: + pass + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" + + +@pytest.mark.vcr() +def test_responses_create_streaming_user_exception( + request, span_exporter, openai_client, instrument_no_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + with pytest.raises(ValueError, match="User raised exception"): + with openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) as stream: + for _ in stream: + raise ValueError("User raised exception") + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" + + +@pytest.mark.vcr() +def test_responses_create_instrumentation_error_swallowed( + request, span_exporter, openai_client, instrument_no_content, monkeypatch +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + def exploding_process_event(self, event): + del self + del event + raise RuntimeError("instrumentation bug") + + monkeypatch.setattr( + ResponseStreamWrapper, "process_event", exploding_process_event + ) + + with openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=True, + ) as stream: + events = list(stream) + + assert len(events) > 0 + + (span,) = span_exporter.get_finished_spans() + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ErrorAttributes.ERROR_TYPE not in span.attributes + + +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_tools_param, + reason="openai SDK too old to support 'tools' parameter on Responses.create", +) +def test_responses_create_captures_tool_call_content( + request, span_exporter, openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=DEFAULT_MODEL, + input="What's the weather in Seattle right now?", + tools=[get_responses_weather_tool_definition()], + tool_choice={"type": "function", "name": "get_current_weather"}, + ) + + (span,) = span_exporter.get_finished_spans() + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + assert any( + part.get("type") == "tool_call" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_reasoning_param, + reason=( + "openai SDK too old to support 'reasoning' parameter on Responses.create" + ), +) +def test_responses_create_captures_reasoning_content( + request, span_exporter, openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=REASONING_MODEL, + input="What is 17*19? Think first.", + reasoning={"summary": "concise"}, + ) + + (span,) = span_exporter.get_finished_spans() + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + assert any( + part.get("type") == "reasoning" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.vcr() +def test_responses_create_with_content_span_unsampled( + request, + span_exporter, + log_exporter, + openai_client, + instrument_with_content_unsampled, +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + assert len(span_exporter.get_finished_spans()) == 0 + assert len(log_exporter.get_finished_logs()) == 0 + + +@pytest.mark.vcr() +def test_responses_create_with_content_shapes( + request, span_exporter, log_exporter, openai_client, instrument_with_content +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + 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 input_messages[0]["parts"][0]["type"] == "text" + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"][0]["type"] == "text" + assert len(log_exporter.get_finished_logs()) == 0 + + +@pytest.mark.vcr() +def test_responses_create_event_only_no_content_in_span( + request, span_exporter, log_exporter, openai_client, instrument_event_only +): + _skip_if_not_latest() + skip_if_cassette_missing_and_no_real_key(request) + + openai_client.responses.create( + model=DEFAULT_MODEL, + instructions=SYSTEM_INSTRUCTIONS, + input=USER_ONLY_PROMPT[0]["content"], + stream=False, + ) + + (span,) = span_exporter.get_finished_spans() + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS not in span.attributes + + logs = log_exporter.get_finished_logs() + assert len(logs) == 1 + assert ( + logs[0].log_record.event_name + == "gen_ai.client.inference.operation.details" + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index 211fa3739d..84ca728e1c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -15,8 +15,12 @@ """Shared test utilities for OpenAI instrumentation tests.""" import json +import os +from pathlib import Path from typing import Any, Optional +import pytest + from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, @@ -184,6 +188,26 @@ def get_current_weather_tool_definition(): } +def get_responses_weather_tool_definition(): + return { + "type": "function", + "name": "get_current_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. Boston, MA", + }, + }, + "required": ["location"], + "additionalProperties": False, + }, + "strict": True, + } + + def remove_none_values(body): """Remove None values from a dictionary recursively""" result = {} @@ -261,6 +285,18 @@ def assert_message_in_logs(log, event_name, expected_content, parent_span): assert_log_parent(log, parent_span) +def skip_if_cassette_missing_and_no_real_key(request): + cassette_path = ( + Path(__file__).parent / "cassettes" / f"{request.node.name}.yaml" + ) + api_key = os.getenv("OPENAI_API_KEY") + if not cassette_path.exists() and api_key == "test_openai_api_key": + pytest.skip( + f"Cassette {cassette_path.name} is missing. " + "Set a real OPENAI_API_KEY to record it." + ) + + def assert_embedding_attributes( span: ReadableSpan, request_model: str, From 89c84343a527abfbe73b05fd9164228eb552b6fb Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Wed, 15 Apr 2026 23:00:27 -0400 Subject: [PATCH 02/10] wip: converted responses to use new handler factory methods. --- .../openai_v2/patch_responses.py | 103 ++---------------- .../openai_v2/response_extractors.py | 45 ++++---- .../openai_v2/response_wrappers.py | 32 ++---- .../tests/test_response_wrappers.py | 55 +++++----- 4 files changed, 73 insertions(+), 162 deletions(-) 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 index dc0905b33a..4617b5efbd 100644 --- 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 @@ -27,7 +27,8 @@ from .instruments import Instruments from .response_extractors import ( - _create_invocation as create_response_invocation, + _apply_request_attributes, + _get_inference_creation_kwargs, _set_invocation_response_attributes, ) from .response_wrappers import AsyncResponseStreamWrapper, ResponseStreamWrapper @@ -43,11 +44,10 @@ def responses_create( capture_content = content_capturing_mode != ContentCapturingMode.NO_CONTENT def traced_method(wrapped, instance, args, kwargs): - invocation = handler.start_llm( - create_response_invocation( - kwargs, instance, capture_content=capture_content - ) + invocation = handler.start_inference( + **_get_inference_creation_kwargs(kwargs, instance) ) + _apply_request_attributes(invocation, kwargs, capture_content) try: result = wrapped(*args, **kwargs) @@ -56,7 +56,6 @@ def traced_method(wrapped, instance, args, kwargs): if is_streaming(kwargs): return ResponseStreamWrapper( parsed_result, - handler, invocation, capture_content, ) @@ -64,12 +63,10 @@ def traced_method(wrapped, instance, args, kwargs): _set_invocation_response_attributes( invocation, parsed_result, capture_content ) - handler.stop_llm(invocation) + invocation.stop() return result except Exception as error: - handler.fail_llm( - invocation, Error(type=type(error), message=str(error)) - ) + invocation.fail(Error(type=type(error), message=str(error))) raise return traced_method @@ -84,11 +81,10 @@ def async_responses_create( capture_content = content_capturing_mode != ContentCapturingMode.NO_CONTENT async def traced_method(wrapped, instance, args, kwargs): - invocation = handler.start_llm( - create_response_invocation( - kwargs, instance, capture_content=capture_content - ) + invocation = handler.start_inference( + **_get_inference_creation_kwargs(kwargs, instance) ) + _apply_request_attributes(invocation, kwargs, capture_content) try: result = await wrapped(*args, **kwargs) @@ -97,7 +93,6 @@ async def traced_method(wrapped, instance, args, kwargs): if is_streaming(kwargs): return AsyncResponseStreamWrapper( parsed_result, - handler, invocation, capture_content, ) @@ -105,12 +100,10 @@ async def traced_method(wrapped, instance, args, kwargs): _set_invocation_response_attributes( invocation, parsed_result, capture_content ) - handler.stop_llm(invocation) + invocation.stop() return result except Exception as error: - handler.fail_llm( - invocation, Error(type=type(error), message=str(error)) - ) + invocation.fail(Error(type=type(error), message=str(error))) raise return traced_method @@ -122,75 +115,3 @@ def _get_response_stream_result(result): return result -def _record_metrics( - instruments: Instruments, - duration: float, - result, - request_attributes: dict, - error_type: Optional[str], -): - common_attributes = { - GenAIAttributes.GEN_AI_OPERATION_NAME: ( - GenAIAttributes.GenAiOperationNameValues.CHAT.value - ), - GenAIAttributes.GEN_AI_SYSTEM: ( - GenAIAttributes.GenAiSystemValues.OPENAI.value - ), - GenAIAttributes.GEN_AI_REQUEST_MODEL: request_attributes[ - GenAIAttributes.GEN_AI_REQUEST_MODEL - ], - } - - 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 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): - input_tokens = getattr(result.usage, "input_tokens", None) - output_tokens = getattr(result.usage, "output_tokens", None) - - if input_tokens is not None: - input_attributes = { - **common_attributes, - GenAIAttributes.GEN_AI_TOKEN_TYPE: ( - GenAIAttributes.GenAiTokenTypeValues.INPUT.value - ), - } - instruments.token_usage_histogram.record( - input_tokens, - attributes=input_attributes, - ) - - if output_tokens is not None: - output_attributes = { - **common_attributes, - GenAIAttributes.GEN_AI_TOKEN_TYPE: ( - GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value - ), - } - instruments.token_usage_histogram.record( - output_tokens, - attributes=output_attributes, - ) 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 index de244a3634..858b409171 100644 --- 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 @@ -60,7 +60,7 @@ OutputMessage, Reasoning, Text, - ToolCall, + ToolCallRequest as ToolCall, ) except ImportError: InputMessage = None @@ -514,21 +514,32 @@ def _get_request_attributes( return {key: value for key, value in attributes.items() if value_is_set(value)} -def _create_invocation( +def _get_inference_creation_kwargs( kwargs: Mapping[str, object], client_instance: object, - capture_content: bool, -) -> "LLMInvocation": - if LLMInvocation is None: - raise RuntimeError("GenAI LLMInvocation type is unavailable") - +) -> dict[str, object]: request = _validate_request_kwargs(kwargs) request_model = request.model if request is not None else None + address, port = get_server_address_and_port(client_instance) + + creation_kwargs: dict[str, object] = { + "provider": GenAIAttributes.GenAiProviderNameValues.OPENAI.value, + } + if request_model is not None: + creation_kwargs["request_model"] = request_model + if address is not None: + creation_kwargs["server_address"] = address + if port is not None: + creation_kwargs["server_port"] = port + return creation_kwargs - invocation = LLMInvocation( - request_model=request_model, - provider=GenAIAttributes.GenAiProviderNameValues.OPENAI.value, - ) + +def _apply_request_attributes( + invocation, + kwargs: Mapping[str, object], + capture_content: bool, +) -> None: + request = _validate_request_kwargs(kwargs) if request is not None: invocation.temperature = request.temperature @@ -547,20 +558,12 @@ def _create_invocation( output_type ) - address, port = get_server_address_and_port(client_instance) - invocation.server_address = address - invocation.server_port = port - if capture_content: invocation.system_instruction = _extract_system_instruction(kwargs) invocation.input_messages = _extract_input_messages(kwargs) - return invocation - -def _set_invocation_usage_attributes( - invocation: "LLMInvocation", usage: _UsageModel -) -> None: +def _set_invocation_usage_attributes(invocation, usage: _UsageModel) -> None: if usage.input_tokens is not None: invocation.input_tokens = usage.input_tokens else: @@ -591,7 +594,7 @@ def _set_invocation_usage_attributes( def _set_invocation_response_attributes( - invocation: "LLMInvocation", + invocation, result: object | None, capture_content: bool, ) -> None: 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 index 9230c3da8d..dd2afca70c 100644 --- 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 @@ -7,11 +7,7 @@ from types import TracebackType from typing import TYPE_CHECKING, Callable, Generator, Generic, TypeVar -from opentelemetry.util.genai.handler import TelemetryHandler -from opentelemetry.util.genai.types import ( - Error, - LLMInvocation, # pylint: disable=no-name-in-module # TODO: migrate to InferenceInvocation -) +from opentelemetry.util.genai.types import Error # OpenAI Responses internals are version-gated (added in openai>=1.66.0), so # pylint may not resolve them in all lint environments even though we guard @@ -72,7 +68,7 @@ def _set_response_attributes( - invocation: "LLMInvocation", + invocation, result: "ParsedResponse[TextFormatT] | Response | None", capture_content: bool, ) -> None: @@ -131,12 +127,10 @@ class ResponseStreamWrapper(Generic[TextFormatT]): def __init__( self, stream: "ResponseStream[TextFormatT]", - handler: TelemetryHandler, - invocation: "LLMInvocation", + invocation, capture_content: bool, ): self.stream = stream - self.handler = handler self.invocation = invocation self._capture_content = capture_content self._finalized = False @@ -215,17 +209,15 @@ def _stop( _set_response_attributes( self.invocation, result, self._capture_content ) - with self._safe_instrumentation("stop_llm"): - self.handler.stop_llm(self.invocation) + with self._safe_instrumentation("inference.stop"): + self.invocation.stop() self._finalized = True def _fail(self, message: str, error_type: type[BaseException]) -> None: if self._finalized: return - with self._safe_instrumentation("fail_llm"): - self.handler.fail_llm( - self.invocation, Error(message=message, type=error_type) - ) + with self._safe_instrumentation("inference.fail"): + self.invocation.fail(Error(message=message, type=error_type)) self._finalized = True @staticmethod @@ -281,12 +273,10 @@ class ResponseStreamManagerWrapper(Generic[TextFormatT]): def __init__( self, manager: "ResponseStreamManager[TextFormatT]", - handler: TelemetryHandler, - invocation: "LLMInvocation", + invocation, capture_content: bool, ): self._manager = manager - self._handler = handler self._invocation = invocation self._capture_content = capture_content self._stream_wrapper: ResponseStreamWrapper[TextFormatT] | None = None @@ -295,7 +285,6 @@ def __enter__(self) -> ResponseStreamWrapper[TextFormatT]: stream = self._manager.__enter__() self._stream_wrapper = ResponseStreamWrapper( stream, - self._handler, self._invocation, self._capture_content, ) @@ -406,12 +395,10 @@ class AsyncResponseStreamManagerWrapper(Generic[TextFormatT]): def __init__( self, manager: "AsyncResponseStreamManager[TextFormatT]", - handler: TelemetryHandler, - invocation: "LLMInvocation", + invocation, capture_content: bool, ): self._manager = manager - self._handler = handler self._invocation = invocation self._capture_content = capture_content self._stream_wrapper: ( @@ -422,7 +409,6 @@ async def __aenter__(self) -> AsyncResponseStreamWrapper[TextFormatT]: stream = await self._manager.__aenter__() self._stream_wrapper = AsyncResponseStreamWrapper( stream, - self._handler, self._invocation, self._capture_content, ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py index 3061a23f3a..14b8c740dd 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py @@ -58,62 +58,63 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): return self._suppressed -def _noop_stop_llm(invocation): - del invocation +def _noop_stop(): + return None -def _noop_fail_llm(invocation, error): - del invocation +def _noop_fail(error): del error def _make_wrapper(manager): - handler = SimpleNamespace() - invocation = SimpleNamespace(request_model=None) + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=_noop_fail, + ) return ResponseStreamManagerWrapper( manager=manager, - handler=handler, invocation=invocation, capture_content=False, ) -def _make_stream_wrapper(stream, handler=None): - if handler is None: - handler = SimpleNamespace( - stop_llm=_noop_stop_llm, - fail_llm=_noop_fail_llm, +def _make_stream_wrapper(stream, invocation=None): + if invocation is None: + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=_noop_fail, ) - invocation = SimpleNamespace(request_model=None) return ResponseStreamWrapper( stream=stream, - handler=handler, invocation=invocation, capture_content=False, ) def _make_async_manager_wrapper(manager): - handler = SimpleNamespace() - invocation = SimpleNamespace(request_model=None) + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=_noop_fail, + ) return AsyncResponseStreamManagerWrapper( manager=manager, - handler=handler, invocation=invocation, capture_content=False, ) -def _make_async_stream_wrapper(stream, handler=None): - if handler is None: - handler = SimpleNamespace( - stop_llm=_noop_stop_llm, - fail_llm=_noop_fail_llm, +def _make_async_stream_wrapper(stream, invocation=None): + if invocation is None: + invocation = SimpleNamespace( + request_model=None, + stop=_noop_stop, + fail=_noop_fail, ) - invocation = SimpleNamespace(request_model=None) return AsyncResponseStreamWrapper( stream=stream, - handler=handler, invocation=invocation, capture_content=False, ) @@ -371,13 +372,13 @@ async def test_async_stream_wrapper_processes_events_and_stops_on_completion(): wrapper.process_event = processed.append wrapper._stop = stopped.append - result = await anext(wrapper) + result = await wrapper.__anext__() assert result is event assert processed == [event] with pytest.raises(StopAsyncIteration): - await anext(wrapper) + await wrapper.__anext__() assert stopped == [None] @@ -416,7 +417,7 @@ def record_failure(message, error_type): wrapper._fail = record_failure with pytest.raises(ValueError, match="boom"): - await anext(wrapper) + await wrapper.__anext__() assert failures == [("boom", ValueError)] From b36205d26c835c51cf9d89f36a7be1322d41012d Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Sun, 19 Apr 2026 18:39:54 -0400 Subject: [PATCH 03/10] WIP: Adding test files and refined the missing parts. --- .../openai_v2/response_wrappers.py | 48 +++---------- ...ggregates_cache_tokens[content_mode0].yaml | 28 ++++---- ...onses_create_api_error[content_mode0].yaml | 14 ++-- ...responses_create_basic[content_mode0].yaml | 28 ++++---- ...reate_captures_content[content_mode0].yaml | 36 +++++----- ...ures_tool_call_content[content_mode0].yaml | 30 ++++---- ..._create_event_only_no_content_in_span.yaml | 30 ++++---- ...tation_error_swallowed[content_mode0].yaml | 40 +++++------ ...orts_reasoning_tokens[content_mode0].yaml} | 69 ++++++++++--------- ...ses_create_stop_reason[content_mode0].yaml | 34 ++++----- ...ream_propagation_error[content_mode0].yaml | 40 +++++------ ...onses_create_streaming[content_mode0].yaml | 40 +++++------ ...ggregates_cache_tokens[content_mode0].yaml | 40 +++++------ ...aming_captures_content[content_mode0].yaml | 38 +++++----- ...tes_response_attribute[content_mode0].yaml | 50 +++++++------- ...te_streaming_iteration[content_mode0].yaml | 51 +++++++------- ...reaming_user_exception[content_mode0].yaml | 61 ++++++++++------ ...ses_create_token_usage[content_mode0].yaml | 36 +++++----- ...create_with_all_params[content_mode0].yaml | 28 ++++---- ...te_with_content_shapes[content_mode0].yaml | 30 ++++---- ...content_span_unsampled[content_mode0].yaml | 30 ++++---- ...er_finalize_idempotent[content_mode0].yaml | 40 +++++------ .../tests/test_async_responses.py | 50 ++++++++++---- .../tests/test_responses.py | 50 ++++++++++---- 24 files changed, 487 insertions(+), 454 deletions(-) rename instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/{test_responses_create_captures_reasoning_content[content_mode0].yaml => test_responses_create_reports_reasoning_tokens[content_mode0].yaml} (59%) 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 index dd2afca70c..e13cc6a400 100644 --- 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 @@ -9,37 +9,6 @@ from opentelemetry.util.genai.types import Error -# OpenAI Responses internals are version-gated (added in openai>=1.66.0), so -# pylint may not resolve them in all lint environments even though we guard -# runtime usage with ImportError fallbacks below. -try: - from openai.lib.streaming.responses._events import ( # pylint: disable=no-name-in-module - ResponseCompletedEvent, - ) - from openai.types.responses import ( # pylint: disable=no-name-in-module - ResponseCreatedEvent, - ResponseErrorEvent, - ResponseFailedEvent, - ResponseIncompleteEvent, - ResponseInProgressEvent, - ) - - _RESPONSE_EVENTS_WITH_RESPONSE = ( - ResponseCreatedEvent, - ResponseInProgressEvent, - ResponseFailedEvent, - ResponseIncompleteEvent, - ResponseCompletedEvent, - ) -except ImportError: - ResponseCompletedEvent = None - ResponseCreatedEvent = None - ResponseErrorEvent = None - ResponseFailedEvent = None - ResponseIncompleteEvent = None - ResponseInProgressEvent = None - _RESPONSE_EVENTS_WITH_RESPONSE = () - try: from opentelemetry.instrumentation.openai_v2.response_extractors import ( # pylint: disable=no-name-in-module _set_invocation_response_attributes, @@ -235,21 +204,20 @@ def _safe_instrumentation(context: str) -> Generator[None, None, None]: def process_event(self, event: "ResponseStreamEvent[TextFormatT]") -> None: event_type = event.type - response: "ParsedResponse[TextFormatT] | Response | None" = None - - if isinstance(event, _RESPONSE_EVENTS_WITH_RESPONSE): - response = event.response + response: "ParsedResponse[TextFormatT] | Response | None" = getattr( + event, "response", None + ) if response and not self.invocation.request_model: model = response.model if model: self.invocation.request_model = model - if isinstance(event, ResponseCompletedEvent): + if event_type == "response.completed": self._stop(response) return - if isinstance(event, (ResponseFailedEvent, ResponseIncompleteEvent)): + if event_type in {"response.failed", "response.incomplete"}: with self._safe_instrumentation("response attribute extraction"): _set_response_attributes( self.invocation, response, self._capture_content @@ -257,9 +225,9 @@ def process_event(self, event: "ResponseStreamEvent[TextFormatT]") -> None: self._fail(event_type, RuntimeError) return - if isinstance(event, ResponseErrorEvent): - error_type = event.code or "response.error" - message = event.message or error_type + if event_type == "response.error": + error_type = getattr(event, "code", None) or "response.error" + message = getattr(event, "message", None) or error_type self._fail(message, RuntimeError) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml index ec42e9a4c8..5f683f2566 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_aggregates_cache_tokens[content_mode0].yaml @@ -47,15 +47,15 @@ interactions: body: string: |- { - "id": "resp_010703fa494eff1b0069d85fdc796481a28f22f0d52e98cb86", + "id": "resp_0a8171a24e10956f0069e2f3f03a4c819da6eca7c5d387a3b8", "object": "response", - "created_at": 1775787996, + "created_at": 1776481264, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775787997, + "completed_at": 1776481265, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, @@ -65,7 +65,7 @@ interactions: "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "msg_010703fa494eff1b0069d85fddb59481a2b130ddbea671fff4", + "id": "msg_0a8171a24e10956f0069e2f3f0f640819d9b572b43c703a798", "type": "message", "status": "completed", "content": [ @@ -83,7 +83,7 @@ interactions: "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, - "prompt_cache_retention": null, + "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null @@ -119,13 +119,13 @@ interactions: } headers: CF-RAY: - - 9e9e4ec1bd558c6d-EWR + - 9ee06c3d2ae3f2f9-EWR Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:26:37 GMT + - Sat, 18 Apr 2026 03:01:05 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -140,18 +140,18 @@ interactions: cf-cache-status: - DYNAMIC content-length: - - '1568' + - '1575' openai-organization: test_openai_org_id openai-processing-ms: - - '1397' + - '917' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=UKb371H39UgnWj6PgjEmWx.OBnfq9XxNpaLdTC6fFhA-1775787996.4343424-1.0.1.1-oUJCBytz8nRgzMoToAAArAixTo3VhLjSwMR9D.gyYqYQV6EIcYFljEj4pgV0vcEJaclwmxGpzQ1NwUa5N5_Wbt0wfqk.xhZRgOkYyB7Z2STM4qfew0YZXSgx69U0yztK; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:37 GMT + - __cf_bm=nawHaNe2Gcjnf3PnJWOe7eCMcuL4GfmHJjNVPemFYOE-1776481264.1903315-1.0.1.1-cmimAw8su.1UpRhI8dWFr6DhgPw2Z2sJi8cKNV_tHFMFijmHK3ND45DjgKsf_ur_EZexZIAN.O__1bqzwnC_acPUsfxzivCaUIWeL1ThD2hWwU2mRBTfDdUDbR04KBsW; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:05 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: @@ -161,11 +161,11 @@ interactions: x-ratelimit-remaining-tokens: - '199959' x-ratelimit-reset-requests: - - 33.459s + - 34.066s x-ratelimit-reset-tokens: - 12ms x-request-id: - - req_463bfcd591944e8b92af0b183b9ad8f2 + - req_07ac89c9040840a6be685694eacd3288 status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml index 0abea51d06..2cf338a501 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_api_error[content_mode0].yaml @@ -55,7 +55,7 @@ interactions: } headers: CF-RAY: - - 9e9e4ee6c98f780c-EWR + - 9ee06c562f5c0f7d-EWR Connection: - keep-alive Content-Length: @@ -63,7 +63,7 @@ interactions: Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:26:42 GMT + - Sat, 18 Apr 2026 03:01:08 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -77,17 +77,17 @@ interactions: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - - '78' + - '107' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=CsRql7dORYmp6jh3htZAm_m0RErYeE1U_PlETalT3QQ-1775788002.3668075-1.0.1.1-aur_w.sUFC8IJwwMM6gXZkWGPoxg7q_jyVIbIkxGi5IqIrU3.CMkvupgsED_7sTuqhp81oRru4_SDZh_J1d6vmJInETx3alIpHCrR5UWJ0vvkhAkJwxLGnZ3AsfrKIoF; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:42 GMT + - __cf_bm=sImUBrv0bwxemWtgUZK8rUGOArohh1idXm1s6ve.9PM-1776481268.1848395-1.0.1.1-wfALia92HJtSuU5STTxRZGW0Cd1tZtPVOpbKTSFoiJLjLOcwVrsTA4C4j2yaYyhAqXvYCG9Ns.EIIhy5ZQv_PNuB11_p2HjTGRNJGtclgs_deByUIWyOSuE2JuY9zxD.; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:08 GMT x-request-id: - - req_f337a313a905474498b8d6f35de72475 + - req_48f6dc739d654972b34e069edb97d2dc status: code: 400 message: Bad Request diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml index f77651fcca..c821fcee85 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_basic[content_mode0].yaml @@ -48,15 +48,15 @@ interactions: body: string: |- { - "id": "resp_04caba33425205fa0069d85fd2ab088197960cb41a20e67147", + "id": "resp_0f4faba17dcd0f1e0069e2f3e4907881909179832ba1237025", "object": "response", - "created_at": 1775787986, + "created_at": 1776481253, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775787987, + "completed_at": 1776481257, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, @@ -66,7 +66,7 @@ interactions: "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "msg_04caba33425205fa0069d85fd3e67481979fda2421b1fba85e", + "id": "msg_0f4faba17dcd0f1e0069e2f3e7b2b88190bff23981628ac362", "type": "message", "status": "completed", "content": [ @@ -84,7 +84,7 @@ interactions: "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, - "prompt_cache_retention": null, + "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null @@ -120,13 +120,13 @@ interactions: } headers: CF-RAY: - - 9e9e4e7ed9ef1f47-EWR + - 9ee06be8ab20d481-EWR Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:26:28 GMT + - Sat, 18 Apr 2026 03:00:58 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -141,18 +141,18 @@ interactions: cf-cache-status: - DYNAMIC content-length: - - '1568' + - '1575' openai-organization: test_openai_org_id openai-processing-ms: - - '1403' + - '7177' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=kg4qtbr35BLD5Mbw.m8kinlLNbYtS5wdwYcXz490hG4-1775787985.7323968-1.0.1.1-HOHGUhKSe8jI.TJjOKssznv1hyW7k6XG3QVyKQ0pROkpfCo.kp.LVJmzFXw6ZZKQzj2z9PYPWTsY9MopqcEOgG4hqe.xx_L0i8u_CLSHHaimvq_4hcS5uJNCd56oxN59; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:28 GMT + - __cf_bm=ckwOJyTtv0EpPspvwSIW3P1dXrhIU1GtdL9BNBJlNbA-1776481250.670554-1.0.1.1-7j2fYPA6kw.SMgF3tYMODcA6O.P9Xwu6N6jolKR9OG0AJczT7a1Jr5HlRnJ6T.tXJ5HYV_jtosjzUCBM1yHZTiRTwGTtu6Klyq1ldSG5hFV4MRK9StK_zmCKjmIbqO9d; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:30:58 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: @@ -160,13 +160,13 @@ interactions: x-ratelimit-remaining-requests: - '9999' x-ratelimit-remaining-tokens: - - '199959' + - '199958' x-ratelimit-reset-requests: - 8.64s x-ratelimit-reset-tokens: - 12ms x-request-id: - - req_838fd6c357d14955aecc16fbafcd81d9 + - req_d6009916bd4447188ce5baaea187709b status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml index 4f857dcec0..c1d95130d0 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_content[content_mode0].yaml @@ -53,15 +53,15 @@ interactions: body: string: |- { - "id": "resp_01ebc5c056a2e1aa0069d85fd620e8819c9e300d5b06537da2", + "id": "resp_0d6d73520bd9405c0069e2f3eb1108819590a9e98dba6a746d", "object": "response", - "created_at": 1775787990, + "created_at": 1776481259, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775787991, + "completed_at": 1776481260, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, @@ -71,7 +71,7 @@ interactions: "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "msg_01ebc5c056a2e1aa0069d85fd772c0819c91006372be0ccfa1", + "id": "msg_0d6d73520bd9405c0069e2f3eba7f081958368e178f0078326", "type": "message", "status": "completed", "content": [ @@ -79,7 +79,7 @@ interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "This is a test." + "text": "This is a test. How can I assist you further?" } ], "role": "assistant" @@ -89,7 +89,7 @@ interactions: "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, - "prompt_cache_retention": null, + "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null @@ -114,24 +114,24 @@ interactions: "input_tokens_details": { "cached_tokens": 0 }, - "output_tokens": 6, + "output_tokens": 13, "output_tokens_details": { "reasoning_tokens": 0 }, - "total_tokens": 28 + "total_tokens": 35 }, "user": null, "metadata": {} } headers: CF-RAY: - - 9e9e4e900b63acc5-EWR + - 9ee06c1ccc4b0f93-EWR Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:26:31 GMT + - Sat, 18 Apr 2026 03:01:00 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -146,18 +146,18 @@ interactions: cf-cache-status: - DYNAMIC content-length: - - '1568' + - '1606' openai-organization: test_openai_org_id openai-processing-ms: - - '1483' + - '1097' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=NnpQnSwjhbSJqk0O6xoIzBRYf8JfdyiD1_LCi1De5Cg-1775787988.488219-1.0.1.1-f57TtdxSDxGhM6_a46bjUxdogyVqJHoU5WHgDmAF2pBs3vzla2D3BLKlV1C0cpPByaaRZP9Zii3E7emk9zrkQLSgPEbM3jiGIxzuW2pXVQO.PdHugSY7PNK9eIq89TL3; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:31 GMT + - __cf_bm=eRjb5xrZMpFJHlBLeAzQEHihWjr.Mb5bNSgOGzGSnwA-1776481259.0045145-1.0.1.1-fAERlGPpEWHNuG0MiIcvmzII8t4Xj7yC_HgjLktqdKXPJmbHLtPtT_beeOd8sCcZVQYjGOH0QcF0w4Hz9S.t29trpZuNiezPTQNuyMSy51F2kKYkypy9Eromd2kQp9Cj; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:00 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: @@ -165,13 +165,13 @@ interactions: x-ratelimit-remaining-requests: - '9998' x-ratelimit-remaining-tokens: - - '199959' + - '199958' x-ratelimit-reset-requests: - - 13.802s + - 13.065s x-ratelimit-reset-tokens: - 12ms x-request-id: - - req_08136e915de245d3a318e4604db8aae5 + - req_9191bbf984a54a3281f8f335b87e7aeb status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml index 00f22123c6..3978b0a80c 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_tool_call_content[content_mode0].yaml @@ -71,15 +71,15 @@ interactions: body: string: |- { - "id": "resp_0cb296f9e5607fd60069d85fef5a9c8190b9a1023b905723b8", + "id": "resp_0bedf6e1ffba28050069e2f401ae1c8196be360fd5993c96de", "object": "response", - "created_at": 1775788015, + "created_at": 1776481281, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775788015, + "completed_at": 1776481282, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, @@ -89,11 +89,11 @@ interactions: "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "fc_0cb296f9e5607fd60069d85fefc01c8190b80847c9f8782620", + "id": "fc_0bedf6e1ffba28050069e2f402203c8196b45065342d00ed17", "type": "function_call", "status": "completed", "arguments": "{\"location\":\"Seattle, WA\"}", - "call_id": "call_9JGnHg3Qns7ZqDfSVNhAEArE", + "call_id": "call_90uO5LcGP5vTBTCrjyhYtWsA", "name": "get_current_weather" } ], @@ -101,7 +101,7 @@ interactions: "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, - "prompt_cache_retention": null, + "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null @@ -160,13 +160,13 @@ interactions: } headers: CF-RAY: - - 9e9e4f379c69ccb6-EWR + - 9ee06caa3d06c745-EWR Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:26:56 GMT + - Sat, 18 Apr 2026 03:01:22 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -181,18 +181,18 @@ interactions: cf-cache-status: - DYNAMIC content-length: - - '2027' + - '2034' openai-organization: test_openai_org_id openai-processing-ms: - - '674' + - '713' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=br01RxGo5SXW00_woYY5L7MyEcOiE8dmdgiNFie.oTo-1775788015.2997324-1.0.1.1-RbN016RUGisBdZ1wBbRYnN_LIzdDghSFytfdtUSHT7hKp09BZqJYvJKB_EN.HojlHyCtS25A2c9KRzLF2X1nk7yQHndMvs2VjtElBF_G5TaBgehj6B4Hu.uWXleelGGa; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:56 GMT + - __cf_bm=77TblxQ40FwSWcmbiooIxTpBbJWtYa4Sa8xnnuOb8P4-1776481281.6342883-1.0.1.1-wYyCi1iDQ3bB4g5c2Jj1pldvzLmLS9NtJ1ryP99rJkxwR77VRG3qXvTHSKzUAQk9LBvwdBumDx4dWSKvmBqXsb7K2O8zcH3uxfGFCw4SLKIaQIsDy2SGcb9JuoVZMe4r; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:22 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: @@ -202,11 +202,11 @@ interactions: x-ratelimit-remaining-tokens: - '199711' x-ratelimit-reset-requests: - - 1m50.441s + - 1m51.595s x-ratelimit-reset-tokens: - 86ms x-request-id: - - req_c096da9861fc4615bae077d9124c49ac + - req_66f20a8d196540e7928ba09ef1baad00 status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml index b408e0a79e..729f09d913 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_event_only_no_content_in_span.yaml @@ -48,15 +48,15 @@ interactions: body: string: |- { - "id": "resp_07cbd7936ceac8510069d85fff0f6c8192b4b33eb68b420ce2", + "id": "resp_0da562f2b64fdb7e0069e2f40b28e081909487104de4d4c8c0", "object": "response", - "created_at": 1775788031, + "created_at": 1776481291, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775788032, + "completed_at": 1776481291, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, @@ -66,7 +66,7 @@ interactions: "model": "gpt-4o-mini-2024-07-18", "output": [ { - "id": "msg_07cbd7936ceac8510069d860005f348192b6be28a796292e6d", + "id": "msg_0da562f2b64fdb7e0069e2f40ba8bc8190a803f1b565d49c7c", "type": "message", "status": "completed", "content": [ @@ -84,7 +84,7 @@ interactions: "presence_penalty": 0.0, "previous_response_id": null, "prompt_cache_key": null, - "prompt_cache_retention": null, + "prompt_cache_retention": "in_memory", "reasoning": { "effort": null, "summary": null @@ -120,13 +120,13 @@ interactions: } headers: CF-RAY: - - 9e9e4f99989843c2-EWR + - 9ee06ce2aead41b2-EWR Connection: - keep-alive Content-Type: - application/json Date: - - Fri, 10 Apr 2026 02:27:12 GMT + - Sat, 18 Apr 2026 03:01:31 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -141,32 +141,32 @@ interactions: cf-cache-status: - DYNAMIC content-length: - - '1568' + - '1575' openai-organization: test_openai_org_id openai-processing-ms: - - '1505' + - '698' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=o3UfucpdW2pEMxXq8x.tv492pSBhzEof3VOOyh4sc4Y-1775788030.9800544-1.0.1.1-u.YezuLAmHWQYIfnfWzLaHz.ZejG1RddVeKxwoukneeNkQhpS1t_NGLmlOeDV..l1AA4Y2rmPjUc2vxSjMBkQvAqbxompYQIT0AzG9jMN2pmovSrVkqWH4RXXf861BxL; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:57:12 GMT + - __cf_bm=5Y3wm6qm03P0G7_SigGuGob8rfE0X7eMfHOJDLR_E5o-1776481290.6632602-1.0.1.1-2QRz.JRmnCza_Xtkyqwgj1IPTwrSSA5LHyD9chhvKiCoHNE9_.I5_0qopgAV60mriRXdX99CCxozoio7VqvW.wnvklHzb6gwZDEsQCAlBqZGcIHrBfFk4jkvspj4sMO0; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:31 GMT x-ratelimit-limit-requests: - '10000' x-ratelimit-limit-tokens: - '200000' x-ratelimit-remaining-requests: - - '9986' + - '9985' x-ratelimit-remaining-tokens: - '199959' x-ratelimit-reset-requests: - - 1m59.732s + - 2m7.93s x-ratelimit-reset-tokens: - 12ms x-request-id: - - req_1d6a9bc8820a4d2998bfbb41ad108b2d + - req_76d5ef520a6b4d74975dae1ade822de9 status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml index 36dab78d58..12dd8a9564 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_instrumentation_error_swallowed[content_mode0].yaml @@ -48,53 +48,53 @@ interactions: body: string: |+ event: response.created - data: {"type":"response.created","response":{"id":"resp_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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} + data: {"type":"response.created","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","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_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","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} + data: {"type":"response.in_progress","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":"You are a helpful assistant.","max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + data: {"type":"response.output_item.added","item":{"id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"SIvcY5ssmJxE","output_index":0,"sequence_number":4} + data: {"type":"response.output_text.delta","content_index":0,"delta":"This","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"hmEwhe4L8nm4","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"KSmXWZskD03gS","output_index":0,"sequence_number":5} + data: {"type":"response.output_text.delta","content_index":0,"delta":" is","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"u3Q5kHUIahzcI","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"FZ3nNox6Jl28SC","output_index":0,"sequence_number":6} + data: {"type":"response.output_text.delta","content_index":0,"delta":" a","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"0ZnBkOCdgrlbIT","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"0vqja7Hignb","output_index":0,"sequence_number":7} + data: {"type":"response.output_text.delta","content_index":0,"delta":" test","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"hYsQ5M0hWLs","output_index":0,"sequence_number":7} event: response.output_text.delta - data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"obfuscation":"n5dD8N0lDkoXOVT","output_index":0,"sequence_number":8} + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","logprobs":[],"obfuscation":"PyM68hgEwGGLkTN","output_index":0,"sequence_number":8} event: response.output_text.done - data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","logprobs":[],"output_index":0,"sequence_number":9,"text":"This is a test."} + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."},"sequence_number":10} + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"},"output_index":0,"sequence_number":11} + data: {"type":"response.output_item.done","item":{"id":"msg_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","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_068057300aa4d0800069d85fee44c081969648bdc230bbc2dd","object":"response","created_at":1775788014,"status":"completed","background":false,"completed_at":1775788014,"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_068057300aa4d0800069d85feeb3a48196a5041e94d1300a42","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":{}},"sequence_number":12} + data: {"type":"response.completed","response":{"id":"resp_052003e5c7cd2fe10069e2f400498c819f943227bcc94a63a6","object":"response","created_at":1776481280,"status":"completed","background":false,"completed_at":1776481280,"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_052003e5c7cd2fe10069e2f400ec70819f90c4df2a4b73f05c","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"This is a test."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":22,"input_tokens_details":{"cached_tokens":0},"output_tokens":6,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":28},"user":null,"metadata":{}},"sequence_number":12} headers: CF-RAY: - - 9e9e4f30ef8e8465-EWR + - 9ee06ca19ad7606a-EWR Connection: - keep-alive Content-Type: - text/event-stream; charset=utf-8 Date: - - Fri, 10 Apr 2026 02:26:54 GMT + - Sat, 18 Apr 2026 03:01:20 GMT Server: - cloudflare Set-Cookie: test_set_cookie @@ -110,17 +110,17 @@ interactions: - DYNAMIC openai-organization: test_openai_org_id openai-processing-ms: - - '67' + - '51' openai-project: - proj_s74VWObPgWXRchv2sHdrOTPY openai-version: - '2020-10-01' set-cookie: - - __cf_bm=G50wZW7_TYVatvs1P15l3XRevsXZA5l7Y6UT8cWKpD8-1775788014.228369-1.0.1.1-.KSdzyylckBVofZyP.ALr55Hk0kPWHo6_3ni1ADFkUmIbaIlOEPS89W1OEHCGeXmslq8UQT2XSaGFvqaF80kFrkvN9jwD2WRFElVtBDJsfSMj8wggSx1neTrIqo9pgGs; - HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 10 Apr 2026 - 02:56:54 GMT + - __cf_bm=zeme85udcp85MpYWX94kpho5aA4pRXRhhWbljEzlyKg-1776481280.2544615-1.0.1.1-DxHmuQ9bjw1.C_t_MZ23Az8WRay9CC2Wh0GbHPNN41zA5npmFYATDR2lI7uN061efp8b0vaq0viZms.aHHjkxiiNYFhewlDgEVLyFCa4F1mVx1iKbXQcmUJWbaXjFOtV; + HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Sat, 18 Apr 2026 + 03:31:20 GMT x-request-id: - - req_6b47baa659dd45859686e511d074943a + - req_4e7549325e3f4abaa4ac13b74667292e status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_reports_reasoning_tokens[content_mode0].yaml similarity index 59% rename from instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml rename to instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_reports_reasoning_tokens[content_mode0].yaml index f31cc34a46..11739e2d3e 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_captures_reasoning_content[content_mode0].yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/cassettes/test_responses_create_reports_reasoning_tokens[content_mode0].yaml @@ -2,10 +2,16 @@ interactions: - request: body: |- { - "input": "What is 17*19? Think first.", - "model": "gpt-5-mini", + "input": [ + { + "role": "user", + "content": "\nWrite a bash script that takes a matrix represented as a string with\nformat '[1,2],[3,4],[5,6]' and prints the transpose in the same format.\n" + } + ], + "max_output_tokens": 300, + "model": "gpt-5.4", "reasoning": { - "summary": "concise" + "effort": "low" } } headers: @@ -16,7 +22,7 @@ interactions: Connection: - keep-alive Content-Length: - - '100' + - '267' Content-Type: - application/json Host: @@ -40,7 +46,7 @@ interactions: authorization: - Bearer test_openai_api_key x-stainless-read-timeout: - - '600' + - '30.0' x-stainless-retry-count: - '0' method: POST @@ -49,30 +55,30 @@ interactions: body: string: |- { - "id": "resp_00ffaee8970a201e0069d85ff21eec8191b6400bb158fb42bf", + "id": "resp_05177a4994c7df3a0069e2f402f00881a1b9eda520cb779fef", "object": "response", - "created_at": 1775788019, + "created_at": 1776481282, "status": "completed", "background": false, "billing": { "payer": "developer" }, - "completed_at": 1775788028, + "completed_at": 1776481288, "error": null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions": null, - "max_output_tokens": null, + "max_output_tokens": 300, "max_tool_calls": null, - "model": "gpt-5-mini-2025-08-07", + "model": "gpt-5.4-2026-03-05", "output": [ { - "id": "rs_00ffaee8970a201e0069d85ff850688191b0eacdc13b1f4752", + "id": "rs_05177a4994c7df3a0069e2f403351c81a1ae31439661e8e976", "type": "reasoning", "summary": [] }, { - "id": "msg_00ffaee8970a201e0069d85ffc9fec819199bbeefde8316d62", + "id": "msg_05177a4994c7df3a0069e2f403633481a1a824dd7ecef75116", "type": "message", "status": "completed", "content": [ @@ -80,9 +86,10 @@ interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "17 \u00d7 19 = 323.\n\n(short method: 17\u00d719 = 17\u00d7(20\u22121) = 340 \u2212 17 = 323)" + "text": "```bash\n#!/usr/bin/env bash\n\n# Usage:\n# ./transpose.sh '[1,2],[3,4],[5,6]'\n\ninput=\"$1\"\n\n# Extract rows into an array\nIFS='|' read -r -a rows <<< \"$(echo \"$input\" | sed 's/],\\[/]|[/g')\"\n\n# Remove leading/trailing brackets and split first row to get column count\nfirst_row=\"${rows[0]#[}\"\nfirst_row=\"${first_row%]}\"\nIFS=',' read -r -a first_vals <<< \"$first_row\"\ncols=\"${#first_vals[@]}\"\n\n# Build transpose\nresult=\"\"\nfor ((c=0; c 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + (span,) = spans + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == ( + REASONING_MODEL ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py index be2ab1d90f..4e18cfdb15 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -16,7 +16,7 @@ import json import pytest -from openai import APIConnectionError, NotFoundError, OpenAI +from openai import APIConnectionError, BadRequestError, NotFoundError, OpenAI from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor from opentelemetry.instrumentation.openai_v2.response_extractors import ( @@ -73,7 +73,11 @@ } ] INVALID_MODEL = "this-model-does-not-exist" -REASONING_MODEL = "gpt-5-mini" +REASONING_MODEL = "gpt-5.4" +REASONING_PROMPT = """ +Write a bash script that takes a matrix represented as a string with +format '[1,2],[3,4],[5,6]' and prints the transpose in the same format. +""" def _skip_if_not_latest(): @@ -387,7 +391,7 @@ def test_responses_create_api_error( _skip_if_not_latest() skip_if_cassette_missing_and_no_real_key(request) - with pytest.raises(NotFoundError): + with pytest.raises((BadRequestError, NotFoundError)) as exc_info: openai_client.responses.create( model=INVALID_MODEL, input="Hello", @@ -395,7 +399,10 @@ def test_responses_create_api_error( (span,) = span_exporter.get_finished_spans() assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL - assert span.attributes[ErrorAttributes.ERROR_TYPE] == "NotFoundError" + assert ( + span.attributes[ErrorAttributes.ERROR_TYPE] + == type(exc_info.value).__name__ + ) @pytest.mark.vcr() @@ -697,26 +704,39 @@ def test_responses_create_captures_tool_call_content( "openai SDK too old to support 'reasoning' parameter on Responses.create" ), ) -def test_responses_create_captures_reasoning_content( +def test_responses_create_reports_reasoning_tokens( request, span_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() skip_if_cassette_missing_and_no_real_key(request) - openai_client.responses.create( + response = openai_client.responses.create( model=REASONING_MODEL, - input="What is 17*19? Think first.", - reasoning={"summary": "concise"}, + reasoning={"effort": "low"}, + input=[ + { + "role": "user", + "content": REASONING_PROMPT, + } + ], + max_output_tokens=300, + timeout=30.0, ) - (span,) = span_exporter.get_finished_spans() - output_messages = _load_span_messages( - span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + reasoning_tokens = getattr( + getattr(response.usage, "output_tokens_details", None), + "reasoning_tokens", + None, ) - assert any( - part.get("type") == "reasoning" - for message in output_messages - for part in message.get("parts", []) + + assert reasoning_tokens is not None + assert reasoning_tokens > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + (span,) = spans + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == ( + REASONING_MODEL ) From 0b241e3c762409e73a411f6df482dc5354856673 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Sun, 19 Apr 2026 18:56:20 -0400 Subject: [PATCH 04/10] WIP: Moving cache assertions to common utils file. --- .../tests/test_async_responses.py | 41 ++----------------- .../tests/test_responses.py | 41 ++----------------- .../tests/test_utils.py | 36 ++++++++++++++++ 3 files changed, 42 insertions(+), 76 deletions(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py index d4d4b71a6a..7a8f9652ad 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py @@ -19,10 +19,6 @@ from openai import APIConnectionError, AsyncOpenAI, BadRequestError, NotFoundError from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor -from opentelemetry.instrumentation.openai_v2.response_extractors import ( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, -) from opentelemetry.instrumentation.openai_v2.response_wrappers import ( AsyncResponseStreamWrapper, ) @@ -42,6 +38,7 @@ USER_ONLY_EXPECTED_INPUT_MESSAGES, USER_ONLY_PROMPT, assert_all_attributes, + assert_cache_attributes, assert_messages_attribute, format_simple_expected_output_message, get_responses_weather_tool_definition, @@ -141,38 +138,6 @@ async def _collect_completed_response(stream): return response -def _get_usage_details(usage): - return getattr(usage, "input_tokens_details", None) or getattr( - usage, "prompt_tokens_details", None - ) - - -def _assert_cache_attributes(span, usage): - details = _get_usage_details(usage) - assert details is not None - - cached_tokens = getattr(details, "cached_tokens", None) - if cached_tokens is None: - assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes - else: - assert ( - span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] - == cached_tokens - ) - - cache_creation = getattr(details, "cache_creation_input_tokens", None) - if cache_creation is None: - assert ( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS - not in span.attributes - ) - else: - assert ( - span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] - == cache_creation - ) - - def test_async_responses_uninstrument_removes_patching( span_exporter, tracer_provider, logger_provider, meter_provider ): @@ -352,7 +317,7 @@ async def test_async_responses_create_aggregates_cache_tokens( ) (span,) = span_exporter.get_finished_spans() - _assert_cache_attributes(span, response.usage) + assert_cache_attributes(span, response.usage) @pytest.mark.asyncio() @@ -469,7 +434,7 @@ async def test_async_responses_create_streaming_aggregates_cache_tokens( response = await _collect_completed_response(stream) (span,) = span_exporter.get_finished_spans() - _assert_cache_attributes(span, response.usage) + assert_cache_attributes(span, response.usage) @pytest.mark.asyncio() diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py index 4e18cfdb15..9351dc0da1 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -19,10 +19,6 @@ from openai import APIConnectionError, BadRequestError, NotFoundError, OpenAI from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor -from opentelemetry.instrumentation.openai_v2.response_extractors import ( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, - GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, -) from opentelemetry.instrumentation.openai_v2.response_wrappers import ( ResponseStreamWrapper, ) @@ -42,6 +38,7 @@ USER_ONLY_EXPECTED_INPUT_MESSAGES, USER_ONLY_PROMPT, assert_all_attributes, + assert_cache_attributes, assert_messages_attribute, format_simple_expected_output_message, get_responses_weather_tool_definition, @@ -141,38 +138,6 @@ def _collect_completed_response(stream): return response -def _get_usage_details(usage): - return getattr(usage, "input_tokens_details", None) or getattr( - usage, "prompt_tokens_details", None - ) - - -def _assert_cache_attributes(span, usage): - details = _get_usage_details(usage) - assert details is not None - - cached_tokens = getattr(details, "cached_tokens", None) - if cached_tokens is None: - assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes - else: - assert ( - span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] - == cached_tokens - ) - - cache_creation = getattr(details, "cache_creation_input_tokens", None) - if cache_creation is None: - assert ( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS - not in span.attributes - ) - else: - assert ( - span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] - == cache_creation - ) - - def test_responses_uninstrument_removes_patching( span_exporter, tracer_provider, logger_provider, meter_provider ): @@ -343,7 +308,7 @@ def test_responses_create_aggregates_cache_tokens( ) (span,) = span_exporter.get_finished_spans() - _assert_cache_attributes(span, response.usage) + assert_cache_attributes(span, response.usage) @pytest.mark.vcr() @@ -451,7 +416,7 @@ def test_responses_create_streaming_aggregates_cache_tokens( response = _collect_completed_response(stream) (span,) = span_exporter.get_finished_spans() - _assert_cache_attributes(span, response.usage) + assert_cache_attributes(span, response.usage) @pytest.mark.vcr() diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index 84ca728e1c..db9b555911 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -21,6 +21,10 @@ import pytest +from opentelemetry.instrumentation.openai_v2.response_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, +) from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, @@ -268,6 +272,38 @@ def format_simple_expected_output_message( ] +def _get_usage_details(usage): + return getattr(usage, "input_tokens_details", None) or getattr( + usage, "prompt_tokens_details", None + ) + + +def assert_cache_attributes(span, usage): + details = _get_usage_details(usage) + assert details is not None + + cached_tokens = getattr(details, "cached_tokens", None) + if cached_tokens is None: + assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS not in span.attributes + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] + == cached_tokens + ) + + cache_creation = getattr(details, "cache_creation_input_tokens", None) + if cache_creation is None: + assert ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS + not in span.attributes + ) + else: + assert ( + span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] + == cache_creation + ) + + def assert_message_in_logs(log, event_name, expected_content, parent_span): assert log.log_record.event_name == event_name assert ( From 9dcb02130910cec6e7f5cde0db0ee4562745719c Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Sun, 19 Apr 2026 19:00:02 -0400 Subject: [PATCH 05/10] WIP: removing the async create method instrumentation. --- .../instrumentation/openai_v2/__init__.py | 8 - .../openai_v2/patch_responses.py | 41 +- .../tests/test_async_responses.py | 842 ------------------ 3 files changed, 1 insertion(+), 890 deletions(-) delete mode 100644 instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py 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 82405e4498..f9d46301e4 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 @@ -72,7 +72,6 @@ embeddings_create, ) from .patch_responses import ( - async_responses_create, responses_create, ) @@ -172,12 +171,6 @@ def _instrument(self, **kwargs): wrapper=responses_create(handler, content_mode), ) - wrap_function_wrapper( - module="openai.resources.responses.responses", - name="AsyncResponses.create", - wrapper=async_responses_create(handler, content_mode), - ) - def _uninstrument(self, **kwargs): import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415 @@ -188,7 +181,6 @@ def _uninstrument(self, **kwargs): responses_module = _get_responses_module() if responses_module is not None: unwrap(responses_module.Responses, "create") - unwrap(responses_module.AsyncResponses, "create") def _get_responses_module(): 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 index 4617b5efbd..0f319762cc 100644 --- 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 @@ -31,7 +31,7 @@ _get_inference_creation_kwargs, _set_invocation_response_attributes, ) -from .response_wrappers import AsyncResponseStreamWrapper, ResponseStreamWrapper +from .response_wrappers import ResponseStreamWrapper from .utils import is_streaming @@ -72,46 +72,7 @@ def traced_method(wrapped, instance, args, kwargs): return traced_method -def async_responses_create( - handler: TelemetryHandler, - content_capturing_mode: ContentCapturingMode, -): - """Wrap the `create` method of the `AsyncResponses` class to trace it.""" - - capture_content = content_capturing_mode != ContentCapturingMode.NO_CONTENT - - async def traced_method(wrapped, instance, args, kwargs): - invocation = handler.start_inference( - **_get_inference_creation_kwargs(kwargs, instance) - ) - _apply_request_attributes(invocation, kwargs, capture_content) - - try: - result = await wrapped(*args, **kwargs) - parsed_result = _get_response_stream_result(result) - - if is_streaming(kwargs): - return AsyncResponseStreamWrapper( - parsed_result, - invocation, - capture_content, - ) - - _set_invocation_response_attributes( - invocation, parsed_result, capture_content - ) - invocation.stop() - return result - except Exception as error: - invocation.fail(Error(type=type(error), message=str(error))) - raise - - return traced_method - - def _get_response_stream_result(result): if hasattr(result, "parse"): return result.parse() return result - - diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py deleted file mode 100644 index 7a8f9652ad..0000000000 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_async_responses.py +++ /dev/null @@ -1,842 +0,0 @@ -# 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 inspect -import json - -import pytest -from openai import APIConnectionError, AsyncOpenAI, BadRequestError, NotFoundError - -from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor -from opentelemetry.instrumentation.openai_v2.response_wrappers import ( - AsyncResponseStreamWrapper, -) -from opentelemetry.semconv._incubating.attributes import ( - error_attributes as ErrorAttributes, -) -from opentelemetry.semconv._incubating.attributes import ( - gen_ai_attributes as GenAIAttributes, -) -from opentelemetry.semconv._incubating.attributes import ( - server_attributes as ServerAttributes, -) -from opentelemetry.util.genai.utils import is_experimental_mode - -from .test_utils import ( - DEFAULT_MODEL, - USER_ONLY_EXPECTED_INPUT_MESSAGES, - USER_ONLY_PROMPT, - assert_all_attributes, - assert_cache_attributes, - assert_messages_attribute, - format_simple_expected_output_message, - get_responses_weather_tool_definition, - skip_if_cassette_missing_and_no_real_key, -) - -try: - from openai.resources.responses.responses import AsyncResponses as _AsyncResponses - - HAS_RESPONSES_API = True - _create_params = set(inspect.signature(_AsyncResponses.create).parameters) - _has_tools_param = "tools" in _create_params - _has_reasoning_param = "reasoning" in _create_params -except ImportError: - HAS_RESPONSES_API = False - _has_tools_param = False - _has_reasoning_param = False - - -pytestmark = pytest.mark.skipif( - not HAS_RESPONSES_API, reason="Responses API requires a newer openai SDK" -) - -SYSTEM_INSTRUCTIONS = "You are a helpful assistant." -EXPECTED_SYSTEM_INSTRUCTIONS = [ - { - "type": "text", - "content": SYSTEM_INSTRUCTIONS, - } -] -INVALID_MODEL = "this-model-does-not-exist" -REASONING_MODEL = "gpt-5.4" -REASONING_PROMPT = """ -Write a bash script that takes a matrix represented as a string with -format '[1,2],[3,4],[5,6]' and prints the transpose in the same format. -""" - - -def _skip_if_not_latest(): - if not is_experimental_mode(): - pytest.skip( - "Responses create instrumentation only supports the latest experimental semconv path" - ) - - -def _load_span_messages(span, attribute): - value = span.attributes.get(attribute) - assert value is not None - return json.loads(value) - - -def _assert_response_content(span, response, log_exporter): - assert_messages_attribute( - span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES], - USER_ONLY_EXPECTED_INPUT_MESSAGES, - ) - assert json.loads( - span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS] - ) == EXPECTED_SYSTEM_INSTRUCTIONS - assert_messages_attribute( - span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES], - format_simple_expected_output_message(response.output_text), - ) - assert len(log_exporter.get_finished_logs()) == 0 - - -def _assert_request_attrs( - span, - *, - temperature=None, - top_p=None, - max_tokens=None, - output_type=None, -): - if temperature is not None: - assert ( - span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] - == temperature - ) - if top_p is not None: - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == top_p - if max_tokens is not None: - assert ( - span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] - == max_tokens - ) - if output_type is not None: - assert span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type - - -async def _collect_completed_response(stream): - response = None - async for event in stream: - if event.type == "response.completed": - response = event.response - assert response is not None - return response - - -def test_async_responses_uninstrument_removes_patching( - span_exporter, tracer_provider, logger_provider, meter_provider -): - instrumentor = OpenAIInstrumentor() - instrumentor.instrument( - tracer_provider=tracer_provider, - logger_provider=logger_provider, - meter_provider=meter_provider, - ) - instrumentor.uninstrument() - - assert len(span_exporter.get_finished_spans()) == 0 - - -def test_async_responses_multiple_instrument_uninstrument_cycles( - tracer_provider, logger_provider, meter_provider -): - instrumentor = OpenAIInstrumentor() - - instrumentor.instrument( - tracer_provider=tracer_provider, - logger_provider=logger_provider, - meter_provider=meter_provider, - ) - instrumentor.uninstrument() - - instrumentor.instrument( - tracer_provider=tracer_provider, - logger_provider=logger_provider, - meter_provider=meter_provider, - ) - instrumentor.uninstrument() - - instrumentor.instrument( - tracer_provider=tracer_provider, - logger_provider=logger_provider, - meter_provider=meter_provider, - ) - instrumentor.uninstrument() - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_basic( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=False, - ) - - (span,) = span_exporter.get_finished_spans() - assert_all_attributes( - span, - DEFAULT_MODEL, - True, - response.id, - response.model, - response.usage.input_tokens, - response.usage.output_tokens, - response_service_tier=getattr(response, "service_tier", None), - ) - assert ( - span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] - == ("stop",) - ) - assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes - assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_captures_content( - request, - span_exporter, - log_exporter, - async_openai_client, - instrument_with_content, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=False, - text={"format": {"type": "text"}}, - ) - - (span,) = span_exporter.get_finished_spans() - _assert_response_content(span, response, log_exporter) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_with_all_params( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - max_output_tokens=50, - temperature=0.7, - top_p=0.9, - service_tier="default", - text={"format": {"type": "text"}}, - ) - - (span,) = span_exporter.get_finished_spans() - assert_all_attributes( - span, - DEFAULT_MODEL, - True, - response.id, - response.model, - response.usage.input_tokens, - response.usage.output_tokens, - request_service_tier="default", - response_service_tier=getattr(response, "service_tier", None), - ) - _assert_request_attrs( - span, - temperature=0.7, - top_p=0.9, - max_tokens=50, - output_type="text", - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_token_usage( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input="Count to 5.", - ) - - (span,) = span_exporter.get_finished_spans() - assert ( - span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] - == response.usage.input_tokens - ) - assert ( - span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] - == response.usage.output_tokens - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_aggregates_cache_tokens( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - ) - - (span,) = span_exporter.get_finished_spans() - assert_cache_attributes(span, response.usage) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_stop_reason( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input="Say hi.", - ) - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( - "stop", - ) - - -@pytest.mark.asyncio() -async def test_async_responses_create_connection_error( - span_exporter, instrument_no_content -): - _skip_if_not_latest() - - client = AsyncOpenAI(base_url="http://localhost:4242") - - with pytest.raises(APIConnectionError): - await client.responses.create( - model=DEFAULT_MODEL, - input="Hello", - timeout=0.1, - ) - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost" - assert span.attributes[ServerAttributes.SERVER_PORT] == 4242 - assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_api_error( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - with pytest.raises((BadRequestError, NotFoundError)) as exc_info: - await async_openai_client.responses.create( - model=INVALID_MODEL, - input="Hello", - ) - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL - assert ( - span.attributes[ErrorAttributes.ERROR_TYPE] - == type(exc_info.value).__name__ - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - service_tier="default", - stream=True, - ) - async with stream: - response = await _collect_completed_response(stream) - - (span,) = span_exporter.get_finished_spans() - assert_all_attributes( - span, - DEFAULT_MODEL, - True, - response.id, - response.model, - response.usage.input_tokens, - response.usage.output_tokens, - request_service_tier="default", - response_service_tier=getattr(response, "service_tier", None), - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming_aggregates_cache_tokens( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - async with stream: - response = await _collect_completed_response(stream) - - (span,) = span_exporter.get_finished_spans() - assert_cache_attributes(span, response.usage) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming_captures_content( - request, - span_exporter, - log_exporter, - async_openai_client, - instrument_with_content, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - async with stream: - response = await _collect_completed_response(stream) - - (span,) = span_exporter.get_finished_spans() - _assert_response_content(span, response, log_exporter) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming_iteration( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input="Say hi.", - stream=True, - ) - events = [] - async for event in stream: - events.append(event) - - assert len(events) > 0 - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes - assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming_delegates_response_attribute( - request, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input="Say hi.", - stream=True, - ) - - assert stream.response is not None - assert stream.response.status_code == 200 - assert stream.response.headers.get("x-request-id") is not None - await stream.close() - - -@pytest.mark.asyncio() -async def test_async_responses_create_streaming_connection_error( - span_exporter, instrument_no_content -): - _skip_if_not_latest() - - client = AsyncOpenAI(base_url="http://localhost:4242") - - with pytest.raises(APIConnectionError): - await client.responses.create( - model=DEFAULT_MODEL, - input="Hello", - stream=True, - timeout=0.1, - ) - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_stream_wrapper_finalize_idempotent( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - - response = await _collect_completed_response(stream) - await stream.close() - - spans = span_exporter.get_finished_spans() - assert len(spans) == 1 - assert_all_attributes( - spans[0], - DEFAULT_MODEL, - True, - response.id, - response.model, - response.usage.input_tokens, - response.usage.output_tokens, - response_service_tier=getattr(response, "service_tier", None), - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_stream_propagation_error( - request, - span_exporter, - async_openai_client, - instrument_no_content, - monkeypatch, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - - class ErrorInjectingStreamDelegate: - def __init__(self, inner): - self._inner = inner - self._count = 0 - - def __aiter__(self): - return self - - async def __anext__(self): - if self._count == 1: - raise ConnectionError("connection reset during stream") - self._count += 1 - return await self._inner.__anext__() - - async def close(self): - return await self._inner.close() - - def __getattr__(self, name): - return getattr(self._inner, name) - - monkeypatch.setattr( - stream, "stream", ErrorInjectingStreamDelegate(stream.stream) - ) - - with pytest.raises(ConnectionError, match="connection reset during stream"): - async with stream: - async for _ in stream: - pass - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_streaming_user_exception( - request, span_exporter, async_openai_client, instrument_no_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - - with pytest.raises(ValueError, match="User raised exception"): - async with stream: - async for _ in stream: - raise ValueError("User raised exception") - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_instrumentation_error_swallowed( - request, - span_exporter, - async_openai_client, - instrument_no_content, - monkeypatch, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - def exploding_process_event(self, event): - del self - del event - raise RuntimeError("instrumentation bug") - - monkeypatch.setattr( - AsyncResponseStreamWrapper, "process_event", exploding_process_event - ) - - stream = await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=True, - ) - async with stream: - events = [event async for event in stream] - - assert len(events) > 0 - - (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL - assert ErrorAttributes.ERROR_TYPE not in span.attributes - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -@pytest.mark.skipif( - not _has_tools_param, - reason=( - "openai SDK too old to support 'tools' parameter on AsyncResponses.create" - ), -) -async def test_async_responses_create_captures_tool_call_content( - request, span_exporter, async_openai_client, instrument_with_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - await async_openai_client.responses.create( - model=DEFAULT_MODEL, - input="What's the weather in Seattle right now?", - tools=[get_responses_weather_tool_definition()], - tool_choice={"type": "function", "name": "get_current_weather"}, - ) - - (span,) = span_exporter.get_finished_spans() - output_messages = _load_span_messages( - span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES - ) - assert any( - part.get("type") == "tool_call" - for message in output_messages - for part in message.get("parts", []) - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -@pytest.mark.skipif( - not _has_reasoning_param, - reason=( - "openai SDK too old to support 'reasoning' parameter on AsyncResponses.create" - ), -) -async def test_async_responses_create_reports_reasoning_tokens( - request, span_exporter, async_openai_client, instrument_with_content -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - response = await async_openai_client.responses.create( - model=REASONING_MODEL, - reasoning={"effort": "low"}, - input=[ - { - "role": "user", - "content": REASONING_PROMPT, - } - ], - max_output_tokens=300, - timeout=30.0, - ) - - reasoning_tokens = getattr( - getattr(response.usage, "output_tokens_details", None), - "reasoning_tokens", - None, - ) - - assert reasoning_tokens is not None - assert reasoning_tokens > 0 - - spans = span_exporter.get_finished_spans() - assert len(spans) == 1 - (span,) = spans - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == ( - REASONING_MODEL - ) - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_with_content_span_unsampled( - request, - span_exporter, - log_exporter, - async_openai_client, - instrument_with_content_unsampled, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=False, - ) - - assert len(span_exporter.get_finished_spans()) == 0 - assert len(log_exporter.get_finished_logs()) == 0 - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_with_content_shapes( - request, - span_exporter, - log_exporter, - async_openai_client, - instrument_with_content, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=False, - ) - - (span,) = span_exporter.get_finished_spans() - 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 input_messages[0]["parts"][0]["type"] == "text" - assert output_messages[0]["role"] == "assistant" - assert output_messages[0]["parts"][0]["type"] == "text" - assert len(log_exporter.get_finished_logs()) == 0 - - -@pytest.mark.asyncio() -@pytest.mark.vcr() -async def test_async_responses_create_event_only_no_content_in_span( - request, - span_exporter, - log_exporter, - async_openai_client, - instrument_event_only, -): - _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) - - await async_openai_client.responses.create( - model=DEFAULT_MODEL, - instructions=SYSTEM_INSTRUCTIONS, - input=USER_ONLY_PROMPT[0]["content"], - stream=False, - ) - - (span,) = span_exporter.get_finished_spans() - assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes - assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes - assert GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS not in span.attributes - - logs = log_exporter.get_finished_logs() - assert len(logs) == 1 - assert ( - logs[0].log_record.event_name - == "gen_ai.client.inference.operation.details" - ) From 881686bb113f993cfe97703663a08cae9cc90204 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Mon, 20 Apr 2026 16:51:03 -0400 Subject: [PATCH 06/10] WIP: removed the unnecessary cassette checks added context around responses wrapping. --- .../instrumentation/openai_v2/__init__.py | 5 +++++ .../tests/test_responses.py | 22 ------------------- .../tests/test_utils.py | 12 ---------- 3 files changed, 5 insertions(+), 34 deletions(-) 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 f9d46301e4..c1e261ef1a 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 @@ -164,6 +164,11 @@ def _instrument(self, **kwargs): ) responses_module = _get_responses_module() + # Responses instrumentation is intentionally limited to the latest + # experimental semconv path. Unlike chat completions, we do not carry + # a second legacy wrapper here; the current implementation is built on + # the inference handler lifecycle and would need a separate old-path + # implementation to support legacy semconv mode. if responses_module is not None and latest_experimental_enabled: wrap_function_wrapper( module="openai.resources.responses.responses", diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py index 9351dc0da1..f9746e559a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -42,7 +42,6 @@ assert_messages_attribute, format_simple_expected_output_message, get_responses_weather_tool_definition, - skip_if_cassette_missing_and_no_real_key, ) try: @@ -184,7 +183,6 @@ def test_responses_create_basic( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=DEFAULT_MODEL, @@ -217,7 +215,6 @@ def test_responses_create_captures_content( request, span_exporter, log_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=DEFAULT_MODEL, @@ -236,7 +233,6 @@ def test_responses_create_with_all_params( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=DEFAULT_MODEL, @@ -275,7 +271,6 @@ def test_responses_create_token_usage( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=DEFAULT_MODEL, @@ -299,7 +294,6 @@ def test_responses_create_aggregates_cache_tokens( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=DEFAULT_MODEL, @@ -316,7 +310,6 @@ def test_responses_create_stop_reason( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) openai_client.responses.create( model=DEFAULT_MODEL, @@ -354,7 +347,6 @@ def test_responses_create_api_error( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) with pytest.raises((BadRequestError, NotFoundError)) as exc_info: openai_client.responses.create( @@ -375,7 +367,6 @@ def test_responses_create_streaming( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) with openai_client.responses.create( model=DEFAULT_MODEL, @@ -405,7 +396,6 @@ def test_responses_create_streaming_aggregates_cache_tokens( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) with openai_client.responses.create( model=DEFAULT_MODEL, @@ -424,7 +414,6 @@ def test_responses_create_streaming_captures_content( request, span_exporter, log_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) with openai_client.responses.create( model=DEFAULT_MODEL, @@ -443,7 +432,6 @@ def test_responses_create_streaming_iteration( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) stream = openai_client.responses.create( model=DEFAULT_MODEL, @@ -466,7 +454,6 @@ def test_responses_create_streaming_delegates_response_attribute( request, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) stream = openai_client.responses.create( model=DEFAULT_MODEL, @@ -506,7 +493,6 @@ def test_responses_stream_wrapper_finalize_idempotent( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) stream = openai_client.responses.create( model=DEFAULT_MODEL, @@ -537,7 +523,6 @@ def test_responses_create_stream_propagation_error( request, span_exporter, openai_client, instrument_no_content, monkeypatch ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) stream = openai_client.responses.create( model=DEFAULT_MODEL, @@ -585,7 +570,6 @@ def test_responses_create_streaming_user_exception( request, span_exporter, openai_client, instrument_no_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) with pytest.raises(ValueError, match="User raised exception"): with openai_client.responses.create( @@ -607,7 +591,6 @@ def test_responses_create_instrumentation_error_swallowed( request, span_exporter, openai_client, instrument_no_content, monkeypatch ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) def exploding_process_event(self, event): del self @@ -642,7 +625,6 @@ def test_responses_create_captures_tool_call_content( request, span_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) openai_client.responses.create( model=DEFAULT_MODEL, @@ -673,7 +655,6 @@ def test_responses_create_reports_reasoning_tokens( request, span_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) response = openai_client.responses.create( model=REASONING_MODEL, @@ -714,7 +695,6 @@ def test_responses_create_with_content_span_unsampled( instrument_with_content_unsampled, ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) openai_client.responses.create( model=DEFAULT_MODEL, @@ -732,7 +712,6 @@ def test_responses_create_with_content_shapes( request, span_exporter, log_exporter, openai_client, instrument_with_content ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) openai_client.responses.create( model=DEFAULT_MODEL, @@ -761,7 +740,6 @@ def test_responses_create_event_only_no_content_in_span( request, span_exporter, log_exporter, openai_client, instrument_event_only ): _skip_if_not_latest() - skip_if_cassette_missing_and_no_real_key(request) openai_client.responses.create( model=DEFAULT_MODEL, diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index db9b555911..1af7e11c1f 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -321,18 +321,6 @@ def assert_message_in_logs(log, event_name, expected_content, parent_span): assert_log_parent(log, parent_span) -def skip_if_cassette_missing_and_no_real_key(request): - cassette_path = ( - Path(__file__).parent / "cassettes" / f"{request.node.name}.yaml" - ) - api_key = os.getenv("OPENAI_API_KEY") - if not cassette_path.exists() and api_key == "test_openai_api_key": - pytest.skip( - f"Cassette {cassette_path.name} is missing. " - "Set a real OPENAI_API_KEY to record it." - ) - - def assert_embedding_attributes( span: ReadableSpan, request_model: str, From 0c277ac9816d93130098ef2983d89de00df1a786 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Tue, 21 Apr 2026 19:41:09 -0400 Subject: [PATCH 07/10] WIP: fixing the lint in files. --- .../instrumentation/openai_v2/patch_responses.py | 9 --------- .../instrumentation/openai_v2/response_extractors.py | 4 ++-- .../tests/test_response_wrappers.py | 6 +++--- .../tests/test_responses.py | 8 ++++++-- .../tests/test_utils.py | 4 ---- 5 files changed, 11 insertions(+), 20 deletions(-) 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 index 0f319762cc..65ef3b6597 100644 --- 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 @@ -14,18 +14,9 @@ from __future__ import annotations -from typing import Optional - -from opentelemetry.semconv._incubating.attributes import ( - gen_ai_attributes as GenAIAttributes, -) -from opentelemetry.semconv._incubating.attributes import ( - server_attributes as ServerAttributes, -) from opentelemetry.util.genai.handler import TelemetryHandler from opentelemetry.util.genai.types import ContentCapturingMode, Error -from .instruments import Instruments from .response_extractors import ( _apply_request_attributes, _get_inference_creation_kwargs, 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 index 858b409171..2edb8034a9 100644 --- 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 @@ -38,6 +38,8 @@ server_attributes as ServerAttributes, ) +from .utils import get_server_address_and_port, value_is_set + _PYDANTIC_V2 = hasattr(BaseModel, "model_validate") if _PYDANTIC_V2: @@ -70,8 +72,6 @@ Text = None ToolCall = None -from .utils import get_server_address_and_port, value_is_set - 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" diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py index 14b8c740dd..7dcc88dd24 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_response_wrappers.py @@ -372,13 +372,13 @@ async def test_async_stream_wrapper_processes_events_and_stops_on_completion(): wrapper.process_event = processed.append wrapper._stop = stopped.append - result = await wrapper.__anext__() + result = await anext(wrapper) assert result is event assert processed == [event] with pytest.raises(StopAsyncIteration): - await wrapper.__anext__() + await anext(wrapper) assert stopped == [None] @@ -417,7 +417,7 @@ def record_failure(message, error_type): wrapper._fail = record_failure with pytest.raises(ValueError, match="boom"): - await wrapper.__anext__() + await anext(wrapper) assert failures == [("boom", ValueError)] diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py index f9746e559a..e43ba4917f 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -45,6 +45,10 @@ ) try: + # Responses is not available in the oldest supported OpenAI SDK, so keep + # this import guarded. Pylint runs against the oldest dependency set and + # cannot resolve this optional module there. + # pylint: disable-next=no-name-in-module from openai.resources.responses.responses import Responses as _Responses HAS_RESPONSES_API = True @@ -329,7 +333,7 @@ def test_responses_create_connection_error(span_exporter, instrument_no_content) client = OpenAI(base_url="http://localhost:4242") with pytest.raises(APIConnectionError): - client.responses.create( + client.responses.create( # pylint: disable=no-member model=DEFAULT_MODEL, input="Hello", timeout=0.1, @@ -476,7 +480,7 @@ def test_responses_create_streaming_connection_error( client = OpenAI(base_url="http://localhost:4242") with pytest.raises(APIConnectionError): - client.responses.create( + client.responses.create( # pylint: disable=no-member model=DEFAULT_MODEL, input="Hello", stream=True, diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index 1af7e11c1f..30a0b1d122 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -15,12 +15,8 @@ """Shared test utilities for OpenAI instrumentation tests.""" import json -import os -from pathlib import Path from typing import Any, Optional -import pytest - from opentelemetry.instrumentation.openai_v2.response_extractors import ( GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, From ecc37741ff0817c7ef5a5fe41de996e75c0e97a1 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Tue, 21 Apr 2026 19:42:37 -0400 Subject: [PATCH 08/10] WIP: fixing the precommit stuff. --- .../openai_v2/response_extractors.py | 10 +-- .../tests/conftest.py | 4 +- .../tests/test_responses.py | 70 ++++++++++++++----- .../tests/test_utils.py | 5 +- 4 files changed, 59 insertions(+), 30 deletions(-) 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 index 2edb8034a9..75da602b69 100644 --- 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 @@ -62,6 +62,8 @@ OutputMessage, Reasoning, Text, + ) + from opentelemetry.util.genai.types import ( ToolCallRequest as ToolCall, ) except ImportError: @@ -511,7 +513,9 @@ def _get_request_attributes( if port is not None: attributes[ServerAttributes.SERVER_PORT] = port - return {key: value for key, value in attributes.items() if value_is_set(value)} + return { + key: value for key, value in attributes.items() if value_is_set(value) + } def _get_inference_creation_kwargs( @@ -554,9 +558,7 @@ def _apply_request_attributes( output_type = _extract_output_type(kwargs) if output_type is not None: - invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = ( - output_type - ) + invocation.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = output_type if capture_content: invocation.system_instruction = _extract_system_instruction(kwargs) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py index cc6dd55796..229e964945 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/conftest.py @@ -227,9 +227,7 @@ def instrument_with_content_unsampled( @pytest.fixture(scope="function") -def instrument_event_only( - tracer_provider, logger_provider, meter_provider -): +def instrument_event_only(tracer_provider, logger_provider, meter_provider): _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update( diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py index e43ba4917f..1efa4d2811 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_responses.py @@ -98,9 +98,10 @@ def _assert_response_content(span, response, log_exporter): span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES], USER_ONLY_EXPECTED_INPUT_MESSAGES, ) - assert json.loads( - span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS] - ) == EXPECTED_SYSTEM_INSTRUCTIONS + assert ( + json.loads(span.attributes[GenAIAttributes.GEN_AI_SYSTEM_INSTRUCTIONS]) + == EXPECTED_SYSTEM_INSTRUCTIONS + ) assert_messages_attribute( span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES], format_simple_expected_output_message(response.output_text), @@ -129,7 +130,9 @@ def _assert_request_attrs( == max_tokens ) if output_type is not None: - assert span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type + assert ( + span.attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] == output_type + ) def _collect_completed_response(stream): @@ -206,9 +209,8 @@ def test_responses_create_basic( response.usage.output_tokens, response_service_tier=getattr(response, "service_tier", None), ) - assert ( - span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] - == ("stop",) + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "stop", ) assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes @@ -216,7 +218,11 @@ def test_responses_create_basic( @pytest.mark.vcr() def test_responses_create_captures_content( - request, span_exporter, log_exporter, openai_client, instrument_with_content + request, + span_exporter, + log_exporter, + openai_client, + instrument_with_content, ): _skip_if_not_latest() @@ -327,7 +333,9 @@ def test_responses_create_stop_reason( ) -def test_responses_create_connection_error(span_exporter, instrument_no_content): +def test_responses_create_connection_error( + span_exporter, instrument_no_content +): _skip_if_not_latest() client = OpenAI(base_url="http://localhost:4242") @@ -340,7 +348,9 @@ def test_responses_create_connection_error(span_exporter, instrument_no_content) ) (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost" assert span.attributes[ServerAttributes.SERVER_PORT] == 4242 assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" @@ -359,7 +369,9 @@ def test_responses_create_api_error( ) (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == INVALID_MODEL + ) assert ( span.attributes[ErrorAttributes.ERROR_TYPE] == type(exc_info.value).__name__ @@ -415,7 +427,11 @@ def test_responses_create_streaming_aggregates_cache_tokens( @pytest.mark.vcr() def test_responses_create_streaming_captures_content( - request, span_exporter, log_exporter, openai_client, instrument_with_content + request, + span_exporter, + log_exporter, + openai_client, + instrument_with_content, ): _skip_if_not_latest() @@ -448,7 +464,9 @@ def test_responses_create_streaming_iteration( assert len(events) > 0 (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes @@ -488,7 +506,9 @@ def test_responses_create_streaming_connection_error( ) (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError" @@ -559,13 +579,17 @@ def __getattr__(self, name): stream, "stream", ErrorInjectingStreamDelegate(stream.stream) ) - with pytest.raises(ConnectionError, match="connection reset during stream"): + with pytest.raises( + ConnectionError, match="connection reset during stream" + ): with stream: for _ in stream: pass (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" @@ -586,7 +610,9 @@ def test_responses_create_streaming_user_exception( raise ValueError("User raised exception") (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" @@ -616,7 +642,9 @@ def exploding_process_event(self, event): assert len(events) > 0 (span,) = span_exporter.get_finished_spans() - assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL + ) assert ErrorAttributes.ERROR_TYPE not in span.attributes @@ -713,7 +741,11 @@ def test_responses_create_with_content_span_unsampled( @pytest.mark.vcr() def test_responses_create_with_content_shapes( - request, span_exporter, log_exporter, openai_client, instrument_with_content + request, + span_exporter, + log_exporter, + openai_client, + instrument_with_content, ): _skip_if_not_latest() diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py index 30a0b1d122..cbb2a538c3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/tests/test_utils.py @@ -289,10 +289,7 @@ def assert_cache_attributes(span, usage): cache_creation = getattr(details, "cache_creation_input_tokens", None) if cache_creation is None: - assert ( - GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS - not in span.attributes - ) + assert GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS not in span.attributes else: assert ( span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] From d4ff5af39ad8a56e237342ff8296f16be89ff561 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Tue, 21 Apr 2026 19:48:34 -0400 Subject: [PATCH 09/10] WIP: added changelog. --- .../opentelemetry-instrumentation-openai-v2/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md index 6fc545df24..77f5bb110a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add strongly typed Responses API extractors with validation and content extraction improvements ([#4337](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4337)) +- Add instrumentation for OpenAI Responses API `create` + ([#4474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4474)) ## Version 2.3b0 (2025-12-24) From 0293f21dc71098b85a3c1f974b74e65dc10c62b6 Mon Sep 17 00:00:00 2001 From: eternalcuriouslearner Date: Tue, 21 Apr 2026 20:55:08 -0400 Subject: [PATCH 10/10] wip: fixing the wrap configuration. --- .../instrumentation/openai_v2/__init__.py | 6 +- .../openai_v2/response_extractors.py | 57 +------------------ 2 files changed, 4 insertions(+), 59 deletions(-) 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 c88904f026..f74a4796a2 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 @@ -171,9 +171,9 @@ def _instrument(self, **kwargs): # implementation to support legacy semconv mode. if responses_module is not None and latest_experimental_enabled: wrap_function_wrapper( - module="openai.resources.responses.responses", - name="Responses.create", - wrapper=responses_create(handler, content_mode), + "openai.resources.responses.responses", + "Responses.create", + responses_create(handler, content_mode), ) def _uninstrument(self, **kwargs): 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 index 75da602b69..bf4f3feb7e 100644 --- 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 @@ -34,11 +34,8 @@ from opentelemetry.semconv._incubating.attributes import ( openai_attributes as OpenAIAttributes, ) -from opentelemetry.semconv._incubating.attributes import ( - server_attributes as ServerAttributes, -) -from .utils import get_server_address_and_port, value_is_set +from .utils import get_server_address_and_port _PYDANTIC_V2 = hasattr(BaseModel, "model_validate") @@ -466,58 +463,6 @@ def _extract_request_service_tier( return service_tier -def _get_request_attributes( - kwargs: Mapping[str, object], - client_instance: object, - latest_experimental_enabled: bool, -) -> dict[str, object]: - request = _validate_request_kwargs(kwargs) - request_model = request.model if request is not None else None - attributes: dict[str, object] = { - GenAIAttributes.GEN_AI_OPERATION_NAME: ( - GenAIAttributes.GenAiOperationNameValues.CHAT.value - ), - GenAIAttributes.GEN_AI_REQUEST_MODEL: request_model, - } - - if latest_experimental_enabled: - attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] = ( - GenAIAttributes.GenAiProviderNameValues.OPENAI.value - ) - else: - attributes[GenAIAttributes.GEN_AI_SYSTEM] = ( - GenAIAttributes.GenAiSystemValues.OPENAI.value - ) - - output_type = _extract_output_type(kwargs) - if output_type is not None: - output_type_key = ( - GenAIAttributes.GEN_AI_OUTPUT_TYPE - if latest_experimental_enabled - else GenAIAttributes.GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT - ) - attributes[output_type_key] = output_type - - service_tier = _extract_request_service_tier(kwargs) - if service_tier is not None: - service_tier_key = ( - OpenAIAttributes.OPENAI_REQUEST_SERVICE_TIER - if latest_experimental_enabled - else GenAIAttributes.GEN_AI_OPENAI_REQUEST_SERVICE_TIER - ) - attributes[service_tier_key] = service_tier - - address, port = get_server_address_and_port(client_instance) - if address is not None: - attributes[ServerAttributes.SERVER_ADDRESS] = address - if port is not None: - attributes[ServerAttributes.SERVER_PORT] = port - - return { - key: value for key, value in attributes.items() if value_is_set(value) - } - - def _get_inference_creation_kwargs( kwargs: Mapping[str, object], client_instance: object,