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.
Files changed (40) hide show
  1. posthoganalytics/__init__.py +84 -7
  2. posthoganalytics/ai/anthropic/__init__.py +10 -0
  3. posthoganalytics/ai/anthropic/anthropic.py +95 -65
  4. posthoganalytics/ai/anthropic/anthropic_async.py +95 -65
  5. posthoganalytics/ai/anthropic/anthropic_converter.py +443 -0
  6. posthoganalytics/ai/gemini/__init__.py +15 -1
  7. posthoganalytics/ai/gemini/gemini.py +66 -71
  8. posthoganalytics/ai/gemini/gemini_async.py +423 -0
  9. posthoganalytics/ai/gemini/gemini_converter.py +652 -0
  10. posthoganalytics/ai/langchain/callbacks.py +58 -13
  11. posthoganalytics/ai/openai/__init__.py +16 -1
  12. posthoganalytics/ai/openai/openai.py +140 -149
  13. posthoganalytics/ai/openai/openai_async.py +127 -82
  14. posthoganalytics/ai/openai/openai_converter.py +741 -0
  15. posthoganalytics/ai/sanitization.py +248 -0
  16. posthoganalytics/ai/types.py +125 -0
  17. posthoganalytics/ai/utils.py +339 -356
  18. posthoganalytics/client.py +345 -97
  19. posthoganalytics/contexts.py +81 -0
  20. posthoganalytics/exception_utils.py +250 -2
  21. posthoganalytics/feature_flags.py +26 -10
  22. posthoganalytics/flag_definition_cache.py +127 -0
  23. posthoganalytics/integrations/django.py +157 -19
  24. posthoganalytics/request.py +203 -23
  25. posthoganalytics/test/test_client.py +250 -22
  26. posthoganalytics/test/test_exception_capture.py +418 -0
  27. posthoganalytics/test/test_feature_flag_result.py +441 -2
  28. posthoganalytics/test/test_feature_flags.py +308 -104
  29. posthoganalytics/test/test_flag_definition_cache.py +612 -0
  30. posthoganalytics/test/test_module.py +0 -8
  31. posthoganalytics/test/test_request.py +536 -0
  32. posthoganalytics/test/test_utils.py +4 -1
  33. posthoganalytics/types.py +40 -0
  34. posthoganalytics/version.py +1 -1
  35. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
  36. posthoganalytics-7.4.3.dist-info/RECORD +57 -0
  37. posthoganalytics-6.7.0.dist-info/RECORD +0 -49
  38. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
  39. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
  40. {posthoganalytics-6.7.0.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,61 @@
1
1
  import datetime # noqa: F401
2
- from typing import Callable, Dict, Optional, Any # noqa: F401
2
+ from typing import Any, Callable, Dict, Optional # noqa: F401
3
+
3
4
  from typing_extensions import Unpack
4
5
 
5
- from posthoganalytics.args import OptionalCaptureArgs, OptionalSetArgs, ExceptionArg
6
+ from posthoganalytics.args import ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
6
7
  from posthoganalytics.client import Client
8
+ from posthoganalytics.contexts import (
9
+ identify_context as inner_identify_context,
10
+ )
7
11
  from posthoganalytics.contexts import (
8
12
  new_context as inner_new_context,
13
+ )
14
+ from posthoganalytics.contexts import (
9
15
  scoped as inner_scoped,
10
- tag as inner_tag,
16
+ )
17
+ from posthoganalytics.contexts import (
18
+ set_capture_exception_code_variables_context as inner_set_capture_exception_code_variables_context,
19
+ )
20
+ from posthoganalytics.contexts import (
21
+ set_code_variables_ignore_patterns_context as inner_set_code_variables_ignore_patterns_context,
22
+ )
23
+ from posthoganalytics.contexts import (
24
+ set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
25
+ )
26
+ from posthoganalytics.contexts import (
11
27
  set_context_session as inner_set_context_session,
12
- identify_context as inner_identify_context,
13
28
  )
14
- from posthoganalytics.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
29
+ from posthoganalytics.contexts import (
30
+ tag as inner_tag,
31
+ )
32
+ from posthoganalytics.exception_utils import (
33
+ DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
34
+ DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
35
+ )
36
+ from posthoganalytics.feature_flags import (
37
+ InconclusiveMatchError as InconclusiveMatchError,
38
+ )
39
+ from posthoganalytics.feature_flags import (
40
+ RequiresServerEvaluation as RequiresServerEvaluation,
41
+ )
42
+ from posthoganalytics.flag_definition_cache import (
43
+ FlagDefinitionCacheData as FlagDefinitionCacheData,
44
+ FlagDefinitionCacheProvider as FlagDefinitionCacheProvider,
45
+ )
46
+ from posthoganalytics.request import (
47
+ disable_connection_reuse as disable_connection_reuse,
48
+ enable_keep_alive as enable_keep_alive,
49
+ set_socket_options as set_socket_options,
50
+ SocketOptions as SocketOptions,
51
+ )
52
+ from posthoganalytics.types import (
53
+ FeatureFlag,
54
+ FlagsAndPayloads,
55
+ )
56
+ from posthoganalytics.types import (
57
+ FeatureFlagResult as FeatureFlagResult,
58
+ )
15
59
  from posthoganalytics.version import VERSION
16
60
 
17
61
  __version__ = VERSION
@@ -19,13 +63,14 @@ __version__ = VERSION
19
63
  """Context management."""
20
64
 
21
65
 
22
- def new_context(fresh=False, capture_exceptions=True):
66
+ def new_context(fresh=False, capture_exceptions=True, client=None):
23
67
  """
24
68
  Create a new context scope that will be active for the duration of the with block.
25
69
 
26
70
  Args:
27
71
  fresh: Whether to start with a fresh context (default: False)
28
72
  capture_exceptions: Whether to capture exceptions raised within the context (default: True)
73
+ client: Optional Posthog client instance to use for this context (default: None)
29
74
 
30
75
  Examples:
31
76
  ```python
@@ -38,7 +83,9 @@ def new_context(fresh=False, capture_exceptions=True):
38
83
  Category:
39
84
  Contexts
40
85
  """
41
- return inner_new_context(fresh=fresh, capture_exceptions=capture_exceptions)
86
+ return inner_new_context(
87
+ fresh=fresh, capture_exceptions=capture_exceptions, client=client
88
+ )
42
89
 
43
90
 
44
91
  def scoped(fresh=False, capture_exceptions=True):
@@ -102,6 +149,27 @@ def identify_context(distinct_id: str):
102
149
  return inner_identify_context(distinct_id)
103
150
 
104
151
 
152
+ def set_capture_exception_code_variables_context(enabled: bool):
153
+ """
154
+ Set whether code variables are captured for the current context.
155
+ """
156
+ return inner_set_capture_exception_code_variables_context(enabled)
157
+
158
+
159
+ def set_code_variables_mask_patterns_context(mask_patterns: list):
160
+ """
161
+ Variable names matching these patterns will be masked with *** when capturing code variables.
162
+ """
163
+ return inner_set_code_variables_mask_patterns_context(mask_patterns)
164
+
165
+
166
+ def set_code_variables_ignore_patterns_context(ignore_patterns: list):
167
+ """
168
+ Variable names matching these patterns will be ignored completely when capturing code variables.
169
+ """
170
+ return inner_set_code_variables_ignore_patterns_context(ignore_patterns)
171
+
172
+
105
173
  def tag(name: str, value: Any):
106
174
  """
107
175
  Add a tag to the current context.
@@ -149,6 +217,11 @@ enable_local_evaluation = True # type: bool
149
217
 
150
218
  default_client = None # type: Optional[Client]
151
219
 
220
+ capture_exception_code_variables = False
221
+ code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
222
+ code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
223
+ in_app_modules = None # type: Optional[list[str]]
224
+
152
225
 
153
226
  # NOTE - this and following functions take unpacked kwargs because we needed to make
154
227
  # it impossible to write `posthog.capture(distinct-id, event-name)` - basically, to enforce
@@ -743,6 +816,10 @@ def setup() -> Client:
743
816
  enable_exception_autocapture=enable_exception_autocapture,
744
817
  log_captured_exceptions=log_captured_exceptions,
745
818
  enable_local_evaluation=enable_local_evaluation,
819
+ capture_exception_code_variables=capture_exception_code_variables,
820
+ code_variables_mask_patterns=code_variables_mask_patterns,
821
+ code_variables_ignore_patterns=code_variables_ignore_patterns,
822
+ in_app_modules=in_app_modules,
746
823
  )
747
824
 
748
825
  # always set incase user changes it
@@ -6,6 +6,12 @@ from .anthropic_providers import (
6
6
  AsyncAnthropicBedrock,
7
7
  AsyncAnthropicVertex,
8
8
  )
9
+ from .anthropic_converter import (
10
+ format_anthropic_response,
11
+ format_anthropic_input,
12
+ extract_anthropic_tools,
13
+ format_anthropic_streaming_content,
14
+ )
9
15
 
10
16
  __all__ = [
11
17
  "Anthropic",
@@ -14,4 +20,8 @@ __all__ = [
14
20
  "AsyncAnthropicBedrock",
15
21
  "AnthropicVertex",
16
22
  "AsyncAnthropicVertex",
23
+ "format_anthropic_response",
24
+ "format_anthropic_input",
25
+ "extract_anthropic_tools",
26
+ "format_anthropic_streaming_content",
17
27
  ]
@@ -8,14 +8,21 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
+ from posthoganalytics.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
13
14
  from posthoganalytics.ai.utils import (
14
15
  call_llm_and_track_usage,
15
- get_model_params,
16
- merge_system_prompt,
17
- with_privacy_mode,
16
+ merge_usage_stats,
18
17
  )
18
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
19
+ extract_anthropic_usage_from_event,
20
+ handle_anthropic_content_block_start,
21
+ handle_anthropic_text_delta,
22
+ handle_anthropic_tool_delta,
23
+ finalize_anthropic_tool_input,
24
+ )
25
+ from posthoganalytics.ai.sanitization import sanitize_anthropic
19
26
  from posthoganalytics.client import Client as PostHogClient
20
27
  from posthoganalytics import setup
21
28
 
@@ -61,6 +68,7 @@ class WrappedMessages(Messages):
61
68
  posthog_groups: Optional group analytics properties
62
69
  **kwargs: Arguments passed to Anthropic's messages.create
63
70
  """
71
+
64
72
  if posthog_trace_id is None:
65
73
  posthog_trace_id = str(uuid.uuid4())
66
74
 
@@ -118,35 +126,66 @@ class WrappedMessages(Messages):
118
126
  **kwargs: Any,
119
127
  ):
