posthog 6.7.1__py3-none-any.whl → 6.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,9 +12,15 @@ except ImportError:
12
12
  from posthog.ai.utils import (
13
13
  call_llm_and_track_usage,
14
14
  extract_available_tool_calls,
15
- get_model_params,
15
+ merge_usage_stats,
16
16
  with_privacy_mode,
17
17
  )
18
+ from posthog.ai.openai.openai_converter import (
19
+ extract_openai_usage_from_chunk,
20
+ extract_openai_content_from_chunk,
21
+ extract_openai_tool_calls_from_chunk,
22
+ accumulate_openai_tool_calls,
23
+ )
18
24
  from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
19
25
  from posthog.client import Client as PostHogClient
20
26
  from posthog import setup
@@ -34,6 +40,7 @@ class OpenAI(openai.OpenAI):
34
40
  posthog_client: If provided, events will be captured via this client instead of the global `posthog`.
35
41
  **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
36
42
  """
43
+
37
44
  super().__init__(**kwargs)
38
45
  self._ph_client = posthog_client or setup()
39
46
 
@@ -123,35 +130,17 @@ class WrappedResponses:
123
130
 
124
131
  try:
125
132
  for chunk in response:
126
- if hasattr(chunk, "type") and chunk.type == "response.completed":
127
- res = chunk.response
128
- if res.output and len(res.output) > 0:
129
- final_content.append(res.output[0])
130
-
131
- if hasattr(chunk, "usage") and chunk.usage:
132
- usage_stats = {
133
- k: getattr(chunk.usage, k, 0)
134
- for k in [
135
- "input_tokens",
136
- "output_tokens",
137
- "total_tokens",
138
- ]
139
- }
140
-
141
- # Add support for cached tokens
142
- if hasattr(chunk.usage, "output_tokens_details") and hasattr(
143
- chunk.usage.output_tokens_details, "reasoning_tokens"
144
- ):
145
- usage_stats["reasoning_tokens"] = (
146
- chunk.usage.output_tokens_details.reasoning_tokens
147
- )
148
-
149
- if hasattr(chunk.usage, "input_tokens_details") and hasattr(
150
- chunk.usage.input_tokens_details, "cached_tokens"
151
- ):
152
- usage_stats["cache_read_input_tokens"] = (
153
- chunk.usage.input_tokens_details.cached_tokens
154
- )
133
+ # Extract usage stats from chunk
134
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "responses")
135
+
136
+ if chunk_usage:
137
+ merge_usage_stats(usage_stats, chunk_usage)
138
+
139
+ # Extract content from chunk
140
+ content = extract_openai_content_from_chunk(chunk, "responses")
141
+
142
+ if content is not None:
143
+ final_content.append(content)
155
144
 
156
145
  yield chunk
157
146
 
@@ -169,7 +158,7 @@ class WrappedResponses:
169
158
  usage_stats,
170
159
  latency,
171
160
  output,
172
- extract_available_tool_calls("openai", kwargs),
161
+ None, # Responses API doesn't have tools
173
162
  )
174
163
 
175
164
  return generator()
@@ -187,49 +176,36 @@ class WrappedResponses:
187
176
  output: Any,
188
177
  available_tool_calls: Optional[List[Dict[str, Any]]] = None,
189
178
  ):
190
- if posthog_trace_id is None:
191
- posthog_trace_id = str(uuid.uuid4())
192
-
193
- event_properties = {
194
- "$ai_provider": "openai",
195
- "$ai_model": kwargs.get("model"),
196
- "$ai_model_parameters": get_model_params(kwargs),
197
- "$ai_input": with_privacy_mode(
198
- self._client._ph_client,
199
- posthog_privacy_mode,
200
- sanitize_openai_response(kwargs.get("input")),
201
- ),
202
- "$ai_output_choices": with_privacy_mode(
203
- self._client._ph_client,
204
- posthog_privacy_mode,
205
- output,
206
- ),
207
- "$ai_http_status": 200,
208
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
209
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
210
- "$ai_cache_read_input_tokens": usage_stats.get(
211
- "cache_read_input_tokens", 0
212
- ),
213
- "$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
214
- "$ai_latency": latency,
215
- "$ai_trace_id": posthog_trace_id,
216
- "$ai_base_url": str(self._client.base_url),
217
- **(posthog_properties or {}),
218
- }
219
-
220
- if available_tool_calls:
221
- event_properties["$ai_tools"] = available_tool_calls
222
-
223
- if posthog_distinct_id is None:
224
- event_properties["$process_person_profile"] = False
179
+ from posthog.ai.types import StreamingEventData
180
+ from posthog.ai.openai.openai_converter import (
181
+ standardize_openai_usage,
182
+ format_openai_streaming_input,
183
+ format_openai_streaming_output,
184
+ )
185
+ from posthog.ai.utils import capture_streaming_event
186
+
187
+ # Prepare standardized event data
188
+ formatted_input = format_openai_streaming_input(kwargs, "responses")
189
+ sanitized_input = sanitize_openai_response(formatted_input)
190
+
191
+ event_data = StreamingEventData(
192
+ provider="openai",
193
+ model=kwargs.get("model", "unknown"),
194
+ base_url=str(self._client.base_url),
195
+ kwargs=kwargs,
196
+ formatted_input=sanitized_input,
197
+ formatted_output=format_openai_streaming_output(output, "responses"),
198
+ usage_stats=standardize_openai_usage(usage_stats, "responses"),
199
+ latency=latency,
200
+ distinct_id=posthog_distinct_id,
201
+ trace_id=posthog_trace_id,
202
+ properties=posthog_properties,
203
+ privacy_mode=posthog_privacy_mode,
204
+ groups=posthog_groups,
205
+ )
225
206
 
226
- if hasattr(self._client._ph_client, "capture"):
227
- self._client._ph_client.capture(
228
- distinct_id=posthog_distinct_id or posthog_trace_id,
229
- event="$ai_generation",
230
- properties=event_properties,
231
- groups=posthog_groups,
232
- )
207
+ # Use the common capture function
208
+ capture_streaming_event(self._client._ph_client, event_data)
233
209
 
234
210
  def parse(
235
211
  self,
@@ -342,6 +318,7 @@ class WrappedCompletions:
342
318
  start_time = time.time()
343
319
  usage_stats: Dict[str, int] = {}
344
320
  accumulated_content = []
321
+ accumulated_tool_calls: Dict[int, Dict[str, Any]] = {}
345
322
  if "stream_options" not in kwargs:
346
323
  kwargs["stream_options"] = {}
347
324
  kwargs["stream_options"]["include_usage"] = True
@@ -350,50 +327,42 @@ class WrappedCompletions:
350
327
  def generator():
351
328
  nonlocal usage_stats
352
329
  nonlocal accumulated_content # noqa: F824
330
+ nonlocal accumulated_tool_calls
353
331
 
354
332
  try:
355
333
  for chunk in response:
356
- if hasattr(chunk, "usage") and chunk.usage:
357
- usage_stats = {
358
- k: getattr(chunk.usage, k, 0)
359
- for k in [
360
- "prompt_tokens",
361
- "completion_tokens",
362
- "total_tokens",
363
- ]
364
- }
365
-
366
- # Add support for cached tokens
367
- if hasattr(chunk.usage, "prompt_tokens_details") and hasattr(
368
- chunk.usage.prompt_tokens_details, "cached_tokens"
369
- ):
370
- usage_stats["cache_read_input_tokens"] = (
371
- chunk.usage.prompt_tokens_details.cached_tokens
372
- )
373
-
374
- if hasattr(chunk.usage, "output_tokens_details") and hasattr(
375
- chunk.usage.output_tokens_details, "reasoning_tokens"
376
- ):
377
- usage_stats["reasoning_tokens"] = (
378
- chunk.usage.output_tokens_details.reasoning_tokens
379
- )
380
-
381
- if (
382
- hasattr(chunk, "choices")
383
- and chunk.choices
384
- and len(chunk.choices) > 0
385
- ):
386
- if chunk.choices[0].delta and chunk.choices[0].delta.content:
387
- content = chunk.choices[0].delta.content
388
- if content:
389
- accumulated_content.append(content)
334
+ # Extract usage stats from chunk
335
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "chat")
336
+
337
+ if chunk_usage:
338
+ merge_usage_stats(usage_stats, chunk_usage)
339
+
340
+ # Extract content from chunk
341
+ content = extract_openai_content_from_chunk(chunk, "chat")
342
+
343
+ if content is not None:
344
+ accumulated_content.append(content)
345
+
346
+ # Extract and accumulate tool calls from chunk
347
+ chunk_tool_calls = extract_openai_tool_calls_from_chunk(chunk)
348
+ if chunk_tool_calls:
349
+ accumulate_openai_tool_calls(
350
+ accumulated_tool_calls, chunk_tool_calls
351
+ )
390
352
 
391
353
  yield chunk
392
354
 
393
355
  finally:
394
356
  end_time = time.time()
395
357
  latency = end_time - start_time
396
- output = "".join(accumulated_content)
358
+
359
+ # Convert accumulated tool calls dict to list
360
+ tool_calls_list = (
361
+ list(accumulated_tool_calls.values())
362
+ if accumulated_tool_calls
363
+ else None
364
+ )
365
+
397
366
  self._capture_streaming_event(
398
367
  posthog_distinct_id,
399
368
  posthog_trace_id,
@@ -403,7 +372,8 @@ class WrappedCompletions:
403
372
  kwargs,
404
373
  usage_stats,
405
374
  latency,
406
- output,
375
+ accumulated_content,
376
+ tool_calls_list,
407
377
  extract_available_tool_calls("openai", kwargs),
408
378
  )
409
379
 
@@ -420,51 +390,39 @@ class WrappedCompletions:
420
390
  usage_stats: Dict[str, int],
421
391
  latency: float,
422
392
  output: Any,
393
+ tool_calls: Optional[List[Dict[str, Any]]] = None,
423
394
  available_tool_calls: Optional[List[Dict[str, Any]]] = None,
424
395
  ):
425
- if posthog_trace_id is None:
426
- posthog_trace_id = str(uuid.uuid4())
427
-
428
- event_properties = {
429
- "$ai_provider": "openai",
430
- "$ai_model": kwargs.get("model"),
431
- "$ai_model_parameters": get_model_params(kwargs),
432
- "$ai_input": with_privacy_mode(
433
- self._client._ph_client,
434
- posthog_privacy_mode,
435
- sanitize_openai(kwargs.get("messages")),
436
- ),
437
- "$ai_output_choices": with_privacy_mode(
438
- self._client._ph_client,
439
- posthog_privacy_mode,
440
- [{"content": output, "role": "assistant"}],
441
- ),
442
- "$ai_http_status": 200,
443
- "$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
444
- "$ai_output_tokens": usage_stats.get("completion_tokens", 0),
445
- "$ai_cache_read_input_tokens": usage_stats.get(
446
- "cache_read_input_tokens", 0
447
- ),
448
- "$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
449
- "$ai_latency": latency,
450
- "$ai_trace_id": posthog_trace_id,
451
- "$ai_base_url": str(self._client.base_url),
452
- **(posthog_properties or {}),
453
- }
454
-
455
- if available_tool_calls:
456
- event_properties["$ai_tools"] = available_tool_calls
457
-
458
- if posthog_distinct_id is None:
459
- event_properties["$process_person_profile"] = False
396
+ from posthog.ai.types import StreamingEventData
397
+ from posthog.ai.openai.openai_converter import (
398
+ standardize_openai_usage,
399
+ format_openai_streaming_input,
400
+ format_openai_streaming_output,
401
+ )
402
+ from posthog.ai.utils import capture_streaming_event
403
+
404
+ # Prepare standardized event data
405
+ formatted_input = format_openai_streaming_input(kwargs, "chat")
406
+ sanitized_input = sanitize_openai(formatted_input)
407
+
408
+ event_data = StreamingEventData(
409
+ provider="openai",
410
+ model=kwargs.get("model", "unknown"),
411
+ base_url=str(self._client.base_url),
412
+ kwargs=kwargs,
413
+ formatted_input=sanitized_input,
414
+ formatted_output=format_openai_streaming_output(output, "chat", tool_calls),
415
+ usage_stats=standardize_openai_usage(usage_stats, "chat"),
416
+ latency=latency,
417
+ distinct_id=posthog_distinct_id,
418
+ trace_id=posthog_trace_id,
419
+ properties=posthog_properties,
420
+ privacy_mode=posthog_privacy_mode,
421
+ groups=posthog_groups,
422
+ )
460
423
 
461
- if hasattr(self._client._ph_client, "capture"):
462
- self._client._ph_client.capture(
463
- distinct_id=posthog_distinct_id or posthog_trace_id,
464
- event="$ai_generation",
465
- properties=event_properties,
466
- groups=posthog_groups,
467
- )
424
+ # Use the common capture function
425
+ capture_streaming_event(self._client._ph_client, event_data)
468
426
 
469
427
 
470
428
  class WrappedEmbeddings:
@@ -501,6 +459,7 @@ class WrappedEmbeddings:
501
459
  Returns:
502
460
  The response from OpenAI's embeddings.create call.
503
461
  """
