posthoganalytics 6.7.0__py3-none-any.whl → 7.4.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/__init__.py +84 -7
- posthoganalytics/ai/anthropic/__init__.py +10 -0
- posthoganalytics/ai/anthropic/anthropic.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
- posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
- posthoganalytics/ai/gemini/__init__.py +15 -1
- posthoganalytics/ai/gemini/gemini.py +66 -71
- posthoganalytics/ai/gemini/gemini_async.py +423 -0
- posthoganalytics/ai/gemini/gemini_converter.py +652 -0
- posthoganalytics/ai/langchain/callbacks.py +58 -13
- posthoganalytics/ai/openai/__init__.py +16 -1
- posthoganalytics/ai/openai/openai.py +140 -149
- posthoganalytics/ai/openai/openai_async.py +127 -82
- posthoganalytics/ai/openai/openai_converter.py +741 -0
- posthoganalytics/ai/sanitization.py +248 -0
- posthoganalytics/ai/types.py +125 -0
- posthoganalytics/ai/utils.py +339 -356
- posthoganalytics/client.py +345 -97
- posthoganalytics/contexts.py +81 -0
- posthoganalytics/exception_utils.py +250 -2
- posthoganalytics/feature_flags.py +26 -10
- posthoganalytics/flag_definition_cache.py +127 -0
- posthoganalytics/integrations/django.py +157 -19
- posthoganalytics/request.py +203 -23
- posthoganalytics/test/test_client.py +250 -22
- posthoganalytics/test/test_exception_capture.py +418 -0
- posthoganalytics/test/test_feature_flag_result.py +441 -2
- posthoganalytics/test/test_feature_flags.py +308 -104
- posthoganalytics/test/test_flag_definition_cache.py +612 -0
- posthoganalytics/test/test_module.py +0 -8
- posthoganalytics/test/test_request.py +536 -0
- posthoganalytics/test/test_utils.py +4 -1
- posthoganalytics/types.py +40 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
- posthoganalytics-7.4.3.dist-info/RECORD +57 -0
- posthoganalytics-6.7.0.dist-info/RECORD +0 -49
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
try:
|
|
2
|
-
import
|
|
2
|
+
import langchain_core # noqa: F401
|
|
3
3
|
except ImportError:
|
|
4
4
|
raise ModuleNotFoundError(
|
|
5
|
-
"Please install LangChain to use this feature: 'pip install langchain'"
|
|
5
|
+
"Please install LangChain to use this feature: 'pip install langchain-core'"
|
|
6
6
|
)
|
|
7
7
|
|
|
8
8
|
import json
|
|
@@ -20,8 +20,14 @@ from typing import (
|
|
|
20
20
|
)
|
|
21
21
|
from uuid import UUID
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
try:
|
|
24
|
+
# LangChain 1.0+ and modern 0.x with langchain-core
|
|
25
|
+
from langchain_core.callbacks.base import BaseCallbackHandler
|
|
26
|
+
from langchain_core.agents import AgentAction, AgentFinish
|
|
27
|
+
except (ImportError, ModuleNotFoundError):
|
|
28
|
+
# Fallback for older LangChain versions
|
|
29
|
+
from langchain.callbacks.base import BaseCallbackHandler
|
|
30
|
+
from langchain.schema.agent import AgentAction, AgentFinish
|
|
25
31
|
from langchain_core.documents import Document
|
|
26
32
|
from langchain_core.messages import (
|
|
27
33
|
AIMessage,
|
|
@@ -37,6 +43,7 @@ from pydantic import BaseModel
|
|
|
37
43
|
|
|
38
44
|
from posthoganalytics import setup
|
|
39
45
|
from posthoganalytics.ai.utils import get_model_params, with_privacy_mode
|
|
46
|
+
from posthoganalytics.ai.sanitization import sanitize_langchain
|
|
40
47
|
from posthoganalytics.client import Client
|
|
41
48
|
|
|
42
49
|
log = logging.getLogger("posthog")
|
|
@@ -72,6 +79,8 @@ class GenerationMetadata(SpanMetadata):
|
|
|
72
79
|
"""Base URL of the provider's API used in the run."""
|
|
73
80
|
tools: Optional[List[Dict[str, Any]]] = None
|
|
74
81
|
"""Tools provided to the model."""
|
|
82
|
+
posthog_properties: Optional[Dict[str, Any]] = None
|
|
83
|
+
"""PostHog properties of the run."""
|
|
75
84
|
|
|
76
85
|
|
|
77
86
|
RunMetadata = Union[SpanMetadata, GenerationMetadata]
|
|
@@ -413,6 +422,8 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
413
422
|
generation.model = model
|
|
414
423
|
if provider := metadata.get("ls_provider"):
|
|
415
424
|
generation.provider = provider
|
|
425
|
+
|
|
426
|
+
generation.posthog_properties = metadata.get("posthog_properties")
|
|
416
427
|
try:
|
|
417
428
|
base_url = serialized["kwargs"]["openai_api_base"]
|
|
418
429
|
if base_url is not None:
|
|
@@ -480,11 +491,12 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
480
491
|
event_properties = {
|
|
481
492
|
"$ai_trace_id": trace_id,
|
|
482
493
|
"$ai_input_state": with_privacy_mode(
|
|
483
|
-
self._ph_client, self._privacy_mode, run.input
|
|
494
|
+
self._ph_client, self._privacy_mode, sanitize_langchain(run.input)
|
|
484
495
|
),
|
|
485
496
|
"$ai_latency": run.latency,
|
|
486
497
|
"$ai_span_name": run.name,
|
|
487
498
|
"$ai_span_id": run_id,
|
|
499
|
+
"$ai_framework": "langchain",
|
|
488
500
|
}
|
|
489
501
|
if parent_run_id is not None:
|
|
490
502
|
event_properties["$ai_parent_id"] = parent_run_id
|
|
@@ -550,13 +562,17 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
550
562
|
"$ai_model": run.model,
|
|
551
563
|
"$ai_model_parameters": run.model_params,
|
|
552
564
|
"$ai_input": with_privacy_mode(
|
|
553
|
-
self._ph_client, self._privacy_mode, run.input
|
|
565
|
+
self._ph_client, self._privacy_mode, sanitize_langchain(run.input)
|
|
554
566
|
),
|
|
555
567
|
"$ai_http_status": 200,
|
|
556
568
|
"$ai_latency": run.latency,
|
|
557
569
|
"$ai_base_url": run.base_url,
|
|
570
|
+
"$ai_framework": "langchain",
|
|
558
571
|
}
|
|
559
572
|
|
|
573
|
+
if isinstance(run.posthog_properties, dict):
|
|
574
|
+
event_properties.update(run.posthog_properties)
|
|
575
|
+
|
|
560
576
|
if run.tools:
|
|
561
577
|
event_properties["$ai_tools"] = run.tools
|
|
562
578
|
|
|
@@ -566,7 +582,7 @@ class CallbackHandler(BaseCallbackHandler):
|
|
|
566
582
|
event_properties["$ai_is_error"] = True
|
|
567
583
|
else:
|
|
568
584
|
# Add usage
|
|
569
|
-
usage = _parse_usage(output)
|
|
585
|
+
usage = _parse_usage(output, run.provider, run.model)
|
|
570
586
|
event_properties["$ai_input_tokens"] = usage.input_tokens
|
|
571
587
|
event_properties["$ai_output_tokens"] = usage.output_tokens
|
|
572
588
|
event_properties["$ai_cache_creation_input_tokens"] = (
|
|
@@ -687,6 +703,8 @@ class ModelUsage:
|
|
|
687
703
|
|
|
688
704
|
def _parse_usage_model(
|
|
689
705
|
usage: Union[BaseModel, dict],
|
|
706
|
+
provider: Optional[str] = None,
|
|
707
|
+
model: Optional[str] = None,
|
|
690
708
|
) -> ModelUsage:
|
|
691
709
|
if isinstance(usage, BaseModel):
|
|
692
710
|
usage = usage.__dict__
|
|
@@ -749,15 +767,38 @@ def _parse_usage_model(
|
|
|
749
767
|
"cache_read": "cache_read_tokens",
|
|
750
768
|
"reasoning": "reasoning_tokens",
|
|
751
769
|
}
|
|
752
|
-
|
|
770
|
+
normalized_usage = ModelUsage(
|
|
753
771
|
**{
|
|
754
772
|
dataclass_key: parsed_usage.get(mapped_key) or 0
|
|
755
773
|
for mapped_key, dataclass_key in field_mapping.items()
|
|
756
774
|
},
|
|
757
775
|
)
|
|
776
|
+
# For Anthropic providers, LangChain reports input_tokens as the sum of all input tokens.
|
|
777
|
+
# Our cost calculation expects them to be separate for Anthropic, so we subtract cache tokens.
|
|
778
|
+
# Both cache_read and cache_write tokens should be subtracted since Anthropic's raw API
|
|
779
|
+
# reports input_tokens as tokens NOT read from or used to create a cache.
|
|
780
|
+
# For other providers (OpenAI, etc.), input_tokens already excludes cache tokens as expected.
|
|
781
|
+
# Match logic consistent with plugin-server: exact match on provider OR substring match on model
|
|
782
|
+
is_anthropic = False
|
|
783
|
+
if provider and provider.lower() == "anthropic":
|
|
784
|
+
is_anthropic = True
|
|
785
|
+
elif model and "anthropic" in model.lower():
|
|
786
|
+
is_anthropic = True
|
|
787
|
+
|
|
788
|
+
if is_anthropic and normalized_usage.input_tokens:
|
|
789
|
+
cache_tokens = (normalized_usage.cache_read_tokens or 0) + (
|
|
790
|
+
normalized_usage.cache_write_tokens or 0
|
|
791
|
+
)
|
|
792
|
+
if cache_tokens > 0:
|
|
793
|
+
normalized_usage.input_tokens = max(
|
|
794
|
+
normalized_usage.input_tokens - cache_tokens, 0
|
|
795
|
+
)
|
|
796
|
+
return normalized_usage
|
|
758
797
|
|
|
759
798
|
|
|
760
|
-
def _parse_usage(
|
|
799
|
+
def _parse_usage(
|
|
800
|
+
response: LLMResult, provider: Optional[str] = None, model: Optional[str] = None
|
|
801
|
+
) -> ModelUsage:
|
|
761
802
|
# langchain-anthropic uses the usage field
|
|
762
803
|
llm_usage_keys = ["token_usage", "usage"]
|
|
763
804
|
llm_usage: ModelUsage = ModelUsage(
|
|
@@ -771,13 +812,15 @@ def _parse_usage(response: LLMResult) -> ModelUsage:
|
|
|
771
812
|
if response.llm_output is not None:
|
|
772
813
|
for key in llm_usage_keys:
|
|
773
814
|
if response.llm_output.get(key):
|
|
774
|
-
llm_usage = _parse_usage_model(
|
|
815
|
+
llm_usage = _parse_usage_model(
|
|
816
|
+
response.llm_output[key], provider, model
|
|
817
|
+
)
|
|
775
818
|
break
|
|
776
819
|
|
|
777
820
|
if hasattr(response, "generations"):
|
|
778
821
|
for generation in response.generations:
|
|
779
822
|
if "usage" in generation:
|
|
780
|
-
llm_usage = _parse_usage_model(generation["usage"])
|
|
823
|
+
llm_usage = _parse_usage_model(generation["usage"], provider, model)
|
|
781
824
|
break
|
|
782
825
|
|
|
783
826
|
for generation_chunk in generation:
|
|
@@ -785,7 +828,9 @@ def _parse_usage(response: LLMResult) -> ModelUsage:
|
|
|
785
828
|
"usage_metadata" in generation_chunk.generation_info
|
|
786
829
|
):
|
|
787
830
|
llm_usage = _parse_usage_model(
|
|
788
|
-
generation_chunk.generation_info["usage_metadata"]
|
|
831
|
+
generation_chunk.generation_info["usage_metadata"],
|
|
832
|
+
provider,
|
|
833
|
+
model,
|
|
789
834
|
)
|
|
790
835
|
break
|
|
791
836
|
|
|
@@ -812,7 +857,7 @@ def _parse_usage(response: LLMResult) -> ModelUsage:
|
|
|
812
857
|
bedrock_anthropic_usage or bedrock_titan_usage or ollama_usage
|
|
813
858
|
)
|
|
814
859
|
if chunk_usage:
|
|
815
|
-
llm_usage = _parse_usage_model(chunk_usage)
|
|
860
|
+
llm_usage = _parse_usage_model(chunk_usage, provider, model)
|
|
816
861
|
break
|
|
817
862
|
|
|
818
863
|
return llm_usage
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
from .openai import OpenAI
|
|
2
2
|
from .openai_async import AsyncOpenAI
|
|
3
3
|
from .openai_providers import AsyncAzureOpenAI, AzureOpenAI
|
|
4
|
+
from .openai_converter import (
|
|
5
|
+
format_openai_response,
|
|
6
|
+
format_openai_input,
|
|
7
|
+
extract_openai_tools,
|
|
8
|
+
format_openai_streaming_content,
|
|
9
|
+
)
|
|
4
10
|
|
|
5
|
-
__all__ = [
|
|
11
|
+
__all__ = [
|
|
12
|
+
"OpenAI",
|
|
13
|
+
"AsyncOpenAI",
|
|
14
|
+
"AzureOpenAI",
|
|
15
|
+
"AsyncAzureOpenAI",
|
|
16
|
+
"format_openai_response",
|
|
17
|
+
"format_openai_input",
|
|
18
|
+
"extract_openai_tools",
|
|
19
|
+
"format_openai_streaming_content",
|
|
20
|
+
]
|
|
@@ -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:
|
|
@@ -12,9 +14,16 @@ except ImportError:
|
|
|
12
14
|
from posthoganalytics.ai.utils import (
|
|
13
15
|
call_llm_and_track_usage,
|
|
14
16
|
extract_available_tool_calls,
|
|
15
|
-
|
|
17
|
+
merge_usage_stats,
|
|
16
18
|
with_privacy_mode,
|
|
17
19
|
)
|
|
20
|
+
from posthoganalytics.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
|
+
)
|
|
26
|
+
from posthoganalytics.ai.sanitization import sanitize_openai, sanitize_openai_response
|
|
18
27
|
from posthoganalytics.client import Client as PostHogClient
|
|
19
28
|
from posthoganalytics import setup
|
|
20
29
|
|
|
@@ -33,6 +42,7 @@ class OpenAI(openai.OpenAI):
|
|
|
33
42
|
posthog_client: If provided, events will be captured via this client instead of the global `posthog`.
|
|
34
43
|
**openai_config: Any additional keyword args to set on openai (e.g. organization="xxx").
|
|
35
44
|
"""
|
|
45
|
+
|
|
36
46
|
super().__init__(**kwargs)
|
|
37
47
|
self._ph_client = posthog_client or setup()
|
|
38
48
|
|
|
@@ -112,45 +122,36 @@ class WrappedResponses:
|
|
|
112
122
|
**kwargs: Any,
|
|
113
123
|
):
|
|
114
124
|
start_time = time.time()
|
|
115
|
-
usage_stats:
|
|
125
|
+
usage_stats: TokenUsage = TokenUsage()
|
|
116
126
|
final_content = []
|
|
127
|
+
model_from_response: Optional[str] = None
|
|
117
128
|
response = self._original.create(**kwargs)
|
|
118
129
|
|
|
119
130
|
def generator():
|
|
120
131
|
nonlocal usage_stats
|
|
121
132
|
nonlocal final_content # noqa: F824
|
|
133
|
+
nonlocal model_from_response
|
|
122
134
|
|
|
123
135
|
try:
|
|
124
136
|
for chunk in response:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if hasattr(chunk, "usage") and chunk.usage:
|
|
131
|
-
usage_stats = {
|
|
132
|
-
k: getattr(chunk.usage, k, 0)
|
|
133
|
-
for k in [
|
|
134
|
-
"input_tokens",
|
|
135
|
-
"output_tokens",
|
|
136
|
-
"total_tokens",
|
|
137
|
-
]
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# Add support for cached tokens
|
|
141
|
-
if hasattr(chunk.usage, "output_tokens_details") and hasattr(
|
|
142
|
-
chunk.usage.output_tokens_details, "reasoning_tokens"
|
|
137
|
+
# Extract model from response object in chunk (for stored prompts)
|
|
138
|
+
if hasattr(chunk, "response") and chunk.response:
|
|
139
|
+
if model_from_response is None and hasattr(
|
|
140
|
+
chunk.response, "model"
|
|
143
141
|
):
|
|
144
|
-
|
|
145
|
-
chunk.usage.output_tokens_details.reasoning_tokens
|
|
146
|
-
)
|
|
142
|
+
model_from_response = chunk.response.model
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
144
|
+
# Extract usage stats from chunk
|
|
145
|
+
chunk_usage = extract_openai_usage_from_chunk(chunk, "responses")
|
|
146
|
+
|
|
147
|
+
if chunk_usage:
|
|
148
|
+
merge_usage_stats(usage_stats, chunk_usage)
|
|
149
|
+
|
|
150
|
+
# Extract content from chunk
|
|
151
|
+
content = extract_openai_content_from_chunk(chunk, "responses")
|
|
152
|
+
|
|
153
|
+
if content is not None:
|
|
154
|
+
final_content.append(content)
|
|
154
155
|
|
|
155
156
|
yield chunk
|
|
156
157
|
|
|
@@ -168,7 +169,8 @@ class WrappedResponses:
|
|
|
168
169
|
usage_stats,
|
|
169
170
|
latency,
|
|
170
171
|
output,
|
|
171
|
-
|
|
172
|
+
None, # Responses API doesn't have tools
|
|
173
|
+
model_from_response,
|
|
172
174
|
)
|
|
173
175
|
|
|
174
176
|
return generator()
|
|
@@ -181,52 +183,44 @@ class WrappedResponses:
|
|
|
181
183
|
posthog_privacy_mode: bool,
|
|
182
184
|
posthog_groups: Optional[Dict[str, Any]],
|
|
183
185
|
kwargs: Dict[str, Any],
|
|
184
|
-
usage_stats:
|
|
186
|
+
usage_stats: TokenUsage,
|
|
185
187
|
latency: float,
|
|
186
188
|
output: Any,
|
|
187
189
|
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
190
|
+
model_from_response: Optional[str] = None,
|
|
188
191
|
):
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
event_properties["$ai_tools"] = available_tool_calls
|
|
219
|
-
|
|
220
|
-
if posthog_distinct_id is None:
|
|
221
|
-
event_properties["$process_person_profile"] = False
|
|
192
|
+
from posthoganalytics.ai.types import StreamingEventData
|
|
193
|
+
from posthoganalytics.ai.openai.openai_converter import (
|
|
194
|
+
format_openai_streaming_input,
|
|
195
|
+
format_openai_streaming_output,
|
|
196
|
+
)
|
|
197
|
+
from posthoganalytics.ai.utils import capture_streaming_event
|
|
198
|
+
|
|
199
|
+
# Prepare standardized event data
|
|
200
|
+
formatted_input = format_openai_streaming_input(kwargs, "responses")
|
|
201
|
+
sanitized_input = sanitize_openai_response(formatted_input)
|
|
202
|
+
|
|
203
|
+
# Use model from kwargs, fallback to model from response
|
|
204
|
+
model = kwargs.get("model") or model_from_response or "unknown"
|
|
205
|
+
|
|
206
|
+
event_data = StreamingEventData(
|
|
207
|
+
provider="openai",
|
|
208
|
+
model=model,
|
|
209
|
+
base_url=str(self._client.base_url),
|
|
210
|
+
kwargs=kwargs,
|
|
211
|
+
formatted_input=sanitized_input,
|
|
212
|
+
formatted_output=format_openai_streaming_output(output, "responses"),
|
|
213
|
+
usage_stats=usage_stats,
|
|
214
|
+
latency=latency,
|
|
215
|
+
distinct_id=posthog_distinct_id,
|
|
216
|
+
trace_id=posthog_trace_id,
|
|
217
|
+
properties=posthog_properties,
|
|
218
|
+
privacy_mode=posthog_privacy_mode,
|
|
219
|
+
groups=posthog_groups,
|
|
220
|
+
)
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
distinct_id=posthog_distinct_id or posthog_trace_id,
|
|
226
|
-
event="$ai_generation",
|
|
227
|
-
properties=event_properties,
|
|
228
|
-
groups=posthog_groups,
|
|
229
|
-
)
|
|
222
|
+
# Use the common capture function
|
|
223
|
+
capture_streaming_event(self._client._ph_client, event_data)
|
|
230
224
|
|
|
231
225
|
def parse(
|
|
232
226
|
self,
|
|
@@ -337,8 +331,10 @@ class WrappedCompletions:
|
|
|
337
331
|
**kwargs: Any,
|
|
338
332
|
):
|
|
339
333
|
start_time = time.time()
|
|
340
|
-
usage_stats:
|
|
334
|
+
usage_stats: TokenUsage = TokenUsage()
|
|
341
335
|
accumulated_content = []
|
|
336
|
+
accumulated_tool_calls: Dict[int, Dict[str, Any]] = {}
|
|
337
|
+
model_from_response: Optional[str] = None
|
|
342
338
|
if "stream_options" not in kwargs:
|
|
343
339
|
kwargs["stream_options"] = {}
|
|
344
340
|
kwargs["stream_options"]["include_usage"] = True
|
|
@@ -347,50 +343,47 @@ class WrappedCompletions:
|
|
|
347
343
|
def generator():
|
|
348
344
|
nonlocal usage_stats
|
|
349
345
|
nonlocal accumulated_content # noqa: F824
|
|
346
|
+
nonlocal accumulated_tool_calls
|
|
347
|
+
nonlocal model_from_response
|
|
350
348
|
|
|
351
349
|
try:
|
|
352
350
|
for chunk in response:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
for k in [
|
|
357
|
-
"prompt_tokens",
|
|
358
|
-
"completion_tokens",
|
|
359
|
-
"total_tokens",
|
|
360
|
-
]
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
# Add support for cached tokens
|
|
364
|
-
if hasattr(chunk.usage, "prompt_tokens_details") and hasattr(
|
|
365
|
-
chunk.usage.prompt_tokens_details, "cached_tokens"
|
|
366
|
-
):
|
|
367
|
-
usage_stats["cache_read_input_tokens"] = (
|
|
368
|
-
chunk.usage.prompt_tokens_details.cached_tokens
|
|
369
|
-
)
|
|
351
|
+
# Extract model from chunk (Chat Completions chunks have model field)
|
|
352
|
+
if model_from_response is None and hasattr(chunk, "model"):
|
|
353
|
+
model_from_response = chunk.model
|
|
370
354
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
355
|
+
# Extract usage stats from chunk
|
|
356
|
+
chunk_usage = extract_openai_usage_from_chunk(chunk, "chat")
|
|
357
|
+
|
|
358
|
+
if chunk_usage:
|
|
359
|
+
merge_usage_stats(usage_stats, chunk_usage)
|
|
360
|
+
|
|
361
|
+
# Extract content from chunk
|
|
362
|
+
content = extract_openai_content_from_chunk(chunk, "chat")
|
|
363
|
+
|
|
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
|
+
)
|
|
387
373
|
|
|
388
374
|
yield chunk
|
|
389
375
|
|
|
390
376
|
finally:
|
|
391
377
|
end_time = time.time()
|
|
392
378
|
latency = end_time - start_time
|
|
393
|
-
|
|
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
|
+
|
|
394
387
|
self._capture_streaming_event(
|
|
395
388
|
posthog_distinct_id,
|
|
396
389
|
posthog_trace_id,
|
|
@@ -400,8 +393,10 @@ class WrappedCompletions:
|
|
|
400
393
|
kwargs,
|
|
401
394
|
usage_stats,
|
|
402
395
|
latency,
|
|
403
|
-
|
|
396
|
+
accumulated_content,
|
|
397
|
+
tool_calls_list,
|
|
404
398
|
extract_available_tool_calls("openai", kwargs),
|
|
399
|
+
model_from_response,
|
|
405
400
|
)
|
|
406
401
|
|
|
407
402
|
return generator()
|
|
@@ -414,52 +409,45 @@ class WrappedCompletions:
|
|
|
414
409
|
posthog_privacy_mode: bool,
|
|
415
410
|
posthog_groups: Optional[Dict[str, Any]],
|
|
416
411
|
kwargs: Dict[str, Any],
|
|
417
|
-
usage_stats:
|
|
412
|
+
usage_stats: TokenUsage,
|
|
418
413
|
latency: float,
|
|
419
414
|
output: Any,
|
|
415
|
+
tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
420
416
|
available_tool_calls: Optional[List[Dict[str, Any]]] = None,
|
|
417
|
+
model_from_response: Optional[str] = None,
|
|
421
418
|
):
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
"
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
),
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
event_properties["$ai_tools"] = available_tool_calls
|
|
452
|
-
|
|
453
|
-
if posthog_distinct_id is None:
|
|
454
|
-
event_properties["$process_person_profile"] = False
|
|
419
|
+
from posthoganalytics.ai.types import StreamingEventData
|
|
420
|
+
from posthoganalytics.ai.openai.openai_converter import (
|
|
421
|
+
format_openai_streaming_input,
|
|
422
|
+
format_openai_streaming_output,
|
|
423
|
+
)
|
|
424
|
+
from posthoganalytics.ai.utils import capture_streaming_event
|
|
425
|
+
|
|
426
|
+
# Prepare standardized event data
|
|
427
|
+
formatted_input = format_openai_streaming_input(kwargs, "chat")
|
|
428
|
+
sanitized_input = sanitize_openai(formatted_input)
|
|
429
|
+
|
|
430
|
+
# Use model from kwargs, fallback to model from response
|
|
431
|
+
model = kwargs.get("model") or model_from_response or "unknown"
|
|
432
|
+
|
|
433
|
+
event_data = StreamingEventData(
|
|
434
|
+
provider="openai",
|
|
435
|
+
model=model,
|
|
436
|
+
base_url=str(self._client.base_url),
|
|
437
|
+
kwargs=kwargs,
|
|
438
|
+
formatted_input=sanitized_input,
|
|
439
|
+
formatted_output=format_openai_streaming_output(output, "chat", tool_calls),
|
|
440
|
+
usage_stats=usage_stats,
|
|
441
|
+
latency=latency,
|
|
442
|
+
distinct_id=posthog_distinct_id,
|
|
443
|
+
trace_id=posthog_trace_id,
|
|
444
|
+
properties=posthog_properties,
|
|
445
|
+
privacy_mode=posthog_privacy_mode,
|
|
446
|
+
groups=posthog_groups,
|
|
447
|
+
)
|
|
455
448
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
distinct_id=posthog_distinct_id or posthog_trace_id,
|
|
459
|
-
event="$ai_generation",
|
|
460
|
-
properties=event_properties,
|
|
461
|
-
groups=posthog_groups,
|
|
462
|
-
)
|
|
449
|
+
# Use the common capture function
|
|
450
|
+
capture_streaming_event(self._client._ph_client, event_data)
|
|
463
451
|
|
|
464
452
|
|
|
465
453
|
class WrappedEmbeddings:
|
|
@@ -496,6 +484,7 @@ class WrappedEmbeddings:
|
|
|
496
484
|
Returns:
|
|
497
485
|
The response from OpenAI's embeddings.create call.
|
|
498
486
|
"""
|
|
487
|
+
|
|
499
488
|
if posthog_trace_id is None:
|
|
500
489
|
posthog_trace_id = str(uuid.uuid4())
|
|
501
490
|
|
|
@@ -518,7 +507,9 @@ class WrappedEmbeddings:
|
|
|
518
507
|
"$ai_provider": "openai",
|
|
519
508
|
"$ai_model": kwargs.get("model"),
|
|
520
509
|
"$ai_input": with_privacy_mode(
|
|
521
|
-
self._client._ph_client,
|
|
510
|
+
self._client._ph_client,
|
|
511
|
+
posthog_privacy_mode,
|
|
512
|
+
sanitize_openai_response(kwargs.get("input")),
|
|
522
513
|
),
|
|
523
514
|
"$ai_http_status": 200,
|
|
524
515
|
"$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
|