120
128
  start_time = time.time()
121
- usage_stats: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0}
122
- accumulated_content = []
129
+ usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
130
+ accumulated_content = ""
131
+ content_blocks: List[StreamingContentBlock] = []
132
+ tools_in_progress: Dict[str, ToolInProgress] = {}
133
+ current_text_block: Optional[StreamingContentBlock] = None
123
134
  response = super().create(**kwargs)
124
135
 
125
136
  def generator():
126
137
  nonlocal usage_stats
127
- nonlocal accumulated_content # noqa: F824
138
+ nonlocal accumulated_content
139
+ nonlocal content_blocks
140
+ nonlocal tools_in_progress
141
+ nonlocal current_text_block
142
+
128
143
  try:
129
144
  for event in response:
130
- if hasattr(event, "usage") and event.usage:
131
- usage_stats = {
132
- k: getattr(event.usage, k, 0)
133
- for k in [
134
- "input_tokens",
135
- "output_tokens",
136
- "cache_read_input_tokens",
137
- "cache_creation_input_tokens",
138
- ]
139
- }
140
-
141
- if hasattr(event, "content") and event.content:
142
- accumulated_content.append(event.content)
145
+ # Extract usage stats from event
146
+ event_usage = extract_anthropic_usage_from_event(event)
147
+ merge_usage_stats(usage_stats, event_usage)
148
+
149
+ # Handle content block start events
150
+ if hasattr(event, "type") and event.type == "content_block_start":
151
+ block, tool = handle_anthropic_content_block_start(event)
152
+
153
+ if block:
154
+ content_blocks.append(block)
155
+
156
+ if block.get("type") == "text":
157
+ current_text_block = block
158
+ else:
159
+ current_text_block = None
160
+
161
+ if tool:
162
+ tool_id = tool["block"].get("id")
163
+ if tool_id:
164
+ tools_in_progress[tool_id] = tool
165
+
166
+ # Handle text delta events
167
+ delta_text = handle_anthropic_text_delta(event, current_text_block)
168
+
169
+ if delta_text:
170
+ accumulated_content += delta_text
171
+
172
+ # Handle tool input delta events
173
+ handle_anthropic_tool_delta(
174
+ event, content_blocks, tools_in_progress
175
+ )
176
+
177
+ # Handle content block stop events
178
+ if hasattr(event, "type") and event.type == "content_block_stop":
179
+ current_text_block = None
180
+ finalize_anthropic_tool_input(
181
+ event, content_blocks, tools_in_progress
182
+ )
143
183
 