462
+
504
463
  if posthog_trace_id is None:
505
464
  posthog_trace_id = str(uuid.uuid4())
506
465
 
@@ -14,8 +14,16 @@ from posthog.ai.utils import (
14
14
  call_llm_and_track_usage_async,
15
15
  extract_available_tool_calls,
16
16
  get_model_params,
17
+ merge_usage_stats,
17
18
  with_privacy_mode,
18
19
  )
20
+ from posthog.ai.openai.openai_converter import (
21
+ extract_openai_usage_from_chunk,
22
+ extract_openai_content_from_chunk,
23
+ extract_openai_tool_calls_from_chunk,
24
+ accumulate_openai_tool_calls,
25
+ format_openai_streaming_output,
26
+ )
19
27
  from posthog.ai.sanitization import sanitize_openai, sanitize_openai_response
20
28
  from posthog.client import Client as PostHogClient
21
29
 
@@ -35,6 +43,7 @@ class AsyncOpenAI(openai.AsyncOpenAI):
35
43
  of the global posthog.
36
44
  **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
37
45
  """
46
+
38
47
  super().__init__(**kwargs)
39
48
  self._ph_client = posthog_client or setup()
40
49
 
@@ -67,6 +76,7 @@ class WrappedResponses:
67
76
 
68
77
  def __getattr__(self, name):
69
78
  """Fallback to original responses object for any methods we don't explicitly handle."""
