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
5 changes: 5 additions & 0 deletions .sampo/changesets/wily-shaman-lempo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
pypi/posthog: patch
---

Track OpenAI chat completions parse calls
78 changes: 68 additions & 10 deletions posthog/ai/openai/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
from posthog.client import Client as PostHogClient
from posthog import setup
from posthog.ai.openai.wrapper_utils import warn_on_fallback


class OpenAI(openai.OpenAI):
Expand Down Expand Up @@ -67,6 +68,29 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
self.responses = WrappedResponses(self, self._original_responses)


def _parse_and_track(
wrapper,
posthog_distinct_id: Optional[str],
posthog_trace_id: Optional[str],
posthog_properties: Optional[Dict[str, Any]],
posthog_privacy_mode: bool,
posthog_groups: Optional[Dict[str, Any]],
**kwargs: Any,
):
return call_llm_and_track_usage(
posthog_distinct_id,
wrapper._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
wrapper._client.base_url,
wrapper._original.parse,
**kwargs,
)


class WrappedResponses:
"""Wrapper for OpenAI responses that tracks usage in PostHog."""

Expand All @@ -76,6 +100,7 @@ def __init__(self, client: OpenAI, original_responses):

def __getattr__(self, name):
"""Fallback to original responses object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

def create(
Expand Down Expand Up @@ -276,16 +301,13 @@ def parse(
Returns:
The response from OpenAI's responses.parse call.
"""
return call_llm_and_track_usage(
return _parse_and_track(
self,
posthog_distinct_id,
self._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
self._client.base_url,
self._original.parse,
**kwargs,
)

Expand All @@ -299,6 +321,7 @@ def __init__(self, client: OpenAI, original_chat):

def __getattr__(self, name):
"""Fallback to original chat object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -316,8 +339,42 @@ def __init__(self, client: OpenAI, original_completions):

def __getattr__(self, name):
"""Fallback to original completions object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

def parse(
self,
posthog_distinct_id: Optional[str] = None,
posthog_trace_id: Optional[str] = None,
posthog_properties: Optional[Dict[str, Any]] = None,
posthog_privacy_mode: bool = False,
posthog_groups: Optional[Dict[str, Any]] = None,
**kwargs: Any,
):
"""
Parse an OpenAI chat completion while tracking usage in PostHog.

Args:
posthog_distinct_id: Optional distinct ID to associate with the usage event.
posthog_trace_id: Optional trace ID. Generated automatically when omitted.
posthog_properties: Additional properties to include with the usage event.
posthog_privacy_mode: Whether to redact captured input and output.
posthog_groups: Optional PostHog groups to associate with the event.
**kwargs: Arguments passed to OpenAI's ``chat.completions.parse`` API.

Returns:
The parsed response from OpenAI.
"""
return _parse_and_track(
self,
posthog_distinct_id,
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
**kwargs,
)

def create(
self,
posthog_distinct_id: Optional[str] = None,
Expand Down Expand Up @@ -518,6 +575,7 @@ def __init__(self, client: OpenAI, original_embeddings):

def __getattr__(self, name):
"""Fallback to original embeddings object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

def create(
Expand Down Expand Up @@ -602,6 +660,7 @@ def __init__(self, client: OpenAI, original_beta):

def __getattr__(self, name):
"""Fallback to original beta object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -619,6 +678,7 @@ def __init__(self, client: OpenAI, original_beta_chat):

def __getattr__(self, name):
"""Fallback to original beta chat object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -636,6 +696,7 @@ def __init__(self, client: OpenAI, original_beta_completions):

def __getattr__(self, name):
"""Fallback to original beta completions object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

def parse(
Expand All @@ -661,15 +722,12 @@ def parse(
Returns:
The parsed response from OpenAI.
"""
return call_llm_and_track_usage(
return _parse_and_track(
self,
posthog_distinct_id,
self._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
self._client.base_url,
self._original.parse,
**kwargs,
)
83 changes: 68 additions & 15 deletions posthog/ai/openai/openai_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
from posthog.client import Client as PostHogClient
from posthog.ai.openai.wrapper_utils import warn_on_fallback


class AsyncOpenAI(openai.AsyncOpenAI):
Expand Down Expand Up @@ -69,6 +70,29 @@ def __init__(self, posthog_client: Optional[PostHogClient] = None, **kwargs):
self.responses = WrappedResponses(self, self._original_responses)


async def _parse_and_track(
wrapper,
posthog_distinct_id: Optional[str],
posthog_trace_id: Optional[str],
posthog_properties: Optional[Dict[str, Any]],
posthog_privacy_mode: bool,
posthog_groups: Optional[Dict[str, Any]],
**kwargs: Any,
):
return await call_llm_and_track_usage_async(
posthog_distinct_id,
wrapper._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
wrapper._client.base_url,
wrapper._original.parse,
**kwargs,
)


class WrappedResponses:
"""Async wrapper for OpenAI responses that tracks usage in PostHog."""

Expand All @@ -78,7 +102,7 @@ def __init__(self, client: AsyncOpenAI, original_responses):

def __getattr__(self, name):
"""Fallback to original responses object for any methods we don't explicitly handle."""

warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

async def create(
Expand Down Expand Up @@ -305,16 +329,13 @@ async def parse(
Returns:
The response from OpenAI's responses.parse call.
"""
return await call_llm_and_track_usage_async(
return await _parse_and_track(
self,
posthog_distinct_id,
self._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
self._client.base_url,
self._original.parse,
**kwargs,
)

Expand All @@ -328,6 +349,7 @@ def __init__(self, client: AsyncOpenAI, original_chat):

def __getattr__(self, name):
"""Fallback to original chat object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -345,8 +367,42 @@ def __init__(self, client: AsyncOpenAI, original_completions):

def __getattr__(self, name):
"""Fallback to original completions object for any methods we don't explicitly handle."""
warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

async def parse(
self,
posthog_distinct_id: Optional[str] = None,
posthog_trace_id: Optional[str] = None,
posthog_properties: Optional[Dict[str, Any]] = None,
posthog_privacy_mode: bool = False,
posthog_groups: Optional[Dict[str, Any]] = None,
**kwargs: Any,
):
"""
Parse an OpenAI chat completion while tracking usage in PostHog.

Args:
posthog_distinct_id: Optional distinct ID to associate with the usage event.
posthog_trace_id: Optional trace ID. Generated automatically when omitted.
posthog_properties: Additional properties to include with the usage event.
posthog_privacy_mode: Whether to redact captured input and output.
posthog_groups: Optional PostHog groups to associate with the event.
**kwargs: Arguments passed to OpenAI's async ``chat.completions.parse`` API.

Returns:
The parsed response from OpenAI.
"""
return await _parse_and_track(
self,
posthog_distinct_id,
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
**kwargs,
)

async def create(
self,
posthog_distinct_id: Optional[str] = None,
Expand Down Expand Up @@ -574,7 +630,7 @@ def __init__(self, client: AsyncOpenAI, original_embeddings):

def __getattr__(self, name):
"""Fallback to original embeddings object for any methods we don't explicitly handle."""

warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

async def create(
Expand Down Expand Up @@ -660,7 +716,7 @@ def __init__(self, client: AsyncOpenAI, original_beta):

def __getattr__(self, name):
"""Fallback to original beta object for any methods we don't explicitly handle."""

warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -678,7 +734,7 @@ def __init__(self, client: AsyncOpenAI, original_beta_chat):

def __getattr__(self, name):
"""Fallback to original beta chat object for any methods we don't explicitly handle."""

warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

@property
Expand All @@ -696,7 +752,7 @@ def __init__(self, client: AsyncOpenAI, original_beta_completions):

def __getattr__(self, name):
"""Fallback to original beta completions object for any methods we don't explicitly handle."""

warn_on_fallback(self.__class__.__name__, name)
return getattr(self._original, name)

async def parse(
Expand All @@ -722,15 +778,12 @@ async def parse(
Returns:
The parsed response from OpenAI.
"""
return await call_llm_and_track_usage_async(
return await _parse_and_track(
self,
posthog_distinct_id,
self._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
self._client.base_url,
self._original.parse,
**kwargs,
)
23 changes: 23 additions & 0 deletions posthog/ai/openai/wrapper_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import logging


log = logging.getLogger("posthog")
_fallback_warnings: set[tuple[str, str]] = set()
Comment thread
marandaneto marked this conversation as resolved.


def reset_fallback_warnings() -> None:
_fallback_warnings.clear()


def warn_on_fallback(wrapper_name: str, name: str) -> None:
key = (wrapper_name, name)
if key in _fallback_warnings:
return

_fallback_warnings.add(key)
log.warning(
"Falling back to unwrapped OpenAI API for %s.%s; PostHog LLM tracking "
"and posthog_* arguments will not be applied.",
wrapper_name,
name,
)
Loading
Loading