posthoganalytics 6.7.1__py3-none-any.whl → 6.7.3__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.
@@ -2,6 +2,8 @@ import time
2
2
  import uuid
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
+ from posthoganalytics.ai.types import TokenUsage
6
+
5
7
  try:
6
8
  import openai
7
9
  except ImportError:
@@ -14,8 +16,16 @@ from posthoganalytics.ai.utils import (
14
16
  call_llm_and_track_usage_async,
15
17
  extract_available_tool_calls,
16
18
  get_model_params,
19
+ merge_usage_stats,
17
20
  with_privacy_mode,
18
21
  )
22
+ from posthoganalytics.ai.openai.openai_converter import (
23
+ extract_openai_usage_from_chunk,
24
+ extract_openai_content_from_chunk,
25
+ extract_openai_tool_calls_from_chunk,
26
+ accumulate_openai_tool_calls,
27
+ format_openai_streaming_output,
28
+ )
19
29
  from posthoganalytics.ai.sanitization import sanitize_openai, sanitize_openai_response
20
30
  from posthoganalytics.client import Client as PostHogClient
21
31
 
@@ -35,6 +45,7 @@ class AsyncOpenAI(openai.AsyncOpenAI):
35
45
  of the global posthog.
36
46
  **openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
37
47
  """
48
+
38
49
  super().__init__(**kwargs)
39
50
  self._ph_client = posthog_client or setup()
40
51
 
@@ -67,6 +78,7 @@ class WrappedResponses:
67
78
 
68
79
  def __getattr__(self, name):
69
80
  """Fallback to original responses object for any methods we don't explicitly handle."""
81
+
70
82
  return getattr(self._original, name)
71
83
 
72
84
  async def create(
@@ -114,9 +126,9 @@ class WrappedResponses:
114
126
  **kwargs: Any,
115
127
  ):
116
128
  start_time = time.time()
117
- usage_stats: Dict[str, int] = {}
129
+ usage_stats: TokenUsage = TokenUsage()
118
130
  final_content = []
119
- response = await self._original.create(**kwargs)
131
+ response = self._original.create(**kwargs)
120
132
 
121
133
  async def async_generator():
122
134
  nonlocal usage_stats
@@ -124,35 +136,17 @@ class WrappedResponses:
124
136
 
125
137
  try:
126
138
  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
- )
139
+ # Extract usage stats from chunk
140
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "responses")
141
+
142
+ if chunk_usage:
143
+ merge_usage_stats(usage_stats, chunk_usage)
144
+
145
+ # Extract content from chunk
146
+ content = extract_openai_content_from_chunk(chunk, "responses")
147
+
148
+ if content is not None:
149
+ final_content.append(content)
156
150
 
157
151
  yield chunk
158
152
 
@@ -160,6 +154,7 @@ class WrappedResponses:
160
154
  end_time = time.time()
161
155
  latency = end_time - start_time
162
156
  output = final_content
157
+
163
158
  await self._capture_streaming_event(
164
159
  posthog_distinct_id,
165
160
  posthog_trace_id,
@@ -183,7 +178,7 @@ class WrappedResponses:
183
178
  posthog_privacy_mode: bool,
184
179
  posthog_groups: Optional[Dict[str, Any]],
185
180
  kwargs: Dict[str, Any],
186
- usage_stats: Dict[str, int],
181
+ usage_stats: TokenUsage,
187
182
  latency: float,
188
183
  output: Any,
189
184
  available_tool_calls: Optional[List[Dict[str, Any]]] = None,
@@ -203,7 +198,7 @@ class WrappedResponses:
203
198
  "$ai_output_choices": with_privacy_mode(
204
199
  self._client._ph_client,
205
200
  posthog_privacy_mode,
206
- output,
201
+ format_openai_streaming_output(output, "responses"),
207
202
  ),
208
203
  "$ai_http_status": 200,
209
204
  "$ai_input_tokens": usage_stats.get("input_tokens", 0),
@@ -343,61 +338,52 @@ class WrappedCompletions:
343
338
  **kwargs: Any,
344
339
  ):
345
340
  start_time = time.time()
346
- usage_stats: Dict[str, int] = {}
341
+ usage_stats: TokenUsage = TokenUsage()
347
342
  accumulated_content = []
343
+ accumulated_tool_calls: Dict[int, Dict[str, Any]] = {}
348
344
 
349
345
  if "stream_options" not in kwargs:
350
346
  kwargs["stream_options"] = {}
351
347
  kwargs["stream_options"]["include_usage"] = True
352
- response = await self._original.create(**kwargs)
348
+ response = self._original.create(**kwargs)
353
349
 
354
350
  async def async_generator():
355
351
  nonlocal usage_stats
356
352
  nonlocal accumulated_content # noqa: F824
353
+ nonlocal accumulated_tool_calls
357
354
 
358
355
  try:
359
356
  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)
357
+ # Extract usage stats from chunk
358
+ chunk_usage = extract_openai_usage_from_chunk(chunk, "chat")
359
+ if chunk_usage:
360
+ merge_usage_stats(usage_stats, chunk_usage)
361
+
362
+ # Extract content from chunk
363
+ content = extract_openai_content_from_chunk(chunk, "chat")
364
+ if content is not None:
365
+ accumulated_content.append(content)
366
+
367
+ # Extract and accumulate tool calls from chunk
368
+ chunk_tool_calls = extract_openai_tool_calls_from_chunk(chunk)
369
+ if chunk_tool_calls:
370
+ accumulate_openai_tool_calls(
371
+ accumulated_tool_calls, chunk_tool_calls
372
+ )
394
373
 