79
+
70
80
  return getattr(self._original, name)
71
81
 
72
82
  async def create(
@@ -116,7 +126,7 @@ class WrappedResponses:
116
126
  start_time = time.time()
117
127
  usage_stats: Dict[str, int] = {}
118
128
  final_content = []
119
- response = await self._original.create(**kwargs)
129
+ response = self._original.create(**kwargs)
120
130
 
121
131
  async def async_generator():
122
132
  nonlocal usage_stats
@@ -124,35 +134,17 @@ class WrappedResponses:
124
134
 
125
135
  try:
126
136
  async for chunk in response:
127
- if hasattr(chunk, "type") and chunk.type == "response.completed":
128
- res = chunk.response
129
- if res.output and len(res.output) > 0:
130
- final_content.append(res.output[0])
131
-
132
- if hasattr(chunk, "usage") and chunk.usage:
133
- usage_stats = {
134
- k: getattr(chunk.usage, k, 0)
135
- for k in [
136
- "input_tokens",
137
- "output_tokens",
138
- "total_tokens",
139
- ]
140
- }
141
-
142
- # Add support for cached tokens
143
- if hasattr(chunk.usage, "output_tokens_details") and hasattr(
144
- chunk.usage.output_tokens_details, "reasoning_tokens"
145
- ):
146
- usage_stats["reasoning_tokens"] = (
147
- chunk.usage.output_tokens_details.reasoning_tokens
148
- )
149
-
150
- if hasattr(chunk.usage, "input_tokens_details") and hasattr(
151
- chunk.usage.input_tokens_details, "cached_tokens"
152
- ):
153
- usage_stats["cache_read_input_tokens"] = (
154
- chunk.usage.input_tokens_details.cached_tokens
155
- )
137
+ # Extract usage stats from chunk
138
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "responses")
139
+
140
+ if chunk_usage:
141
+ merge_usage_stats(usage_stats, chunk_usage)
142
+
143
+ # Extract content from chunk
144
+ content = extract_openai_content_from_chunk(chunk, "responses")
145
+
146
+ if content is not None:
147
+ final_content.append(content)
156
148
 
