Skip to content
Merged
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
8 changes: 8 additions & 0 deletions py/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ def test_openai_http2_streaming(session, version):
_run_tests(session, f"{INTEGRATION_DIR}/openai/test_openai_http2.py", version=version)


@nox.session()
def test_openai_ddtrace(session):
_install_test_deps(session)
_install_matrix_dep(session, "openai", LATEST)
_install_group_locked(session, "test-openai-ddtrace")
_run_tests(session, f"{INTEGRATION_DIR}/openai/test_openai_ddtrace.py", version=LATEST)


OPENAI_AGENTS_VERSIONS = _get_matrix_versions("openai-agents")


Expand Down
5 changes: 5 additions & 0 deletions py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ test-openai-http2 = [
"h2==4.3.0",
]

test-openai-ddtrace = [
{include-group = "test"},
"ddtrace==4.1.0",
]

test-openai-agents = [
{include-group = "test"},
# openai is an auxiliary dep (matrix dep is openai-agents); pin here.
Expand Down
17 changes: 11 additions & 6 deletions py/src/braintrust/integrations/openai/patchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,15 +434,20 @@ class _WrapResponsesRaw(CompositeFunctionWrapperPatcher):
# ---------------------------------------------------------------------------


def _is_class_method_wrapped(resource: Any, method_name: str) -> bool:
def _is_class_method_wrapped(
resource: Any,
method_name: str,
patcher: type[FunctionWrapperPatcher],
) -> bool:
"""Return ``True`` if *method_name* on the **class** of *resource* is
already a wrapt ``FunctionWrapper`` (i.e. patched by ``setup()``).
already wrapped by the matching Braintrust ``setup()`` patcher.

This prevents double-tracing when both ``setup()`` and ``wrap_openai()``
are active for the same client.
Other libraries (for example ddtrace) may also use wrapt wrappers on OpenAI
class methods. Only Braintrust's own patch marker should make
``wrap_openai()`` skip instance-level instrumentation.
"""
cls_attr = inspect.getattr_static(type(resource), method_name, None)
return isinstance(cls_attr, FunctionWrapper)
return patcher.has_patch_marker(cls_attr)


def _delegates_to_wrapped_method(resource: Any, method_name: str) -> bool:
Expand Down Expand Up @@ -480,7 +485,7 @@ def _wrap_resource(
# level patchers are active and we can skip instance-level wrapping.
for sub in patcher.sub_patchers:
attr = sub.target_path.rsplit(".", 1)[-1]
if _is_class_method_wrapped(resource, attr):
if _is_class_method_wrapped(resource, attr, sub):
return
if attr_path.endswith("with_raw_response") and _delegates_to_wrapped_method(resource, attr):
return
Expand Down
22 changes: 22 additions & 0 deletions py/src/braintrust/integrations/openai/test_openai_ddtrace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Compatibility tests for Braintrust OpenAI wrapping with ddtrace."""

import importlib
import inspect

import openai
from braintrust import wrap_openai
from braintrust.integrations.openai.patchers import _wrap_chat_create
from wrapt import FunctionWrapper


def test_wrap_openai_wraps_instance_when_ddtrace_patched_class():
ddtrace = importlib.import_module("ddtrace")

ddtrace.patch(openai=True)

class_create = inspect.getattr_static(openai.resources.chat.completions.Completions, "create")
assert isinstance(class_create, FunctionWrapper)

client = wrap_openai(openai.OpenAI())

assert _wrap_chat_create.has_patch_marker(client.chat.completions)
Loading