Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f3fbd78
Implement OpenAI Responses API instrumentation and examples
vasantteja Feb 5, 2026
2e568bb
Add support for OpenAI Responses API instrumentation in CHANGELOG.md
vasantteja Feb 5, 2026
0f670d9
Enhance OpenAI Responses API instrumentation with version checks
vasantteja Feb 5, 2026
8987864
Refactor OpenAI instrumentation code and add comments for clarity
vasantteja Feb 5, 2026
02a80cd
Refactor OpenAI instrumentation code for improved readability
vasantteja Feb 5, 2026
88c7908
Update OpenAI package version and refactor span name retrieval
vasantteja Feb 6, 2026
65ee9c0
Enhance OpenAI instrumentation with improved span name handling and c…
vasantteja Feb 7, 2026
e4378e8
Merge branch 'main' into feat/instrument-openai-responses
vasantteja Feb 8, 2026
9bbdd62
Implement tracing for OpenAI Responses.retrieve method
vasantteja Feb 9, 2026
73df7d6
Add TODO for future migration of Responses instrumentation
vasantteja Feb 10, 2026
3c0c4bb
Add responses instrumentation for OpenAI API
vasantteja Feb 10, 2026
c15627f
Refactor metrics recording and improve response handling in OpenAI in…
vasantteja Feb 10, 2026
5e26cdd
Merge remote-tracking branch 'upstream/main' into feat/instrument-ope…
vasantteja Feb 12, 2026
3f69b56
Enhance OpenAI instrumentation with new response handling and telemet…
vasantteja Feb 12, 2026
9eeac24
Refactor OpenAI instrumentation to enhance content capture and teleme…
vasantteja Feb 12, 2026
e2604b6
Remove example files for OpenAI Responses API instrumentation.
vasantteja Feb 14, 2026
8f4679e
Refactor OpenAI instrumentation to improve content capture and respon…
vasantteja Feb 15, 2026
11d1b3c
regerating cassettes and fixing failing tests.
vasantteja Feb 15, 2026
bc1a604
Update OpenAI instrumentation to enhance dependency management and im…
vasantteja Feb 15, 2026
67b49ed
Merge branch 'main' into feat/instrument-openai-responses
vasantteja Feb 15, 2026
0d77913
Enhance OpenAI response handling by adding new extraction and attribu…
vasantteja Feb 16, 2026
a8c5f43
Refactor OpenAI response handling by modularizing extraction and resp…
vasantteja Feb 16, 2026
4962a9f
Merge branch 'main' into feat/instrument-openai-responses
vasantteja Feb 17, 2026
f2f0b48
Refactor OpenAI instrumentation to remove optional dependency handlin…
vasantteja Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add support for OpenAI Responses API instrumentation
([#4166](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4166))
- Fix `StreamWrapper` missing `.headers` and other attributes when using `with_raw_response` streaming
([#4113](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4113))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ Make sure to configure OpenTelemetry tracing, logging, and events to capture all
input="Generate vector embeddings for this text"
)

# Responses API example
response = client.responses.create(
model="gpt-4o-mini",
input="Write a short poem on OpenTelemetry.",
)

# Responses streaming example
with client.responses.stream(
model="gpt-4o-mini",
input="Write a short poem on OpenTelemetry.",
background=True,
) as stream:
for event in stream:
if event.type == "response.completed":
response = event.response
break

Enabling message content
*************************

Expand Down Expand Up @@ -109,4 +126,3 @@ References
* `OpenTelemetry OpenAI Instrumentation <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation-genai/openai.html>`_
* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ classifiers = [
dependencies = [
"opentelemetry-api ~= 1.37",
"opentelemetry-instrumentation ~= 0.58b0",
"opentelemetry-semantic-conventions ~= 0.58b0"
"opentelemetry-semantic-conventions ~= 0.58b0",
"opentelemetry-util-genai >= 0.2b0, <0.3b0",
]

[project.optional-dependencies]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from opentelemetry.metrics import get_meter
from opentelemetry.semconv.schemas import Schemas
from opentelemetry.trace import get_tracer
from opentelemetry.util.genai.handler import TelemetryHandler

from .instruments import Instruments
from .patch import (
Expand All @@ -60,6 +61,7 @@
chat_completions_create,
embeddings_create,
)
from .patch_responses import responses_create


class OpenAIInstrumentor(BaseInstrumentor):
Expand Down Expand Up @@ -94,44 +96,74 @@ def _instrument(self, **kwargs):
)

instruments = Instruments(self._meter)
capture_content = is_content_enabled()

wrap_function_wrapper(
module="openai.resources.chat.completions",
name="Completions.create",
wrapper=chat_completions_create(
tracer, logger, instruments, is_content_enabled()
tracer, logger, instruments, capture_content
),
)

wrap_function_wrapper(
module="openai.resources.chat.completions",
name="AsyncCompletions.create",
wrapper=async_chat_completions_create(
tracer, logger, instruments, is_content_enabled()
tracer, logger, instruments, capture_content
),
)

# Add instrumentation for the embeddings API
wrap_function_wrapper(
module="openai.resources.embeddings",
name="Embeddings.create",
wrapper=embeddings_create(
tracer, instruments, is_content_enabled()
),
wrapper=embeddings_create(tracer, instruments, capture_content),
)

wrap_function_wrapper(
module="openai.resources.embeddings",
name="AsyncEmbeddings.create",
wrapper=async_embeddings_create(
tracer, instruments, is_content_enabled()
tracer, instruments, capture_content
),
)

# Responses API is only available in openai>=1.66.0
# https://github.com/openai/openai-python/blob/main/CHANGELOG.md#1660-2025-03-11
try:
if TelemetryHandler is None:
raise ModuleNotFoundError(
"opentelemetry.util.genai.handler is unavailable"
)

handler = TelemetryHandler(
tracer_provider=tracer_provider,
meter_provider=meter_provider,
logger_provider=logger_provider,
)

wrap_function_wrapper(
module="openai.resources.responses.responses",
name="Responses.create",
wrapper=responses_create(handler, capture_content),
)
except (AttributeError, ModuleNotFoundError):
# Responses API or TelemetryHandler not available
pass

def _uninstrument(self, **kwargs):
import openai # pylint: disable=import-outside-toplevel # noqa: PLC0415

unwrap(openai.resources.chat.completions.Completions, "create")
unwrap(openai.resources.chat.completions.AsyncCompletions, "create")
unwrap(openai.resources.embeddings.Embeddings, "create")
unwrap(openai.resources.embeddings.AsyncEmbeddings, "create")

# Responses API is only available in openai>=1.66.0
# https://github.com/openai/openai-python/blob/main/CHANGELOG.md#1660-2025-03-11
try:
unwrap(openai.resources.responses.responses.Responses, "create")
except (AttributeError, ModuleNotFoundError):
# Responses API not available in this version of openai
pass
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

# pylint: disable=too-many-lines

from timeit import default_timer
from typing import Any, Optional
Expand All @@ -23,14 +24,12 @@
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.semconv._incubating.attributes import (
server_attributes as ServerAttributes,
)
from opentelemetry.trace import Span, SpanKind, Tracer
from opentelemetry.trace.propagation import set_span_in_context

from .instruments import Instruments
from .utils import (
_record_metrics,
choice_to_event,
get_llm_request_attributes,
handle_span_exception,
Expand Down Expand Up @@ -283,83 +282,6 @@ def _get_embeddings_span_name(span_attributes):
return f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"


def _record_metrics(
instruments: Instruments,
duration: float,
result,
request_attributes: dict,
error_type: Optional[str],
operation_name: str,
):
common_attributes = {
GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name,
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.OPENAI.value,
GenAIAttributes.GEN_AI_REQUEST_MODEL: request_attributes[
GenAIAttributes.GEN_AI_REQUEST_MODEL
],
}

if "gen_ai.embeddings.dimension.count" in request_attributes:
common_attributes["gen_ai.embeddings.dimension.count"] = (
request_attributes["gen_ai.embeddings.dimension.count"]
)

if error_type:
common_attributes["error.type"] = error_type

if result and getattr(result, "model", None):
common_attributes[GenAIAttributes.GEN_AI_RESPONSE_MODEL] = result.model

if result and getattr(result, "service_tier", None):
common_attributes[
GenAIAttributes.GEN_AI_OPENAI_RESPONSE_SERVICE_TIER
] = result.service_tier

if result and getattr(result, "system_fingerprint", None):
common_attributes["gen_ai.openai.response.system_fingerprint"] = (
result.system_fingerprint
)

if ServerAttributes.SERVER_ADDRESS in request_attributes:
common_attributes[ServerAttributes.SERVER_ADDRESS] = (
request_attributes[ServerAttributes.SERVER_ADDRESS]
)

if ServerAttributes.SERVER_PORT in request_attributes:
common_attributes[ServerAttributes.SERVER_PORT] = request_attributes[
ServerAttributes.SERVER_PORT
]

instruments.operation_duration_histogram.record(
duration,
attributes=common_attributes,
)

if result and getattr(result, "usage", None):
# Always record input tokens
input_attributes = {
**common_attributes,
GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.INPUT.value,
}
instruments.token_usage_histogram.record(
result.usage.prompt_tokens,
attributes=input_attributes,
)

# For embeddings, don't record output tokens as all tokens are input tokens
if (
operation_name
!= GenAIAttributes.GenAiOperationNameValues.EMBEDDINGS.value
):
output_attributes = {
**common_attributes,
GenAIAttributes.GEN_AI_TOKEN_TYPE: GenAIAttributes.GenAiTokenTypeValues.COMPLETION.value,
}
instruments.token_usage_histogram.record(
result.usage.completion_tokens, attributes=output_attributes
)


def _set_response_attributes(
span, result, logger: Logger, capture_content: bool
):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING

from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.util.genai.types import Error, LLMInvocation

from .response_extractors import (
GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
OPENAI,
_extract_input_messages,
_extract_output_type,
_extract_system_instruction,
_set_invocation_response_attributes,
)
from .response_wrappers import ResponseStreamWrapper
from .utils import get_llm_request_attributes, is_streaming

if TYPE_CHECKING:
from opentelemetry.util.genai.handler import TelemetryHandler

__all__ = [
"responses_create",
"_set_invocation_response_attributes",
"GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS",
"GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS",
]

# ---------------------------------------------------------------------------
# Patch functions
# ---------------------------------------------------------------------------


def responses_create(
handler: "TelemetryHandler",
capture_content: bool,
):
"""Wrap the `create` method of the `Responses` class to trace it."""
# https://github.com/openai/openai-python/blob/dc68b90655912886bd7a6c7787f96005452ebfc9/src/openai/resources/responses/responses.py#L828

def traced_method(wrapped, instance, args, kwargs):
if Error is None or LLMInvocation is None:
raise ModuleNotFoundError(
"opentelemetry.util.genai.types is unavailable"
)

operation_name = GenAIAttributes.GenAiOperationNameValues.CHAT.value
span_attributes = get_llm_request_attributes(
kwargs,
instance,
operation_name,
)
output_type = _extract_output_type(kwargs)
if output_type:
span_attributes[GenAIAttributes.GEN_AI_OUTPUT_TYPE] = output_type
request_model = str(
span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
or "unknown"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to set model, it should not be set when not provided

)
streaming = is_streaming(kwargs)

invocation = handler.start_llm(
LLMInvocation(
request_model=request_model,
operation_name=operation_name,
provider=OPENAI,
input_messages=_extract_input_messages(kwargs)
if capture_content
else [],
system_instruction=_extract_system_instruction(kwargs)
if capture_content
else [],
attributes=span_attributes.copy(),
metric_attributes={
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not be necessary -

GenAI.GEN_AI_OPERATION_NAME: GenAI.GenAiOperationNameValues.CHAT.value

GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name
},
)
)

try:
result = wrapped(*args, **kwargs)
if hasattr(result, "parse"):
parsed_result = result.parse()
else:
parsed_result = result

if streaming:
return ResponseStreamWrapper(
parsed_result,
handler,
invocation,
capture_content,
)

_set_invocation_response_attributes(
invocation, parsed_result, capture_content
)
handler.stop_llm(invocation)
return result

except Exception as error:
handler.fail_llm(
invocation, Error(message=str(error), type=type(error))
)
raise

return traced_method
Loading