157
149
  yield chunk
158
150
 
@@ -160,6 +152,7 @@ class WrappedResponses:
160
152
  end_time = time.time()
161
153
  latency = end_time - start_time
162
154
  output = final_content
155
+
163
156
  await self._capture_streaming_event(
164
157
  posthog_distinct_id,
165
158
  posthog_trace_id,
@@ -203,7 +196,7 @@ class WrappedResponses:
203
196
  "$ai_output_choices": with_privacy_mode(
204
197
  self._client._ph_client,
205
198
  posthog_privacy_mode,
206
- output,
199
+ format_openai_streaming_output(output, "responses"),
207
200
  ),
208
201
  "$ai_http_status": 200,
209
202
  "$ai_input_tokens": usage_stats.get("input_tokens", 0),
@@ -345,59 +338,50 @@ class WrappedCompletions:
345
338
  start_time = time.time()
346
339
  usage_stats: Dict[str, int] = {}
347
340
  accumulated_content = []
341
+ accumulated_tool_calls: Dict[int, Dict[str, Any]] = {}
348
342
 
349
343
  if "stream_options" not in kwargs:
350
344
  kwargs["stream_options"] = {}
351
345
  kwargs["stream_options"]["include_usage"] = True
352
- response = await self._original.create(**kwargs)
346
+ response = self._original.create(**kwargs)
353
347
 
354
348
  async def async_generator():
355
349
  nonlocal usage_stats
356
350
  nonlocal accumulated_content # noqa: F824
351
+ nonlocal accumulated_tool_calls
357
352
 
358
353
  try:
359
354
  async for chunk in response:
360
- if hasattr(chunk, "usage") and chunk.usage:
361
- usage_stats = {
362
- k: getattr(chunk.usage, k, 0)
363
- for k in [
364
- "prompt_tokens",
365
- "completion_tokens",
366
- "total_tokens",
367
- ]
368
- }
369
-
370
- # Add support for cached tokens
371
- if hasattr(chunk.usage, "prompt_tokens_details") and hasattr(
372
- chunk.usage.prompt_tokens_details, "cached_tokens"
373
- ):
374
- usage_stats["cache_read_input_tokens"] = (
375
- chunk.usage.prompt_tokens_details.cached_tokens
376
- )
377
-
378
- if hasattr(chunk.usage, "output_tokens_details") and hasattr(
379
- chunk.usage.output_tokens_details, "reasoning_tokens"
380
- ):
381
- usage_stats["reasoning_tokens"] = (
382
- chunk.usage.output_tokens_details.reasoning_tokens
383
- )
384
-
385
- if (
386
- hasattr(chunk, "choices")
387
- and chunk.choices
388
- and len(chunk.choices) > 0
389
- ):
390
- if chunk.choices[0].delta and chunk.choices[0].delta.content:
391
- content = chunk.choices[0].delta.content
392
- if content:
393
- accumulated_content.append(content)
355
+ # Extract usage stats from chunk
356
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "chat")
357
+ if chunk_usage:
358
+ merge_usage_stats(usage_stats, chunk_usage)
359
+
360
+ # Extract content from chunk
361
+ content = extract_openai_content_from_chunk(chunk, "chat")
362
+ if content is not None:
363
+ accumulated_content.append(content)
364
+
365
+ # Extract and accumulate tool calls from chunk
366
+ chunk_tool_calls = extract_openai_tool_calls_from_chunk(chunk)
367
+ if chunk_tool_calls:
368
+ accumulate_openai_tool_calls(
369
+ accumulated_tool_calls, chunk_tool_calls
370
+ )
394
371
 