395
374
  yield chunk
396
375
 
397
376
  finally:
398
377
  end_time = time.time()
399
378
  latency = end_time - start_time
400
- output = "".join(accumulated_content)
379
+
380
+ # Convert accumulated tool calls dict to list
381
+ tool_calls_list = (
382
+ list(accumulated_tool_calls.values())
383
+ if accumulated_tool_calls
384
+ else None
385
+ )
386
+
401
387
  await self._capture_streaming_event(
402
388
  posthog_distinct_id,
403
389
  posthog_trace_id,
@@ -407,7 +393,8 @@ class WrappedCompletions:
407
393
  kwargs,
408
394
  usage_stats,
409
395
  latency,
410
- output,
396
+ accumulated_content,
397
+ tool_calls_list,
411
398
  extract_available_tool_calls("openai", kwargs),
412
399
  )
413
400
 
@@ -421,9 +408,10 @@ class WrappedCompletions:
421
408
  posthog_privacy_mode: bool,
422
409
  posthog_groups: Optional[Dict[str, Any]],
423
410
  kwargs: Dict[str, Any],
424
- usage_stats: Dict[str, int],
411
+ usage_stats: TokenUsage,
425
412
  latency: float,
426
413
  output: Any,
414
+ tool_calls: Optional[List[Dict[str, Any]]] = None,
427
415
  available_tool_calls: Optional[List[Dict[str, Any]]] = None,
428
416
  ):
429
417
  if posthog_trace_id is None:
@@ -441,11 +429,11 @@ class WrappedCompletions:
441
429
  "$ai_output_choices": with_privacy_mode(
442
430
  self._client._ph_client,
443
431
  posthog_privacy_mode,
444
- [{"content": output, "role": "assistant"}],
432
+ format_openai_streaming_output(output, "chat", tool_calls),
445
433
  ),
446
434
  "$ai_http_status": 200,
447
- "$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
448
- "$ai_output_tokens": usage_stats.get("completion_tokens", 0),
435
+ "$ai_input_tokens": usage_stats.get("input_tokens", 0),
436
+ "$ai_output_tokens": usage_stats.get("output_tokens", 0),
449
437
  "$ai_cache_read_input_tokens": usage_stats.get(
450
438
  "cache_read_input_tokens", 0
451
439
  ),
@@ -480,6 +468,7 @@ class WrappedEmbeddings:
480
468
 
481
469
  def __getattr__(self, name):
482
470
  """Fallback to original embeddings object for any methods we don't explicitly handle."""
471
+
483
472
  return getattr(self._original, name)
484
473
 
485
474
  async def create(
@@ -505,20 +494,22 @@ class WrappedEmbeddings:
505
494
  Returns:
506
495
  The response from OpenAI's embeddings.create call.
507
496
  """
497
+
508
498
  if posthog_trace_id is None:
509
499
  posthog_trace_id = str(uuid.uuid4())
510
500
 
511
501
  start_time = time.time()
512
- response = await self._original.create(**kwargs)
502
+ response = self._original.create(**kwargs)
513
503
  end_time = time.time()
514
504
 
515
505
  # Extract usage statistics if available
516
- usage_stats = {}
506
+ usage_stats: TokenUsage = TokenUsage()
507
+
517
508
  if hasattr(response, "usage") and response.usage:
518
- usage_stats = {
519
- "prompt_tokens": getattr(response.usage, "prompt_tokens", 0),
520
- "total_tokens": getattr(response.usage, "total_tokens", 0),
521
- }
509
+ usage_stats = TokenUsage(
510
+ input_tokens=getattr(response.usage, "prompt_tokens", 0),
511
+ output_tokens=getattr(response.usage, "completion_tokens", 0),
512
+ )
522
513
 
523
514
  latency = end_time - start_time
524
515
 
@@ -532,7 +523,7 @@ class WrappedEmbeddings:
532
523
  sanitize_openai_response(kwargs.get("input")),
533
524
  ),
534
525
  "$ai_http_status": 200,
535
- "$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
526
+ "$ai_input_tokens": usage_stats.get("input_tokens", 0),
536
527
  "$ai_latency": latency,
537
528
  "$ai_trace_id": posthog_trace_id,
538
529
  "$ai_base_url": str(self._client.base_url),
@@ -563,6 +554,7 @@ class WrappedBeta:
563
554
 
564
555
  def __getattr__(self, name):
565
556
  """Fallback to original beta object for any methods we don't explicitly handle."""
557
+
566
558
  return getattr(self._original, name)
567
559
 
568
560
  @property
@@ -579,6 +571,7 @@ class WrappedBetaChat:
579
571
 
580
572
  def __getattr__(self, name):
581
573
  """Fallback to original beta chat object for any methods we don't explicitly handle."""
574
+
582
575
  return getattr(self._original, name)
583
576
 
584
577
  @property
@@ -595,6 +588,7 @@ class WrappedBetaCompletions:
595
588
 
596
589
  def __getattr__(self, name):
597
590
  """Fallback to original beta completions object for any methods we don't explicitly handle."""
591
+
598
592
  return getattr(self._original, name)
599
593
 
600
594
  async def parse(