diff --git a/posthog/ai/openai/_embeddings.py b/posthog/ai/openai/_embeddings.py new file mode 100644 index 00000000..834e8bc5 --- /dev/null +++ b/posthog/ai/openai/_embeddings.py @@ -0,0 +1,49 @@ +from typing import Any, Dict, Optional + +from ..utils import _capture_ai_event, finalize_ai_content, with_privacy_mode + + +def _capture_embedding_event( + *, + posthog_client: Any, + base_url: Any, + response: Any, + request_kwargs: Dict[str, Any], + latency: float, + distinct_id: Optional[str], + trace_id: str, + properties: Optional[Dict[str, Any]], + privacy_mode: bool, + groups: Optional[Dict[str, Any]], +) -> None: + """Build and capture telemetry shared by sync and async embedding wrappers.""" + usage = getattr(response, "usage", None) + input_tokens = getattr(usage, "prompt_tokens", 0) if usage else 0 + + event_properties = { + "$ai_provider": "openai", + "$ai_model": request_kwargs.get("model"), + "$ai_input": with_privacy_mode( + posthog_client, + privacy_mode, + finalize_ai_content(request_kwargs.get("input"), posthog_client), + ), + "$ai_http_status": 200, + "$ai_input_tokens": input_tokens, + "$ai_latency": latency, + "$ai_trace_id": trace_id, + "$ai_base_url": str(base_url), + **(properties or {}), + } + + if distinct_id is None: + event_properties["$process_person_profile"] = False + + if hasattr(posthog_client, "capture"): + _capture_ai_event( + posthog_client, + "$ai_embedding", + distinct_id=distinct_id or trace_id, + properties=event_properties, + groups=groups, + ) diff --git a/posthog/ai/openai/openai.py b/posthog/ai/openai/openai.py index 3e8234b6..9a046831 100644 --- a/posthog/ai/openai/openai.py +++ b/posthog/ai/openai/openai.py @@ -1,6 +1,6 @@ import time import uuid -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING as _TYPE_CHECKING, Any, Dict, Optional from posthog.ai.types import TokenUsage as TokenUsage @@ -13,11 +13,10 @@ from posthog.ai.utils import ( call_llm_and_track_usage, - _capture_ai_event, extract_available_tool_calls as extract_available_tool_calls, - finalize_ai_content, + finalize_ai_content as finalize_ai_content, merge_usage_stats as merge_usage_stats, - with_privacy_mode, + with_privacy_mode as with_privacy_mode, ) from posthog.ai.openai.openai_converter import ( accumulate_openai_tool_calls as accumulate_openai_tool_calls, @@ -34,8 +33,10 @@ _ResponsesStreamState, _build_streaming_event_data, ) -from posthog.ai.openai.wrapper_utils import ( +from ._embeddings import _capture_embedding_event +from .wrapper_utils import ( _OpenAIWrapperResource, + _wrap_openai_resources, merge_provider_override, ) @@ -47,6 +48,12 @@ class OpenAI(openai.OpenAI): _ph_client: PostHogClient + if _TYPE_CHECKING: + chat: "WrappedChat" + embeddings: "WrappedEmbeddings" + beta: "WrappedBeta" + responses: "WrappedResponses" + def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: @@ -59,24 +66,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): super().__init__(**kwargs) self._ph_client = posthog_client or setup() - # Store original objects after parent initialization (only if they exist) - self._original_chat = getattr(self, "chat", None) - self._original_embeddings = getattr(self, "embeddings", None) - self._original_beta = getattr(self, "beta", None) - self._original_responses = getattr(self, "responses", None) - - # Replace with wrapped versions (only if originals exist) - if self._original_chat is not None: - self.chat = WrappedChat(self, self._original_chat) - - if self._original_embeddings is not None: - self.embeddings = WrappedEmbeddings(self, self._original_embeddings) - - if self._original_beta is not None: - self.beta = WrappedBeta(self, self._original_beta) - - if self._original_responses is not None: - self.responses = WrappedResponses(self, self._original_responses) + _wrap_openai_resources(self, _SYNC_RESOURCE_WRAPPERS) def _parse_and_track( @@ -491,46 +481,18 @@ def create( response = self._original.create(**kwargs) end_time = time.time() - # Extract usage statistics if available - usage_stats = {} - if hasattr(response, "usage") and response.usage: - usage_stats = { - "prompt_tokens": getattr(response.usage, "prompt_tokens", 0), - "total_tokens": getattr(response.usage, "total_tokens", 0), - } - - latency = end_time - start_time - - # Build the event properties - event_properties = { - "$ai_provider": "openai", - "$ai_model": kwargs.get("model"), - "$ai_input": with_privacy_mode( - self._client._ph_client, - posthog_privacy_mode, - finalize_ai_content(kwargs.get("input"), self._client._ph_client), - ), - "$ai_http_status": 200, - "$ai_input_tokens": usage_stats.get("prompt_tokens", 0), - "$ai_latency": latency, - "$ai_trace_id": posthog_trace_id, - "$ai_base_url": str(self._client.base_url), - **(posthog_properties or {}), - } - - if posthog_distinct_id is None: - event_properties["$process_person_profile"] = False - - # Send capture event for embeddings - if hasattr(self._client._ph_client, "capture"): - _capture_ai_event( - self._client._ph_client, - "$ai_embedding", - distinct_id=posthog_distinct_id or posthog_trace_id, - properties=event_properties, - groups=posthog_groups, - ) - + _capture_embedding_event( + posthog_client=self._client._ph_client, + base_url=self._client.base_url, + response=response, + request_kwargs=kwargs, + latency=end_time - start_time, + distinct_id=posthog_distinct_id, + trace_id=posthog_trace_id, + properties=posthog_properties, + privacy_mode=posthog_privacy_mode, + groups=posthog_groups, + ) return response @@ -594,3 +556,11 @@ def parse( posthog_provider_override, **kwargs, ) + + +_SYNC_RESOURCE_WRAPPERS = { + "chat": WrappedChat, + "embeddings": WrappedEmbeddings, + "beta": WrappedBeta, + "responses": WrappedResponses, +} diff --git a/posthog/ai/openai/openai_async.py b/posthog/ai/openai/openai_async.py index 7d61a921..e7a955cc 100644 --- a/posthog/ai/openai/openai_async.py +++ b/posthog/ai/openai/openai_async.py @@ -1,9 +1,9 @@ import time import uuid -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING as _TYPE_CHECKING, Any, Dict, Optional from posthog.ai.stream import AsyncStreamWrapper -from posthog.ai.types import TokenUsage +from posthog.ai.types import TokenUsage as TokenUsage try: import openai @@ -15,12 +15,11 @@ from posthog import setup from posthog.ai.utils import ( call_llm_and_track_usage_async, - _capture_ai_event, extract_available_tool_calls as extract_available_tool_calls, - finalize_ai_content, + finalize_ai_content as finalize_ai_content, get_model_params as get_model_params, merge_usage_stats as merge_usage_stats, - with_privacy_mode, + with_privacy_mode as with_privacy_mode, ) from posthog.ai.openai.openai_converter import ( accumulate_openai_tool_calls as accumulate_openai_tool_calls, @@ -36,8 +35,10 @@ _ResponsesStreamState, _build_streaming_event_data, ) -from posthog.ai.openai.wrapper_utils import ( +from ._embeddings import _capture_embedding_event +from .wrapper_utils import ( _OpenAIWrapperResource, + _wrap_openai_resources, merge_provider_override, ) @@ -49,6 +50,12 @@ class AsyncOpenAI(openai.AsyncOpenAI): _ph_client: PostHogClient + if _TYPE_CHECKING: + chat: "WrappedChat" + embeddings: "WrappedEmbeddings" + beta: "WrappedBeta" + responses: "WrappedResponses" + def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: @@ -61,24 +68,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): super().__init__(**kwargs) self._ph_client = posthog_client or setup() - # Store original objects after parent initialization (only if they exist) - self._original_chat = getattr(self, "chat", None) - self._original_embeddings = getattr(self, "embeddings", None) - self._original_beta = getattr(self, "beta", None) - self._original_responses = getattr(self, "responses", None) - - # Replace with wrapped versions (only if originals exist) - if self._original_chat is not None: - self.chat = WrappedChat(self, self._original_chat) - - if self._original_embeddings is not None: - self.embeddings = WrappedEmbeddings(self, self._original_embeddings) - - if self._original_beta is not None: - self.beta = WrappedBeta(self, self._original_beta) - - if self._original_responses is not None: - self.responses = WrappedResponses(self, self._original_responses) + _wrap_openai_resources(self, _ASYNC_RESOURCE_WRAPPERS) async def _parse_and_track( @@ -496,47 +486,18 @@ async def create( response = await self._original.create(**kwargs) end_time = time.time() - # Extract usage statistics if available - usage_stats: TokenUsage = TokenUsage() - - if hasattr(response, "usage") and response.usage: - usage_stats = TokenUsage( - input_tokens=getattr(response.usage, "prompt_tokens", 0), - output_tokens=getattr(response.usage, "completion_tokens", 0), - ) - - latency = end_time - start_time - - # Build the event properties - event_properties = { - "$ai_provider": "openai", - "$ai_model": kwargs.get("model"), - "$ai_input": with_privacy_mode( - self._client._ph_client, - posthog_privacy_mode, - finalize_ai_content(kwargs.get("input"), self._client._ph_client), - ), - "$ai_http_status": 200, - "$ai_input_tokens": usage_stats.get("input_tokens", 0), - "$ai_latency": latency, - "$ai_trace_id": posthog_trace_id, - "$ai_base_url": str(self._client.base_url), - **(posthog_properties or {}), - } - - if posthog_distinct_id is None: - event_properties["$process_person_profile"] = False - - # Send capture event for embeddings - if hasattr(self._client._ph_client, "capture"): - _capture_ai_event( - self._client._ph_client, - "$ai_embedding", - distinct_id=posthog_distinct_id or posthog_trace_id, - properties=event_properties, - groups=posthog_groups, - ) - + _capture_embedding_event( + posthog_client=self._client._ph_client, + base_url=self._client.base_url, + response=response, + request_kwargs=kwargs, + latency=end_time - start_time, + distinct_id=posthog_distinct_id, + trace_id=posthog_trace_id, + properties=posthog_properties, + privacy_mode=posthog_privacy_mode, + groups=posthog_groups, + ) return response @@ -600,3 +561,11 @@ async def parse( posthog_provider_override, **kwargs, ) + + +_ASYNC_RESOURCE_WRAPPERS = { + "chat": WrappedChat, + "embeddings": WrappedEmbeddings, + "beta": WrappedBeta, + "responses": WrappedResponses, +} diff --git a/posthog/ai/openai/openai_providers.py b/posthog/ai/openai/openai_providers.py index d86d4998..7c0ce04b 100644 --- a/posthog/ai/openai/openai_providers.py +++ b/posthog/ai/openai/openai_providers.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING as _TYPE_CHECKING, Optional + try: import openai except ImportError: @@ -5,17 +7,21 @@ "Please install the Open AI SDK to use this feature: 'pip install openai'" ) -from posthog.ai.openai.openai import ( - WrappedBeta, - WrappedChat, - WrappedEmbeddings, - WrappedResponses, +from .openai import ( + WrappedBeta as WrappedBeta, + WrappedChat as WrappedChat, + WrappedEmbeddings as WrappedEmbeddings, + WrappedResponses as WrappedResponses, + _SYNC_RESOURCE_WRAPPERS, +) +from .openai_async import ( + WrappedBeta as AsyncWrappedBeta, + WrappedChat as AsyncWrappedChat, + WrappedEmbeddings as AsyncWrappedEmbeddings, + WrappedResponses as AsyncWrappedResponses, + _ASYNC_RESOURCE_WRAPPERS, ) -from posthog.ai.openai.openai_async import WrappedBeta as AsyncWrappedBeta -from posthog.ai.openai.openai_async import WrappedChat as AsyncWrappedChat -from posthog.ai.openai.openai_async import WrappedEmbeddings as AsyncWrappedEmbeddings -from posthog.ai.openai.openai_async import WrappedResponses as AsyncWrappedResponses -from typing import Optional +from .wrapper_utils import _wrap_openai_resources from posthog.client import Client as PostHogClient from posthog import setup @@ -28,6 +34,12 @@ class AzureOpenAI(openai.AzureOpenAI): _ph_client: PostHogClient + if _TYPE_CHECKING: + chat: "WrappedChat" + embeddings: "WrappedEmbeddings" + beta: "WrappedBeta" + responses: "WrappedResponses" + def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: @@ -39,24 +51,7 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): super().__init__(**kwargs) self._ph_client = posthog_client or setup() - # Store original objects after parent initialization (only if they exist) - self._original_chat = getattr(self, "chat", None) - self._original_embeddings = getattr(self, "embeddings", None) - self._original_beta = getattr(self, "beta", None) - self._original_responses = getattr(self, "responses", None) - - # Replace with wrapped versions (only if originals exist) - if self._original_chat is not None: - self.chat = WrappedChat(self, self._original_chat) - - if self._original_embeddings is not None: - self.embeddings = WrappedEmbeddings(self, self._original_embeddings) - - if self._original_beta is not None: - self.beta = WrappedBeta(self, self._original_beta) - - if self._original_responses is not None: - self.responses = WrappedResponses(self, self._original_responses) + _wrap_openai_resources(self, _SYNC_RESOURCE_WRAPPERS) class AsyncAzureOpenAI(openai.AsyncAzureOpenAI): @@ -66,6 +61,12 @@ class AsyncAzureOpenAI(openai.AsyncAzureOpenAI): _ph_client: PostHogClient + if _TYPE_CHECKING: + chat: "AsyncWrappedChat" + embeddings: "AsyncWrappedEmbeddings" + beta: "AsyncWrappedBeta" + responses: "AsyncWrappedResponses" + def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): """ Args: @@ -77,22 +78,4 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs): super().__init__(**kwargs) self._ph_client = posthog_client or setup() - # Store original objects after parent initialization (only if they exist) - self._original_chat = getattr(self, "chat", None) - self._original_embeddings = getattr(self, "embeddings", None) - self._original_beta = getattr(self, "beta", None) - self._original_responses = getattr(self, "responses", None) - - # Replace with wrapped versions (only if originals exist) - if self._original_chat is not None: - self.chat = AsyncWrappedChat(self, self._original_chat) - - if self._original_embeddings is not None: - self.embeddings = AsyncWrappedEmbeddings(self, self._original_embeddings) - - if self._original_beta is not None: - self.beta = AsyncWrappedBeta(self, self._original_beta) - - # Only add responses if available (newer OpenAI versions) - if self._original_responses is not None: - self.responses = AsyncWrappedResponses(self, self._original_responses) + _wrap_openai_resources(self, _ASYNC_RESOURCE_WRAPPERS) diff --git a/posthog/ai/openai/wrapper_utils.py b/posthog/ai/openai/wrapper_utils.py index 5cefab81..509f2d94 100644 --- a/posthog/ai/openai/wrapper_utils.py +++ b/posthog/ai/openai/wrapper_utils.py @@ -1,5 +1,5 @@ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, Mapping, Optional log = logging.getLogger("posthog") @@ -33,6 +33,15 @@ def merge_provider_override( return {**(posthog_properties or {}), "$ai_provider": posthog_provider_override} +def _wrap_openai_resources(client: Any, wrappers: Mapping[str, type]) -> None: + """Save and replace available SDK resources using an explicit wrapper mapping.""" + for resource_name, wrapper_type in wrappers.items(): + original = getattr(client, resource_name, None) + setattr(client, f"_original_{resource_name}", original) + if original is not None: + setattr(client, resource_name, wrapper_type(client, original)) + + def reset_fallback_warnings() -> None: _fallback_warnings.clear() diff --git a/posthog/test/ai/openai/test_async_parity.py b/posthog/test/ai/openai/test_async_parity.py index 6601197e..850b2a88 100644 --- a/posthog/test/ai/openai/test_async_parity.py +++ b/posthog/test/ai/openai/test_async_parity.py @@ -13,7 +13,7 @@ """ from types import SimpleNamespace -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -144,6 +144,53 @@ async def chunks(): assert async_props["$ai_provider"] == "groq" +@pytest.mark.asyncio +async def test_embedding_telemetry_has_sync_async_parity(mock_client): + response = SimpleNamespace(usage=SimpleNamespace(prompt_tokens=12, total_tokens=12)) + request = { + "model": "text-embedding-3-small", + "input": "private input", + "posthog_trace_id": "shared-trace", + "posthog_properties": {"custom": "value"}, + "posthog_privacy_mode": True, + "posthog_groups": {"company": "test-company"}, + "posthog_provider_override": "azure", + } + provider_request = { + "model": "text-embedding-3-small", + "input": "private input", + } + + with patch( + "openai.resources.embeddings.Embeddings.create", return_value=response + ) as sync_create: + client = OpenAI(api_key="test-key", posthog_client=mock_client) + assert client.embeddings.create(**request) is response + sync_create.assert_called_once_with(**provider_request) + sync_capture = mock_client.capture.call_args + + async_create = AsyncMock(return_value=response) + mock_client.capture.reset_mock() + with patch("openai.resources.embeddings.AsyncEmbeddings.create", new=async_create): + client = AsyncOpenAI(api_key="test-key", posthog_client=mock_client) + assert await client.embeddings.create(**request) is response + async_create.assert_awaited_once_with(**provider_request) + async_capture = mock_client.capture.call_args + + sync_props = sync_capture.kwargs["properties"] + async_props = async_capture.kwargs["properties"] + sync_without_latency = {k: v for k, v in sync_props.items() if k != "$ai_latency"} + async_without_latency = {k: v for k, v in async_props.items() if k != "$ai_latency"} + + assert async_without_latency == sync_without_latency + assert async_props["$ai_input"] is None + assert async_props["$ai_input_tokens"] == 12 + assert async_props["$ai_provider"] == "azure" + assert async_props["$process_person_profile"] is False + assert async_capture.kwargs["distinct_id"] == "shared-trace" + assert async_capture.kwargs["groups"] == {"company": "test-company"} + + def test_sync_stream_close_after_early_exit_captures_partial_state( mock_client, streaming_tool_call_chunks ): diff --git a/posthog/test/ai/openai/test_resource_wrapping.py b/posthog/test/ai/openai/test_resource_wrapping.py new file mode 100644 index 00000000..02bf7b09 --- /dev/null +++ b/posthog/test/ai/openai/test_resource_wrapping.py @@ -0,0 +1,68 @@ +from unittest.mock import MagicMock + +import pytest +from openai.resources.beta import AsyncBeta, Beta +from openai.resources.chat import AsyncChat, Chat +from openai.resources.embeddings import AsyncEmbeddings, Embeddings +from openai.resources.responses import AsyncResponses, Responses + +from posthog.ai.openai import AsyncAzureOpenAI, AsyncOpenAI, AzureOpenAI, OpenAI +from posthog.ai.openai.openai import _SYNC_RESOURCE_WRAPPERS +from posthog.ai.openai.openai_async import _ASYNC_RESOURCE_WRAPPERS + +_SYNC_RESOURCES = { + "chat": Chat, + "embeddings": Embeddings, + "beta": Beta, + "responses": Responses, +} +_ASYNC_RESOURCES = { + "chat": AsyncChat, + "embeddings": AsyncEmbeddings, + "beta": AsyncBeta, + "responses": AsyncResponses, +} +_AZURE_KWARGS = { + "api_key": "test-key", + "azure_endpoint": "https://example.openai.azure.com", + "api_version": "2024-02-01", +} + + +@pytest.mark.parametrize( + "client_type, client_kwargs, wrappers, resource_types", + [ + ( + OpenAI, + {"api_key": "test-key"}, + _SYNC_RESOURCE_WRAPPERS, + _SYNC_RESOURCES, + ), + ( + AsyncOpenAI, + {"api_key": "test-key"}, + _ASYNC_RESOURCE_WRAPPERS, + _ASYNC_RESOURCES, + ), + (AzureOpenAI, _AZURE_KWARGS, _SYNC_RESOURCE_WRAPPERS, _SYNC_RESOURCES), + ( + AsyncAzureOpenAI, + _AZURE_KWARGS, + _ASYNC_RESOURCE_WRAPPERS, + _ASYNC_RESOURCES, + ), + ], +) +def test_client_resources_are_discovered_and_wrapped( + client_type, client_kwargs, wrappers, resource_types +): + client = client_type(posthog_client=MagicMock(), **client_kwargs) + + for resource_name, wrapper_type in wrappers.items(): + wrapped = getattr(client, resource_name) + original = getattr(client, f"_original_{resource_name}") + + assert type(wrapped) is wrapper_type + assert type(original) is resource_types[resource_name] + assert wrapped._client is client + assert wrapped._original is original diff --git a/references/public_api_snapshot.txt b/references/public_api_snapshot.txt index 54ca98d0..3a414ae9 100644 --- a/references/public_api_snapshot.txt +++ b/references/public_api_snapshot.txt @@ -403,28 +403,28 @@ attribute posthog.ai.langchain.callbacks.SpanMetadata.latency: float attribute posthog.ai.langchain.callbacks.SpanMetadata.name: str attribute posthog.ai.langchain.callbacks.SpanMetadata.start_time: float attribute posthog.ai.langchain.callbacks.log = logging.getLogger('posthog') -attribute posthog.ai.openai.openai.OpenAI.beta = WrappedBeta(self, self._original_beta) -attribute posthog.ai.openai.openai.OpenAI.chat = WrappedChat(self, self._original_chat) -attribute posthog.ai.openai.openai.OpenAI.embeddings = WrappedEmbeddings(self, self._original_embeddings) -attribute posthog.ai.openai.openai.OpenAI.responses = WrappedResponses(self, self._original_responses) +attribute posthog.ai.openai.openai.OpenAI.beta: WrappedBeta +attribute posthog.ai.openai.openai.OpenAI.chat: WrappedChat +attribute posthog.ai.openai.openai.OpenAI.embeddings: WrappedEmbeddings +attribute posthog.ai.openai.openai.OpenAI.responses: WrappedResponses attribute posthog.ai.openai.openai.WrappedBeta.chat attribute posthog.ai.openai.openai.WrappedBetaChat.completions attribute posthog.ai.openai.openai.WrappedChat.completions -attribute posthog.ai.openai.openai_async.AsyncOpenAI.beta = WrappedBeta(self, self._original_beta) -attribute posthog.ai.openai.openai_async.AsyncOpenAI.chat = WrappedChat(self, self._original_chat) -attribute posthog.ai.openai.openai_async.AsyncOpenAI.embeddings = WrappedEmbeddings(self, self._original_embeddings) -attribute posthog.ai.openai.openai_async.AsyncOpenAI.responses = WrappedResponses(self, self._original_responses) +attribute posthog.ai.openai.openai_async.AsyncOpenAI.beta: WrappedBeta +attribute posthog.ai.openai.openai_async.AsyncOpenAI.chat: WrappedChat +attribute posthog.ai.openai.openai_async.AsyncOpenAI.embeddings: WrappedEmbeddings +attribute posthog.ai.openai.openai_async.AsyncOpenAI.responses: WrappedResponses attribute posthog.ai.openai.openai_async.WrappedBeta.chat attribute posthog.ai.openai.openai_async.WrappedBetaChat.completions attribute posthog.ai.openai.openai_async.WrappedChat.completions -attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.beta = AsyncWrappedBeta(self, self._original_beta) -attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.chat = AsyncWrappedChat(self, self._original_chat) -attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.embeddings = AsyncWrappedEmbeddings(self, self._original_embeddings) -attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.responses = AsyncWrappedResponses(self, self._original_responses) -attribute posthog.ai.openai.openai_providers.AzureOpenAI.beta = WrappedBeta(self, self._original_beta) -attribute posthog.ai.openai.openai_providers.AzureOpenAI.chat = WrappedChat(self, self._original_chat) -attribute posthog.ai.openai.openai_providers.AzureOpenAI.embeddings = WrappedEmbeddings(self, self._original_embeddings) -attribute posthog.ai.openai.openai_providers.AzureOpenAI.responses = WrappedResponses(self, self._original_responses) +attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.beta: AsyncWrappedBeta +attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.chat: AsyncWrappedChat +attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.embeddings: AsyncWrappedEmbeddings +attribute posthog.ai.openai.openai_providers.AsyncAzureOpenAI.responses: AsyncWrappedResponses +attribute posthog.ai.openai.openai_providers.AzureOpenAI.beta: WrappedBeta +attribute posthog.ai.openai.openai_providers.AzureOpenAI.chat: WrappedChat +attribute posthog.ai.openai.openai_providers.AzureOpenAI.embeddings: WrappedEmbeddings +attribute posthog.ai.openai.openai_providers.AzureOpenAI.responses: WrappedResponses attribute posthog.ai.openai.wrapper_utils.log = logging.getLogger('posthog') attribute posthog.ai.openai_agents.processor.log = logging.getLogger('posthog') attribute posthog.ai.otel.spans.AI_SPAN_PREFIXES = ('gen_ai.', 'llm.', 'ai.', 'traceloop.')