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
@@ -3,6 +3,9 @@ import time
3
3
  import uuid
4
4
  from typing import Any, Dict, Optional
5
5
 
6
+ from posthoganalytics.ai.types import TokenUsage, StreamingEventData
7
+ from posthoganalytics.ai.utils import merge_system_prompt
8
+
6
9
  try:
7
10
  from google import genai
8
11
  except ImportError:
@@ -13,9 +16,15 @@ except ImportError:
13
16
  from posthoganalytics import setup
14
17
  from posthoganalytics.ai.utils import (
15
18
  call_llm_and_track_usage,
16
- get_model_params,
17
- with_privacy_mode,
19
+ capture_streaming_event,
20
+ merge_usage_stats,
21
+ )
22
+ from posthoganalytics.ai.gemini.gemini_converter import (
23
+ extract_gemini_usage_from_chunk,
24
+ extract_gemini_content_from_chunk,
25
+ format_gemini_streaming_output,
18
26
  )
27
+ from posthoganalytics.ai.sanitization import sanitize_gemini
19
28
  from posthoganalytics.client import Client as PostHogClient
20
29
 
21
30
 
@@ -71,6 +80,7 @@ class Client:
71
80
  posthog_groups: Default groups for all calls (can be overridden per call)
72
81
  **kwargs: Additional arguments (for future compatibility)
73
82
  """
83
+
74
84
  self._ph_client = posthog_client or setup()
75
85
 
76
86
  if self._ph_client is None:
@@ -132,6 +142,7 @@ class Models:
132
142
  posthog_groups: Default groups for all calls
133
143
  **kwargs: Additional arguments (for future compatibility)
134
144
  """
145
+
135
146
  self._ph_client = posthog_client or setup()
136
147
 
137
148
  if self._ph_client is None:
@@ -149,14 +160,19 @@ class Models:
149
160
  # Add Vertex AI parameters if provided
150
161
  if vertexai is not None:
151
162
  client_args["vertexai"] = vertexai
163
+
152
164
  if credentials is not None:
153
165
  client_args["credentials"] = credentials
166
+
154
167
  if project is not None:
155
168
  client_args["project"] = project
169
+
156
170
  if location is not None:
157
171
  client_args["location"] = location
172
+
158
173
  if debug_config is not None:
159
174
  client_args["debug_config"] = debug_config
175
+
160
176
  if http_options is not None:
161
177
  client_args["http_options"] = http_options
162
178
 
@@ -174,6 +190,7 @@ class Models:
174
190
  raise ValueError(
175
191
  "API key must be provided either as parameter or via GOOGLE_API_KEY/API_KEY environment variable"
176
192
  )
193
+
177
194
  client_args["api_key"] = api_key
178
195
 
179
196
  self._client = genai.Client(**client_args)
@@ -188,6 +205,7 @@ class Models:
188
205
  call_groups: Optional[Dict[str, Any]],
189
206
  ):
190
207
  """Merge call-level PostHog parameters with client defaults."""
208
+
191
209
  # Use call-level values if provided, otherwise fall back to defaults
