Skip to content

Commit 9f4ef4f

Browse files
authored
feat: composition over inheritance (#236)
1 parent 7aea6b7 commit 9f4ef4f

File tree

6 files changed

+280
-75
lines changed

6 files changed

+280
-75
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.1.0 - 2025-05-22
2+
3+
Moved ai openai package to a composition approach over inheritance.
4+
15
## 4.0.1 – 2025-04-29
26

37
1. Remove deprecated `monotonic` library. Use Python's core `time.monotonic` function instead

posthog/ai/openai/openai.py

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
try:
66
import openai
7-
import openai.resources
87
except ImportError:
98
raise ModuleNotFoundError("Please install the OpenAI SDK to use this feature: 'pip install openai'")
109

@@ -29,14 +28,37 @@ def __init__(self, posthog_client: PostHogClient, **kwargs):
2928
"""
3029
super().__init__(**kwargs)
3130
self._ph_client = posthog_client
32-
self.chat = WrappedChat(self)
33-
self.embeddings = WrappedEmbeddings(self)
34-
self.beta = WrappedBeta(self)
35-
self.responses = WrappedResponses(self)
3631

32+
# Store original objects after parent initialization (only if they exist)
33+
self._original_chat = getattr(self, "chat", None)
34+
self._original_embeddings = getattr(self, "embeddings", None)
35+
self._original_beta = getattr(self, "beta", None)
36+
self._original_responses = getattr(self, "responses", None)
3737

38-
class WrappedResponses(openai.resources.responses.Responses):
39-
_client: OpenAI
38+
# Replace with wrapped versions (only if originals exist)
39+
if self._original_chat is not None:
40+
self.chat = WrappedChat(self, self._original_chat)
41+
42+
if self._original_embeddings is not None:
43+
self.embeddings = WrappedEmbeddings(self, self._original_embeddings)
44+
45+
if self._original_beta is not None:
46+
self.beta = WrappedBeta(self, self._original_beta)
47+
48+
if self._original_responses is not None:
49+
self.responses = WrappedResponses(self, self._original_responses)
50+
51+
52+
class WrappedResponses:
53+
"""Wrapper for OpenAI responses that tracks usage in PostHog."""
54+
55+
def __init__(self, client: OpenAI, original_responses):
56+
self._client = client
57+
self._original = original_responses
58+
59+
def __getattr__(self, name):
60+
"""Fallback to original responses object for any methods we don't explicitly handle."""
61+
return getattr(self._original, name)
4062

4163
def create(
4264
self,
@@ -69,7 +91,7 @@ def create(
6991
posthog_privacy_mode,
7092
posthog_groups,
7193
self._client.base_url,
72-
super().create,
94+
self._original.create,
7395
**kwargs,
7496
)
7597

@@ -85,7 +107,7 @@ def _create_streaming(
85107
start_time = time.time()
86108
usage_stats: Dict[str, int] = {}
87109
final_content = []
88-
response = super().create(**kwargs)
110+
response = self._original.create(**kwargs)
89111

90112
def generator():
91113
nonlocal usage_stats
@@ -195,16 +217,32 @@ def _capture_streaming_event(
195217
)
196218

197219

198-
class WrappedChat(openai.resources.chat.Chat):
199-
_client: OpenAI
220+
class WrappedChat:
221+
"""Wrapper for OpenAI chat that tracks usage in PostHog."""
222+
223+
def __init__(self, client: OpenAI, original_chat):
224+
self._client = client
225+
self._original = original_chat
226+
227+
def __getattr__(self, name):
228+
"""Fallback to original chat object for any methods we don't explicitly handle."""
229+
return getattr(self._original, name)
200230

201231
@property
202232
def completions(self):
203-
return WrappedCompletions(self._client)
233+
return WrappedCompletions(self._client, self._original.completions)
234+
204235

236+
class WrappedCompletions:
237+
"""Wrapper for OpenAI chat completions that tracks usage in PostHog."""
205238

206-
class WrappedCompletions(openai.resources.chat.completions.Completions):
207-
_client: OpenAI
239+
def __init__(self, client: OpenAI, original_completions):
240+
self._client = client
241+
self._original = original_completions
242+
243+
def __getattr__(self, name):
244+
"""Fallback to original completions object for any methods we don't explicitly handle."""
245+
return getattr(self._original, name)
208246

209247
def create(
210248
self,
@@ -237,7 +275,7 @@ def create(
237275
posthog_privacy_mode,
238276
posthog_groups,
239277
self._client.base_url,
240-
super().create,
278+
self._original.create,
241279
**kwargs,
242280
)
243281

@@ -257,7 +295,7 @@ def _create_streaming(
257295
if "stream_options" not in kwargs:
258296
kwargs["stream_options"] = {}
259297
kwargs["stream_options"]["include_usage"] = True
260-
response = super().create(**kwargs)
298+
response = self._original.create(**kwargs)
261299

262300
def generator():
263301
nonlocal usage_stats
@@ -383,8 +421,16 @@ def _capture_streaming_event(
383421
)
384422

385423

386-
class WrappedEmbeddings(openai.resources.embeddings.Embeddings):
387-
_client: OpenAI
424+
class WrappedEmbeddings:
425+
"""Wrapper for OpenAI embeddings that tracks usage in PostHog."""
426+
427+
def __init__(self, client: OpenAI, original_embeddings):
428+
self._client = client
429+
self._original = original_embeddings
430+
431+
def __getattr__(self, name):
432+
"""Fallback to original embeddings object for any methods we don't explicitly handle."""
433+
return getattr(self._original, name)
388434

389435
def create(
390436
self,
@@ -402,6 +448,8 @@ def create(
402448
posthog_distinct_id: Optional ID to associate with the usage event.
403449
posthog_trace_id: Optional trace UUID for linking events.
404450
posthog_properties: Optional dictionary of extra properties to include in the event.
451+
posthog_privacy_mode: Whether to anonymize the input and output.
452+
posthog_groups: Optional dictionary of groups to associate with the event.
405453
**kwargs: Any additional parameters for the OpenAI Embeddings API.
406454
407455
Returns:
@@ -411,7 +459,7 @@ def create(
411459
posthog_trace_id = str(uuid.uuid4())
412460

413461
start_time = time.time()
414-
response = super().create(**kwargs)
462+
response = self._original.create(**kwargs)
415463
end_time = time.time()
416464

417465
# Extract usage statistics if available
@@ -452,24 +500,48 @@ def create(
452500
return response
453501

454502

455-
class WrappedBeta(openai.resources.beta.Beta):
456-
_client: OpenAI
503+
class WrappedBeta:
504+
"""Wrapper for OpenAI beta features that tracks usage in PostHog."""
505+
506+
def __init__(self, client: OpenAI, original_beta):
507+
self._client = client
508+
self._original = original_beta
509+
510+
def __getattr__(self, name):
511+
"""Fallback to original beta object for any methods we don't explicitly handle."""
512+
return getattr(self._original, name)
457513

458514
@property
459515
def chat(self):
460-
return WrappedBetaChat(self._client)
516+
return WrappedBetaChat(self._client, self._original.chat)
461517

462518

463-
class WrappedBetaChat(openai.resources.beta.chat.Chat):
464-
_client: OpenAI
519+
class WrappedBetaChat:
520+
"""Wrapper for OpenAI beta chat that tracks usage in PostHog."""
521+
522+
def __init__(self, client: OpenAI, original_beta_chat):
523+
self._client = client
524+
self._original = original_beta_chat
525+
526+
def __getattr__(self, name):
527+
"""Fallback to original beta chat object for any methods we don't explicitly handle."""
528+
return getattr(self._original, name)
465529

466530
@property
467531
def completions(self):
468-
return WrappedBetaCompletions(self._client)
532+
return WrappedBetaCompletions(self._client, self._original.completions)
533+
534+
535+
class WrappedBetaCompletions:
536+
"""Wrapper for OpenAI beta chat completions that tracks usage in PostHog."""
469537

538+
def __init__(self, client: OpenAI, original_beta_completions):
539+
self._client = client
540+
self._original = original_beta_completions
470541

471-
class WrappedBetaCompletions(openai.resources.beta.chat.completions.Completions):
472-
_client: OpenAI
542+
def __getattr__(self, name):
543+
"""Fallback to original beta completions object for any methods we don't explicitly handle."""
544+
return getattr(self._original, name)
473545

474546
def parse(
475547
self,
@@ -489,6 +561,6 @@ def parse(
489561
posthog_privacy_mode,
490562
posthog_groups,
491563
self._client.base_url,
492-
super().parse,
564+
self._original.parse,
493565
**kwargs,
494566
)

0 commit comments

Comments
 (0)