395
372
  yield chunk
396
373
 
397
374
  finally:
398
375
  end_time = time.time()
399
376
  latency = end_time - start_time
400
- output = "".join(accumulated_content)
377
+
378
+ # Convert accumulated tool calls dict to list
379
+ tool_calls_list = (
380
+ list(accumulated_tool_calls.values())
381
+ if accumulated_tool_calls
382
+ else None
383
+ )
384
+
401
385
  await self._capture_streaming_event(
402
386
  posthog_distinct_id,
403
387
  posthog_trace_id,
@@ -407,7 +391,8 @@ class WrappedCompletions:
407
391
  kwargs,
408
392
  usage_stats,
409
393
  latency,
410
- output,
394
+ accumulated_content,
395
+ tool_calls_list,
411
396
  extract_available_tool_calls("openai", kwargs),
412
397
  )
413
398
 
@@ -424,6 +409,7 @@ class WrappedCompletions:
424
409
  usage_stats: Dict[str, int],
425
410
  latency: float,
426
411
  output: Any,
412
+ tool_calls: Optional[List[Dict[str, Any]]] = None,
427
413
  available_tool_calls: Optional[List[Dict[str, Any]]] = None,
428
414
  ):
429
415
  if posthog_trace_id is None:
@@ -441,7 +427,7 @@ class WrappedCompletions:
441
427
  "$ai_output_choices": with_privacy_mode(
442
428
  self._client._ph_client,
443
429
  posthog_privacy_mode,
444
- [{"content": output, "role": "assistant"}],
430
+ format_openai_streaming_output(output, "chat", tool_calls),
445
431
  ),