192
210
  distinct_id = (
193
211
  call_distinct_id
@@ -203,6 +221,7 @@ class Models:
203
221
 
204
222
  # Merge properties: default properties + call properties (call properties override)
205
223
  properties = dict(self._default_properties)
224
+
206
225
  if call_properties:
207
226
  properties.update(call_properties)
208
227
 
@@ -238,6 +257,7 @@ class Models:
238
257
  posthog_groups: Group analytics properties (overrides client default)
239
258
  **kwargs: Arguments passed to Gemini's generate_content
240
259
  """
260
+
241
261
  # Merge PostHog parameters
242
262
  distinct_id, trace_id, properties, privacy_mode, groups = (
243
263
  self._merge_posthog_params(
@@ -276,7 +296,7 @@ class Models:
276
296
  **kwargs: Any,
277
297
  ):
278
298
  start_time = time.time()
279
- usage_stats: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0}
299
+ usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
280
300
  accumulated_content = []
281
301
 
282
302
  kwargs_without_stream = {"model": model, "contents": contents, **kwargs}
@@ -284,28 +304,27 @@ class Models:
284
304
 
285
305
  def generator():
286
306
  nonlocal usage_stats
287
- nonlocal accumulated_content # noqa: F824
307
+ nonlocal accumulated_content
288
308
  try:
289
309
  for chunk in response:
290
- if hasattr(chunk, "usage_metadata") and chunk.usage_metadata:
291
- usage_stats = {
292
- "input_tokens": getattr(
293
- chunk.usage_metadata, "prompt_token_count", 0
294
- ),
295
- "output_tokens": getattr(
296
- chunk.usage_metadata, "candidates_token_count", 0
297
- ),
298
- }
299
-
300
- if hasattr(chunk, "text") and chunk.text:
301
- accumulated_content.append(chunk.text)
310
+ # Extract usage stats from chunk
311
+ chunk_usage = extract_gemini_usage_from_chunk(chunk)
312
+
313
+ if chunk_usage:
314
+ # Gemini reports cumulative totals, not incremental values
315
+ merge_usage_stats(usage_stats, chunk_usage, mode="cumulative")
316
+
317
+ # Extract content from chunk (now returns content blocks)
318
+ content_block = extract_gemini_content_from_chunk(chunk)
319
+
320
+ if content_block is not None:
321
+ accumulated_content.append(content_block)
302
322
 
303
323
  yield chunk
304
324
 
305
325
  finally:
306
326
  end_time = time.time()
307
327
  latency = end_time - start_time
308
- output = "".join(accumulated_content)
309
328
 
310
329
  self._capture_streaming_event(
311
330
  model,
@@ -318,7 +337,7 @@ class Models:
318
337
  kwargs,
319
338
  usage_stats,
320
339
  latency,
321
- output,
340
+ accumulated_content,
322
341
  )
323
342
 
324
343
  return generator()
@@ -333,63 +352,39 @@ class Models:
333
352
  privacy_mode: bool,
334
353
  groups: Optional[Dict[str, Any]],
335
354
  kwargs: Dict[str, Any],
336
- usage_stats: Dict[str, int],
355
+ usage_stats: TokenUsage,
337
356
  latency: float,
338
- output: str,
357
+ output: Any,
339
358
  ):
340
- if trace_id is None:
341
- trace_id = str(uuid.uuid4())
342
-
343
- event_properties = {
344
- "$ai_provider": "gemini",
345
- "$ai_model": model,
346
- "$ai_model_parameters": get_model_params(kwargs),
347
- "$ai_input": with_privacy_mode(
348
- self._ph_client,
349
- privacy_mode,
350
- self._format_input(contents),
351
- ),
352
- "$ai_output_choices": with_privacy_mode(
353
- self._ph_client,
354
- privacy_mode,
355
- [{"content": output, "role": "assistant"}],
356
- ),
357
- "$ai_http_status": 200,
358
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
359
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
360
- "$ai_latency": latency,
361
- "$ai_trace_id": trace_id,
362
- "$ai_base_url": self._base_url,
363
- **(properties or {}),
364
- }
365
-
366
- if distinct_id is None:
367
- event_properties["$process_person_profile"] = False
368
-
369
- if hasattr(self._ph_client, "capture"):
370
- self._ph_client.capture(
371
- distinct_id=distinct_id,
372
- event="$ai_generation",
373
- properties=event_properties,
374
- groups=groups,
375
- )
359
+ # Prepare standardized event data
360
+ formatted_input = self._format_input(contents, **kwargs)
361
+ sanitized_input = sanitize_gemini(formatted_input)
362
+
363
+ event_data = StreamingEventData(
364
+ provider="gemini",
365
+ model=model,
366
+ base_url=self._base_url,
367
+ kwargs=kwargs,
368
+ formatted_input=sanitized_input,
369
+ formatted_output=format_gemini_streaming_output(output),
370
+ usage_stats=usage_stats,
371
+ latency=latency,
372
+ distinct_id=distinct_id,
373
+ trace_id=trace_id,
374
+ properties=properties,
375
+ privacy_mode=privacy_mode,
376
+ groups=groups,
377
+ )
378
+
379
+ # Use the common capture function
380
+ capture_streaming_event(self._ph_client, event_data)
376
381
 
377
- def _format_input(self, contents):
382
+ def _format_input(self, contents, **kwargs):
378
383
  """Format input contents for PostHog tracking"""
379
- if isinstance(contents, str):
380
- return [{"role": "user", "content": contents}]
381
- elif isinstance(contents, list):
382
- formatted = []
383
- for item in contents:
384
- if isinstance(item, str):
385
- formatted.append({"role": "user", "content": item})
386
- elif hasattr(item, "text"):
387
- formatted.append({"role": "user", "content": item.text})
388
- else:
389
- formatted.append({"role": "user", "content": str(item)})
390
- return formatted
391
- else:
392
- return [{"role": "user", "content": str(contents)}]
384
+
385
+ # Create kwargs dict with contents for merge_system_prompt
386
+ input_kwargs = {"contents": contents, **kwargs}
387
+ return merge_system_prompt(input_kwargs, "gemini")
393
388
 
394
389
  def generate_content_stream(
395
390
  self,
@@ -0,0 +1,423 @@
1
+ import os
2
+ import time
3
+ import uuid
4
+ from typing import Any, Dict, Optional
5
+
6
+ from posthoganalytics.ai.types import TokenUsage, StreamingEventData
7
+ from posthoganalytics.ai.utils import merge_system_prompt
8
+
9
+ try:
10
+ from google import genai
11
+ except ImportError:
12
+ raise ModuleNotFoundError(
13
+ "Please install the Google Gemini SDK to use this feature: 'pip install google-genai'"
14
+ )
15
+
16
+ from posthoganalytics import setup
17
+ from posthoganalytics.ai.utils import (
18
+ call_llm_and_track_usage_async,
19
+ capture_streaming_event,
20
+ merge_usage_stats,
21
+ )
22
+ from posthoganalytics.ai.gemini.gemini_converter import (
23
+ extract_gemini_usage_from_chunk,
24
+ extract_gemini_content_from_chunk,
25
+ format_gemini_streaming_output,
26
+ )
27
+ from posthoganalytics.ai.sanitization import sanitize_gemini
28
+ from posthoganalytics.client import Client as PostHogClient
29
+
30
+
31
+ class AsyncClient:
32
+ """
33
+ An async drop-in replacement for genai.Client that automatically sends LLM usage events to PostHog.
34
+
35
+ Usage:
36
+ client = AsyncClient(
37
+ api_key="your_api_key",
38
+ posthog_client=posthog_client,
39
+ posthog_distinct_id="default_user", # Optional defaults
40
+ posthog_properties={"team": "ai"} # Optional defaults
41
+ )
42
+ response = await client.models.generate_content(
43
+ model="gemini-2.0-flash",
44
+ contents=["Hello world"],
45
+ posthog_distinct_id="specific_user" # Override default
46
+ )
47
+ """
48
+
49
+ _ph_client: PostHogClient
50
+
51
+ def __init__(
52
+ self,
53
+ api_key: Optional[str] = None,
54
+ vertexai: Optional[bool] = None,
55
+ credentials: Optional[Any] = None,
56
+ project: Optional[str] = None,
57
+ location: Optional[str] = None,
58
+ debug_config: Optional[Any] = None,
59
+ http_options: Optional[Any] = None,
60
+ posthog_client: Optional[PostHogClient] = None,
61
+ posthog_distinct_id: Optional[str] = None,
62
+ posthog_properties: Optional[Dict[str, Any]] = None,
63
+ posthog_privacy_mode: bool = False,
64
+ posthog_groups: Optional[Dict[str, Any]] = None,
65
+ **kwargs,
66
+ ):
67
+ """
68
+ Args:
69
+ api_key: Google AI API key. If not provided, will use GOOGLE_API_KEY or API_KEY environment variable (not required for Vertex AI)
70
+ vertexai: Whether to use Vertex AI authentication
71
+ credentials: Vertex AI credentials object
72
+ project: GCP project ID for Vertex AI
73
+ location: GCP location for Vertex AI
74
+ debug_config: Debug configuration for the client
75
+ http_options: HTTP options for the client
76
+ posthog_client: PostHog client for tracking usage
77
+ posthog_distinct_id: Default distinct ID for all calls (can be overridden per call)
78
+ posthog_properties: Default properties for all calls (can be overridden per call)
79
+ posthog_privacy_mode: Default privacy mode for all calls (can be overridden per call)
80
+ posthog_groups: Default groups for all calls (can be overridden per call)
81
+ **kwargs: Additional arguments (for future compatibility)
82
+ """
83
+
84
+ self._ph_client = posthog_client or setup()
85
+
86
+ if self._ph_client is None:
87
+ raise ValueError("posthog_client is required for PostHog tracking")
88
+
89
+ self.models = AsyncModels(
90
+ api_key=api_key,
91
+ vertexai=vertexai,
92
+ credentials=credentials,
93
+ project=project,
94
+ location=location,
95
+ debug_config=debug_config,
96
+ http_options=http_options,
97
+ posthog_client=self._ph_client,
98
+ posthog_distinct_id=posthog_distinct_id,
99
+ posthog_properties=posthog_properties,
100
+ posthog_privacy_mode=posthog_privacy_mode,
101
+ posthog_groups=posthog_groups,
102
+ **kwargs,
103
+ )
104
+
105
+
106
+ class AsyncModels:
107
+ """
108
+ Async Models interface that mimics genai.Client().aio.models with PostHog tracking.
109
+ """
110
+
111
+ _ph_client: PostHogClient # Not None after __init__ validation
112
+
113
+ def __init__(
114
+ self,
115
+ api_key: Optional[str] = None,
116
+ vertexai: Optional[bool] = None,
117
+ credentials: Optional[Any] = None,
118
+ project: Optional[str] = None,
119
+ location: Optional[str] = None,
120
+ debug_config: Optional[Any] = None,
121
+ http_options: Optional[Any] = None,
122
+ posthog_client: Optional[PostHogClient] = None,
123
+ posthog_distinct_id: Optional[str] = None,
124
+ posthog_properties: Optional[Dict[str, Any]] = None,
125
+ posthog_privacy_mode: bool = False,
126
+ posthog_groups: Optional[Dict[str, Any]] = None,
127
+ **kwargs,
128
+ ):
129
+ """
130
+ Args:
131
+ api_key: Google AI API key. If not provided, will use GOOGLE_API_KEY or API_KEY environment variable (not required for Vertex AI)
132
+ vertexai: Whether to use Vertex AI authentication
133
+ credentials: Vertex AI credentials object
134
+ project: GCP project ID for Vertex AI
135
+ location: GCP location for Vertex AI
136
+ debug_config: Debug configuration for the client
137
+ http_options: HTTP options for the client
138
+ posthog_client: PostHog client for tracking usage
139
+ posthog_distinct_id: Default distinct ID for all calls
140
+ posthog_properties: Default properties for all calls
141
+ posthog_privacy_mode: Default privacy mode for all calls
142
+ posthog_groups: Default groups for all calls
143
+ **kwargs: Additional arguments (for future compatibility)
144
+ """
145
+
146
+ self._ph_client = posthog_client or setup()
147
+
148
+ if self._ph_client is None:
149
+ raise ValueError("posthog_client is required for PostHog tracking")
150
+
151
+ # Store default PostHog settings
152
+ self._default_distinct_id = posthog_distinct_id
153
+ self._default_properties = posthog_properties or {}
154
+ self._default_privacy_mode = posthog_privacy_mode
155
+ self._default_groups = posthog_groups
156
+
157
+ # Build genai.Client arguments
158
+ client_args: Dict[str, Any] = {}
159
+
160
+ # Add Vertex AI parameters if provided
161
+ if vertexai is not None:
162
+ client_args["vertexai"] = vertexai
163
+
164
+ if credentials is not None:
165
+ client_args["credentials"] = credentials
166
+
167
+ if project is not None:
168
+ client_args["project"] = project
169
+
170
+ if location is not None:
171
+ client_args["location"] = location
172
+
173
+ if debug_config is not None:
174
+ client_args["debug_config"] = debug_config
175
+
176
+ if http_options is not None:
177
+ client_args["http_options"] = http_options
178
+
179
+ # Handle API key authentication
180
+ if vertexai:
181
+ # For Vertex AI, api_key is optional
182
+ if api_key is not None:
183
+ client_args["api_key"] = api_key
184
+ else:
185
+ # For non-Vertex AI mode, api_key is required (backwards compatibility)
186
+ if api_key is None:
187
+ api_key = os.environ.get("GOOGLE_API_KEY") or os.environ.get("API_KEY")
188
+
189
+ if api_key is None:
190
+ raise ValueError(
191
+ "API key must be provided either as parameter or via GOOGLE_API_KEY/API_KEY environment variable"
192
+ )
193
+
194
+ client_args["api_key"] = api_key
195
+
196
+ self._client = genai.Client(**client_args)
197
+ self._base_url = "https://generativelanguage.googleapis.com"
198
+
199
+ def _merge_posthog_params(
200
+ self,
201
+ call_distinct_id: Optional[str],
202
+ call_trace_id: Optional[str],
203
+ call_properties: Optional[Dict[str, Any]],
204
+ call_privacy_mode: Optional[bool],
205
+ call_groups: Optional[Dict[str, Any]],
206
+ ):
207
+ """Merge call-level PostHog parameters with client defaults."""
208
+
209
+ # Use call-level values if provided, otherwise fall back to defaults
210
+ distinct_id = (
211
+ call_distinct_id
212
+ if call_distinct_id is not None
213
+ else self._default_distinct_id
214
+ )
215
+ privacy_mode = (
216
+ call_privacy_mode
217
+ if call_privacy_mode is not None
218
+ else self._default_privacy_mode
219
+ )
220
+ groups = call_groups if call_groups is not None else self._default_groups
221
+
222
+ # Merge properties: default properties + call properties (call properties override)
223
+ properties = dict(self._default_properties)
224
+
225
+ if call_properties:
226
+ properties.update(call_properties)
227
+
228
+ if call_trace_id is None:
229
+ call_trace_id = str(uuid.uuid4())
230
+
231
+ return distinct_id, call_trace_id, properties, privacy_mode, groups
232
+
233
+ async def generate_content(
234
+ self,
235
+ model: str,
236
+ contents,
237
+ posthog_distinct_id: Optional[str] = None,
238
+ posthog_trace_id: Optional[str] = None,
239
+ posthog_properties: Optional[Dict[str, Any]] = None,
240
+ posthog_privacy_mode: Optional[bool] = None,
241
+ posthog_groups: Optional[Dict[str, Any]] = None,
242
+ **kwargs: Any,
243
+ ):
244
+ """
245
+ Generate content using Gemini's API while tracking usage in PostHog.
246
+
247
+ This method signature exactly matches genai.Client().aio.models.generate_content()
248
+ with additional PostHog tracking parameters.
249
+
250
+ Args:
251
+ model: The model to use (e.g., 'gemini-2.0-flash')
252
+ contents: The input content for generation
253
+ posthog_distinct_id: ID to associate with the usage event (overrides client default)
254
+ posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
255
+ posthog_properties: Extra properties to include in the event (merged with client defaults)
256
+ posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
257
+ posthog_groups: Group analytics properties (overrides client default)
258
+ **kwargs: Arguments passed to Gemini's generate_content
259
+ """
260
+
261
+ # Merge PostHog parameters
262
+ distinct_id, trace_id, properties, privacy_mode, groups = (
263
+ self._merge_posthog_params(
264
+ posthog_distinct_id,
265
+ posthog_trace_id,
266
+ posthog_properties,
267
+ posthog_privacy_mode,
268
+ posthog_groups,
269
+ )
270
+ )
271
+
272
+ kwargs_with_contents = {"model": model, "contents": contents, **kwargs}
273
+
274
+ return await call_llm_and_track_usage_async(
275
+ distinct_id,
276
+ self._ph_client,
277
+ "gemini",
278
+ trace_id,
279
+ properties,
280
+ privacy_mode,
281
+ groups,
282
+ self._base_url,
283
+ self._client.aio.models.generate_content,
284
+ **kwargs_with_contents,
285
+ )
286
+
287
+ async def _generate_content_streaming(
288
+ self,
289
+ model: str,
290
+ contents,
291
+ distinct_id: Optional[str],
292
+ trace_id: Optional[str],
293
+ properties: Optional[Dict[str, Any]],
294
+ privacy_mode: bool,
295
+ groups: Optional[Dict[str, Any]],
296
+ **kwargs: Any,
297
+ ):
298
+ start_time = time.time()
299
+ usage_stats: TokenUsage = TokenUsage(input_tokens=0, output_tokens=0)
300
+ accumulated_content = []
301
+
302
+ kwargs_without_stream = {"model": model, "contents": contents, **kwargs}
303
+ response = await self._client.aio.models.generate_content_stream(
304
+ **kwargs_without_stream
305
+ )
306
+
307
+ async def async_generator():
308
+ nonlocal usage_stats
309
+ nonlocal accumulated_content
310
+
311
+ try:
312
+ async for chunk in response:
313
+ # Extract usage stats from chunk
314
+ chunk_usage = extract_gemini_usage_from_chunk(chunk)
315
+
316
+ if chunk_usage:
317
+ # Gemini reports cumulative totals, not incremental values
318
+ merge_usage_stats(usage_stats, chunk_usage, mode="cumulative")
319
+
320
+ # Extract content from chunk (now returns content blocks)
321
+ content_block = extract_gemini_content_from_chunk(chunk)
322
+
323
+ if content_block is not None:
324
+ accumulated_content.append(content_block)
325
+
326
+ yield chunk
327
+
328
+ finally:
329
+ end_time = time.time()
330
+ latency = end_time - start_time
331
+
332
+ self._capture_streaming_event(
333
+ model,
334
+ contents,
335
+ distinct_id,
336
+ trace_id,
337
+ properties,
338
+ privacy_mode,
339
+ groups,
340
+ kwargs,
341
+ usage_stats,
342
+ latency,
343
+ accumulated_content,
344
+ )
345
+
346
+ return async_generator()
347
+
348
+ def _capture_streaming_event(
349
+ self,
350
+ model: str,
351
+ contents,
352
+ distinct_id: Optional[str],
353
+ trace_id: Optional[str],
354
+ properties: Optional[Dict[str, Any]],
355
+ privacy_mode: bool,
356
+ groups: Optional[Dict[str, Any]],
357
+ kwargs: Dict[str, Any],
358
+ usage_stats: TokenUsage,
359
+ latency: float,
360
+ output: Any,
361
+ ):
362
+ # Prepare standardized event data
363
+ formatted_input = self._format_input(contents, **kwargs)
364
+ sanitized_input = sanitize_gemini(formatted_input)
365
+
366
+ event_data = StreamingEventData(
367
+ provider="gemini",
368
+ model=model,
369
+ base_url=self._base_url,
370
+ kwargs=kwargs,
371
+ formatted_input=sanitized_input,
372
+ formatted_output=format_gemini_streaming_output(output),
373
+ usage_stats=usage_stats,
374
+ latency=latency,
375
+ distinct_id=distinct_id,
376
+ trace_id=trace_id,
377
+ properties=properties,
378
+ privacy_mode=privacy_mode,
379
+ groups=groups,
380
+ )
381
+
382
+ # Use the common capture function
383
+ capture_streaming_event(self._ph_client, event_data)
384
+
385
+ def _format_input(self, contents, **kwargs):
386
+ """Format input contents for PostHog tracking"""
387
+
388
+ # Create kwargs dict with contents for merge_system_prompt
389
+ input_kwargs = {"contents": contents, **kwargs}
390
+ return merge_system_prompt(input_kwargs, "gemini")
391
+
392
+ async def generate_content_stream(
393
+ self,
394
+ model: str,
395
+ contents,
396
+ posthog_distinct_id: Optional[str] = None,
397
+ posthog_trace_id: Optional[str] = None,
398
+ posthog_properties: Optional[Dict[str, Any]] = None,
399
+ posthog_privacy_mode: Optional[bool] = None,
400
+ posthog_groups: Optional[Dict[str, Any]] = None,
401
+ **kwargs: Any,
402
+ ):
403
+ # Merge PostHog parameters
404
+ distinct_id, trace_id, properties, privacy_mode, groups = (
405
+ self._merge_posthog_params(
406
+ posthog_distinct_id,
407
+ posthog_trace_id,
408
+ posthog_properties,
409
+ posthog_privacy_mode,
410
+ posthog_groups,
411
+ )
412
+ )
413
+
414
+ return await self._generate_content_streaming(
415
+ model,
416
+ contents,
417
+ distinct_id,
418
+ trace_id,
419
+ properties,
420
+ privacy_mode,
421
+ groups,
422
+ **kwargs,
423
+ )