144
184
  yield event
145
185
 
146
186
  finally:
147
187
  end_time = time.time()
148
188
  latency = end_time - start_time
149
- output = "".join(accumulated_content)
150
189
 
151
190
  self._capture_streaming_event(
152
191
  posthog_distinct_id,
@@ -157,7 +196,8 @@ class WrappedMessages(Messages):
157
196
  kwargs,
158
197
  usage_stats,
159
198
  latency,
160
- output,
199
+ content_blocks,
200
+ accumulated_content,
161
201
  )
162
202
 
163
203
  return generator()
@@ -170,49 +210,39 @@ class WrappedMessages(Messages):
170
210
  posthog_privacy_mode: bool,
171
211
  posthog_groups: Optional[Dict[str, Any]],
172
212
  kwargs: Dict[str, Any],
173
- usage_stats: Dict[str, int],
213
+ usage_stats: TokenUsage,
174
214
  latency: float,
175
- output: str,
215
+ content_blocks: List[StreamingContentBlock],
216
+ accumulated_content: str,
176
217
  ):
177
- if posthog_trace_id is None:
178
- posthog_trace_id = str(uuid.uuid4())
179
-
180
- event_properties = {
181
- "$ai_provider": "anthropic",
182
- "$ai_model": kwargs.get("model"),
183
- "$ai_model_parameters": get_model_params(kwargs),
184
- "$ai_input": with_privacy_mode(
185
- self._client._ph_client,
186
- posthog_privacy_mode,
187
- merge_system_prompt(kwargs, "anthropic"),
188
- ),
189
- "$ai_output_choices": with_privacy_mode(
190
- self._client._ph_client,
191
- posthog_privacy_mode,
192
- [{"content": output, "role": "assistant"}],
193
- ),
194
- "$ai_http_status": 200,
195
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
196
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
197
- "$ai_cache_read_input_tokens": usage_stats.get(
198
- "cache_read_input_tokens", 0
199
- ),
200
- "$ai_cache_creation_input_tokens": usage_stats.get(
201
- "cache_creation_input_tokens", 0
218
+ from posthoganalytics.ai.types import StreamingEventData
219
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
220
+ format_anthropic_streaming_input,
221
+ format_anthropic_streaming_output_complete,
222
+ )
223
+ from posthoganalytics.ai.utils import capture_streaming_event
224
+
225
+ # Prepare standardized event data
226
+ formatted_input = format_anthropic_streaming_input(kwargs)
227
+ sanitized_input = sanitize_anthropic(formatted_input)
228
+
229
+ event_data = StreamingEventData(
230
+ provider="anthropic",
231
+ model=kwargs.get("model", "unknown"),
232
+ base_url=str(self._client.base_url),
233
+ kwargs=kwargs,
234
+ formatted_input=sanitized_input,
235
+ formatted_output=format_anthropic_streaming_output_complete(
236
+ content_blocks, accumulated_content
202
237
  ),
203
- "$ai_latency": latency,
204
- "$ai_trace_id": posthog_trace_id,
205
- "$ai_base_url": str(self._client.base_url),
206
- **(posthog_properties or {}),
207
- }
208
-
209
- if posthog_distinct_id is None:
210
- event_properties["$process_person_profile"] = False
211
-
212
- if hasattr(self._client._ph_client, "capture"):
213
- self._client._ph_client.capture(
214
- distinct_id=posthog_distinct_id or posthog_trace_id,
215
- event="$ai_generation",
216
- properties=event_properties,
217
- groups=posthog_groups,
218
- )
238
+ usage_stats=usage_stats,
239
+ latency=latency,
240
+ distinct_id=posthog_distinct_id,
241
+ trace_id=posthog_trace_id,
242
+ properties=posthog_properties,
243
+ privacy_mode=posthog_privacy_mode,
244
+ groups=posthog_groups,
245
+ )
246
+
247
+ # Use the common capture function
248
+ capture_streaming_event(self._client._ph_client, event_data)
@@ -8,15 +8,22 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
13
  from posthoganalytics import setup