446
432
  "$ai_http_status": 200,
447
433
  "$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
@@ -480,6 +466,7 @@ class WrappedEmbeddings:
480
466
 
481
467
  def __getattr__(self, name):
482
468
  """Fallback to original embeddings object for any methods we don't explicitly handle."""
469
+
483
470
  return getattr(self._original, name)
484
471
 
485
472
  async def create(
@@ -505,15 +492,17 @@ class WrappedEmbeddings:
505
492
  Returns:
506
493
  The response from OpenAI's embeddings.create call.
507
494
  """
495
+
508
496
  if posthog_trace_id is None:
509
497
  posthog_trace_id = str(uuid.uuid4())
510
498
 
511
499
  start_time = time.time()
512
- response = await self._original.create(**kwargs)
500
+ response = self._original.create(**kwargs)
513
501
  end_time = time.time()
514
502
 
515
503
  # Extract usage statistics if available
516
504
  usage_stats = {}
505
+
517
506
  if hasattr(response, "usage") and response.usage:
518
507
  usage_stats = {
519
508
  "prompt_tokens": getattr(response.usage, "prompt_tokens", 0),
@@ -563,6 +552,7 @@ class WrappedBeta:
563
552
 
564
553
  def __getattr__(self, name):
565
554
  """Fallback to original beta object for any methods we don't explicitly handle."""
555
+
566
556
  return getattr(self._original, name)
567
557
 
568
558
  @property
@@ -579,6 +569,7 @@ class WrappedBetaChat:
579
569
 
580
570
  def __getattr__(self, name):
581
571
  """Fallback to original beta chat object for any methods we don't explicitly handle."""
572
+
582
573
  return getattr(self._original, name)
583
574
 
584
575
  @property
@@ -595,6 +586,7 @@ class WrappedBetaCompletions:
595
586
 
596
587
  def __getattr__(self, name):
597
588
  """Fallback to original beta completions object for any methods we don't explicitly handle."""
589
+
598
590
  return getattr(self._original, name)
599
591
 
600
592
  async def parse(