Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add latest-experimental semantic convention support for OpenAI Responses API
`responses.create`, including sync, async, streaming, token usage, message
content, response status, reasoning metadata, and tool definitions.
([#209](https://github.com/alibaba/loongsuite-python-agent/issues/209))
- Fix `StreamWrapper` missing `.headers` and other attributes when using `with_raw_response` streaming
([#4113](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4113))
- Add opt-in support for latest experimental semantic conventions (v1.37.0). Set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Instrumenting all clients
*************************

When using the instrumentor, all clients will automatically trace OpenAI operations including chat completions and embeddings.
With `latest experimental features <#enabling-the-latest-experimental-features>`_ enabled, it also traces sync and async
Responses API calls made with ``responses.create``, including ``stream=True`` calls.
You can also optionally capture prompts and completions as log events.

Make sure to configure OpenTelemetry tracing, logging, and events to capture all telemetry emitted by the instrumentation.
Expand Down Expand Up @@ -130,4 +132,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 @@ -40,6 +40,8 @@
---
"""

import logging
from importlib import import_module
from typing import Collection

from wrapt import wrap_function_wrapper
Expand All @@ -66,11 +68,47 @@
async_chat_completions_create_v_new,
async_chat_completions_create_v_old,
async_embeddings_create,
async_responses_create_v_new,
async_responses_parse_v_new,
async_responses_retrieve_v_new,
chat_completions_create_v_new,
chat_completions_create_v_old,
embeddings_create,
responses_create_v_new,
responses_parse_v_new,
responses_retrieve_v_new,
)

_logger = logging.getLogger(__name__)


def _wrap_function_wrapper_if_available(module, name, wrapper):
try:
imported_module = import_module(module)
target = imported_module
for part in name.split(".")[:-1]:
target = getattr(target, part)
getattr(target, name.split(".")[-1])
except (AttributeError, ModuleNotFoundError) as exc:
_logger.debug(
"Skipping optional OpenAI wrapper %s.%s: %s",
module,
name,
exc,
)
return
wrap_function_wrapper(module=module, name=name, wrapper=wrapper)


def _unwrap_if_available(module_name, class_name, method_name):
try:
module = import_module(module_name)
except ModuleNotFoundError:
return
cls = getattr(module, class_name, None)
if cls is not None and hasattr(cls, method_name):
unwrap(cls, method_name)


class OpenAIInstrumentor(BaseInstrumentor):
def __init__(self):
Expand Down Expand Up @@ -159,10 +197,61 @@ def _instrument(self, **kwargs):
),
)

if latest_experimental_enabled:
_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="Responses.create",
wrapper=responses_create_v_new(handler, content_mode),
)
_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="Responses.parse",
wrapper=responses_parse_v_new(handler, content_mode),
)
_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="Responses.retrieve",
wrapper=responses_retrieve_v_new(handler, content_mode),
)

_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="AsyncResponses.create",
wrapper=async_responses_create_v_new(handler, content_mode),
)
_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="AsyncResponses.parse",
wrapper=async_responses_parse_v_new(handler, content_mode),
)
_wrap_function_wrapper_if_available(
module="openai.resources.responses",
name="AsyncResponses.retrieve",
wrapper=async_responses_retrieve_v_new(handler, content_mode),
)

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")
_unwrap_if_available(
"openai.resources.responses", "Responses", "create"
)
_unwrap_if_available(
"openai.resources.responses", "Responses", "parse"
)
_unwrap_if_available(
"openai.resources.responses", "Responses", "retrieve"
)
_unwrap_if_available(
"openai.resources.responses", "AsyncResponses", "create"
)
_unwrap_if_available(
"openai.resources.responses", "AsyncResponses", "parse"
)
_unwrap_if_available(
"openai.resources.responses", "AsyncResponses", "retrieve"
)
Loading
Loading