14
+ from posthoganalytics.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
14
15
  from posthoganalytics.ai.utils import (
15
16
  call_llm_and_track_usage_async,
16
- get_model_params,
17
- merge_system_prompt,
18
- with_privacy_mode,
17
+ merge_usage_stats,
19
18
  )
19
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
20
+ extract_anthropic_usage_from_event,
21
+ handle_anthropic_content_block_start,
22
+ handle_anthropic_text_delta,
23
+ handle_anthropic_tool_delta,
24
+ finalize_anthropic_tool_input,
25
+ )
26
+ from posthoganalytics.ai.sanitization import sanitize_anthropic
20
27
  from posthoganalytics.client import Client as PostHogClient
21
28
 
22
29
 
@@ -61,6 +68,7 @@ class AsyncWrappedMessages(AsyncMessages):
61
68
  posthog_groups: Optional group analytics properties
62
69
  **kwargs: Arguments passed to Anthropic's messages.create
63
70
  """
71
+
64
72
  if posthog_trace_id is None:
65
73
  posthog_trace_id = str(uuid.uuid4())
66
74
 
@@ -118,35 +126,66 @@ class AsyncWrappedMessages(AsyncMessages):
118
126
  **kwargs: Any,
119
127
  ):
120
128
  start_time = time.time()
121
- usage_stats: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0}
122
- accumulated_content = []
129
+ usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
130
+ accumulated_content = ""
131
+ content_blocks: List[StreamingContentBlock] = []
132
+ tools_in_progress: Dict[str, ToolInProgress] = {}
133
+ current_text_block: Optional[StreamingContentBlock] = None
123
134
  response = await super().create(**kwargs)
124
135
 
125
136
  async def generator():
126
137
  nonlocal usage_stats
127
- nonlocal accumulated_content # noqa: F824
138
+ nonlocal accumulated_content
139
+ nonlocal content_blocks
140
+ nonlocal tools_in_progress
141
+ nonlocal current_text_block
142
+
128
143
  try:
129
144
  async for event in response:
130
- if hasattr(event, "usage") and event.usage:
131
- usage_stats = {
132
- k: getattr(event.usage, k, 0)
133
- for k in [
134
- "input_tokens",
135
- "output_tokens",
136
- "cache_read_input_tokens",
137
- "cache_creation_input_tokens",
138
- ]
139
- }
140
-
141
- if hasattr(event, "content") and event.content:
142
- accumulated_content.append(event.content)
145
+ # Extract usage stats from event
146
+ event_usage = extract_anthropic_usage_from_event(event)
147
+ merge_usage_stats(usage_stats, event_usage)
148
+
149
+ # Handle content block start events
150
+ if hasattr(event, "type") and event.type == "content_block_start":
151
+ block, tool = handle_anthropic_content_block_start(event)
152
+
153
+ if block:
154
+ content_blocks.append(block)
155
+
156
+ if block.get("type") == "text":
157
+ current_text_block = block
158
+ else:
159
+ current_text_block = None
160
+
161
+ if tool:
162
+ tool_id = tool["block"].get("id")
163
+ if tool_id:
164
+ tools_in_progress[tool_id] = tool
165
+
166
+ # Handle text delta events
167
+ delta_text = handle_anthropic_text_delta(event, current_text_block)
168
+
169
+ if delta_text:
170
+ accumulated_content += delta_text
171
+
172
+ # Handle tool input delta events
173
+ handle_anthropic_tool_delta(
174
+ event, content_blocks, tools_in_progress
175
+ )
176
+
177
+ # Handle content block stop events
178
+ if hasattr(event, "type") and event.type == "content_block_stop":
179
+ current_text_block = None
180
+ finalize_anthropic_tool_input(
181
+ event, content_blocks, tools_in_progress
182
+ )
143
183
 
144
184
  yield event
145
185
 
146
186
  finally:
147
187
  end_time = time.time()
148
188
  latency = end_time - start_time
149
- output = "".join(accumulated_content)
150
189
 
151
190
  await self._capture_streaming_event(
152
191
  posthog_distinct_id,
@@ -157,7 +196,8 @@ class AsyncWrappedMessages(AsyncMessages):
157
196
  kwargs,
158
197
  usage_stats,
159
198
  latency,
160
- output,
199
+ content_blocks,
200
+ accumulated_content,
161
201
  )
162
202
 
163
203
  return generator()
@@ -170,49 +210,39 @@ class AsyncWrappedMessages(AsyncMessages):
170
210
  posthog_privacy_mode: bool,
171
211
  posthog_groups: Optional[Dict[str, Any]],
172
212
  kwargs: Dict[str, Any],
173
- usage_stats: Dict[str, int],
213
+ usage_stats: TokenUsage,
174
214
  latency: float,
175
- output: str,
215
+ content_blocks: List[StreamingContentBlock],
216
+ accumulated_content: str,
176
217
  ):
177
- if posthog_trace_id is None:
178
- posthog_trace_id = str(uuid.uuid4())
179
-
180
- event_properties = {
181
- "$ai_provider": "anthropic",
182
- "$ai_model": kwargs.get("model"),
183
- "$ai_model_parameters": get_model_params(kwargs),
184
- "$ai_input": with_privacy_mode(
185
- self._client._ph_client,
186
- posthog_privacy_mode,
187
- merge_system_prompt(kwargs, "anthropic"),
188
- ),
189
- "$ai_output_choices": with_privacy_mode(
190
- self._client._ph_client,
191
- posthog_privacy_mode,
192
- [{"content": output, "role": "assistant"}],
193
- ),
194
- "$ai_http_status": 200,
195
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
196
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
197
- "$ai_cache_read_input_tokens": usage_stats.get(
198
- "cache_read_input_tokens", 0
199
- ),
200
- "$ai_cache_creation_input_tokens": usage_stats.get(
201
- "cache_creation_input_tokens", 0
218
+ from posthoganalytics.ai.types import StreamingEventData
219
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
220
+ format_anthropic_streaming_input,
221
+ format_anthropic_streaming_output_complete,
222
+ )
223
+ from posthoganalytics.ai.utils import capture_streaming_event
224
+
225
+ # Prepare standardized event data
226
+ formatted_input = format_anthropic_streaming_input(kwargs)
227
+ sanitized_input = sanitize_anthropic(formatted_input)
228
+
229
+ event_data = StreamingEventData(
230
+ provider="anthropic",
231
+ model=kwargs.get("model", "unknown"),
232
+ base_url=str(self._client.base_url),
233
+ kwargs=kwargs,
234
+ formatted_input=sanitized_input,
235
+ formatted_output=format_anthropic_streaming_output_complete(
236
+ content_blocks, accumulated_content
202
237
  ),
203
- "$ai_latency": latency,
204
- "$ai_trace_id": posthog_trace_id,
205
- "$ai_base_url": str(self._client.base_url),
206
- **(posthog_properties or {}),
207
- }
208
-
209
- if posthog_distinct_id is None:
210
- event_properties["$process_person_profile"] = False
211
-
212
- if hasattr(self._client._ph_client, "capture"):
213
- self._client._ph_client.capture(
214
- distinct_id=posthog_distinct_id or posthog_trace_id,
215
- event="$ai_generation",
216
- properties=event_properties,
217
- groups=posthog_groups,
218
- )
238
+ usage_stats=usage_stats,
239
+ latency=latency,
240
+ distinct_id=posthog_distinct_id,
241
+ trace_id=posthog_trace_id,
242
+ properties=posthog_properties,
243
+ privacy_mode=posthog_privacy_mode,
244
+ groups=posthog_groups,
245
+ )
246
+
247
+ # Use the common capture function
248
+ capture_streaming_event(self._client._ph_client, event_data)