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.
- posthoganalytics/ai/anthropic/__init__.py +10 -0
- posthoganalytics/ai/anthropic/anthropic.py +94 -65
- posthoganalytics/ai/anthropic/anthropic_async.py +88 -22
- posthoganalytics/ai/anthropic/anthropic_converter.py +403 -0
- posthoganalytics/ai/gemini/__init__.py +12 -1
- posthoganalytics/ai/gemini/gemini.py +63 -69
- posthoganalytics/ai/gemini/gemini_converter.py +460 -0
- posthoganalytics/ai/openai/__init__.py +16 -1
- posthoganalytics/ai/openai/openai.py +114 -155
- posthoganalytics/ai/openai/openai_async.py +76 -82
- posthoganalytics/ai/openai/openai_converter.py +612 -0
- posthoganalytics/ai/types.py +124 -0
- posthoganalytics/ai/utils.py +272 -351
- posthoganalytics/client.py +7 -7
- posthoganalytics/test/test_feature_flags.py +2 -2
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.1.dist-info → posthoganalytics-6.7.3.dist-info}/METADATA +1 -1
- {posthoganalytics-6.7.1.dist-info → posthoganalytics-6.7.3.dist-info}/RECORD +21 -17
- {posthoganalytics-6.7.1.dist-info → posthoganalytics-6.7.3.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.1.dist-info → posthoganalytics-6.7.3.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.7.1.dist-info → posthoganalytics-6.7.3.dist-info}/top_level.txt +0 -0
|
@@ -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:
|
|
129
|
+
usage_stats: TokenUsage = TokenUsage()
|
|
118
130
|
final_content = []
|
|
119
|
-
response =
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
432
|
+
format_openai_streaming_output(output, "chat", tool_calls),
|
|
445
433
|
),
|
|
446
434
|
"$ai_http_status": 200,
|
|
447
|
-
"$ai_input_tokens": usage_stats.get("
|
|
448
|
-
"$ai_output_tokens": usage_stats.get("
|
|
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 =
|
|
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
|
-
|
|
520
|
-
|
|
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("
|
|
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(
|