Skip to content

Commit 1ab9e9d

Browse files
committed
fix(llma): add $ai_tools to streaming Gemini
1 parent b3a7f72 commit 1ab9e9d

File tree

11 files changed

+590
-244
lines changed

11 files changed

+590
-244
lines changed

posthog/ai/anthropic/anthropic.py

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@
1212

1313
from posthog.ai.utils import (
1414
call_llm_and_track_usage,
15-
extract_available_tool_calls,
16-
get_model_params,
17-
merge_system_prompt,
18-
with_privacy_mode,
1915
)
2016
from posthog.ai.anthropic.anthropic_converter import (
21-
format_anthropic_streaming_content,
2217
extract_anthropic_usage_from_event,
2318
handle_anthropic_content_block_start,
2419
handle_anthropic_text_delta,
@@ -215,66 +210,32 @@ def _capture_streaming_event(
215210
content_blocks: List[Dict[str, Any]],
216211
accumulated_content: str,
217212
):
218-
if posthog_trace_id is None:
219-
posthog_trace_id = str(uuid.uuid4())
220-
221-
# Format output using converter
222-
formatted_content = format_anthropic_streaming_content(content_blocks)
223-
formatted_output = []
224-
225-
if formatted_content:
226-
formatted_output = [{"role": "assistant", "content": formatted_content}]
227-
else:
228-
# Fallback to accumulated content if no blocks
229-
formatted_output = [
230-
{
231-
"role": "assistant",
232-
"content": [{"type": "text", "text": accumulated_content}],
233-
}
234-
]
235-
236-
event_properties = {
237-
"$ai_provider": "anthropic",
238-
"$ai_model": kwargs.get("model"),
239-
"$ai_model_parameters": get_model_params(kwargs),
240-
"$ai_input": with_privacy_mode(
241-
self._client._ph_client,
242-
posthog_privacy_mode,
243-
merge_system_prompt(kwargs, "anthropic"),
244-
),
245-
"$ai_output_choices": with_privacy_mode(
246-
self._client._ph_client,
247-
posthog_privacy_mode,
248-
formatted_output,
249-
),
250-
"$ai_http_status": 200,
251-
"$ai_input_tokens": usage_stats.get("input_tokens", 0),
252-
"$ai_output_tokens": usage_stats.get("output_tokens", 0),
253-
"$ai_cache_read_input_tokens": usage_stats.get(
254-
"cache_read_input_tokens", 0
255-
),
256-
"$ai_cache_creation_input_tokens": usage_stats.get(
257-
"cache_creation_input_tokens", 0
213+
from posthog.ai.types import StreamingEventData
214+
from posthog.ai.anthropic.anthropic_converter import (
215+
standardize_anthropic_usage,
216+
format_anthropic_streaming_input,
217+
format_anthropic_streaming_output_complete,
218+
)
219+
from posthog.ai.utils import capture_streaming_event
220+
221+
# Prepare standardized event data
222+
event_data = StreamingEventData(
223+
provider="anthropic",
224+
model=kwargs.get("model"),
225+
base_url=str(self._client.base_url),
226+
kwargs=kwargs,
227+
formatted_input=format_anthropic_streaming_input(kwargs),
228+
formatted_output=format_anthropic_streaming_output_complete(
229+
content_blocks, accumulated_content
258230
),
259-
"$ai_latency": latency,
260-
"$ai_trace_id": posthog_trace_id,
261-
"$ai_base_url": str(self._client.base_url),
262-
**(posthog_properties or {}),
263-
}
264-
265-
# Add tools if available
266-
available_tools = extract_available_tool_calls("anthropic", kwargs)
267-
268-
if available_tools:
269-
event_properties["$ai_tools"] = available_tools
270-
271-
if posthog_distinct_id is None:
272-
event_properties["$process_person_profile"] = False
273-
274-
if hasattr(self._client._ph_client, "capture"):
275-
self._client._ph_client.capture(
276-
distinct_id=posthog_distinct_id or posthog_trace_id,
277-
event="$ai_generation",
278-
properties=event_properties,
279-
groups=posthog_groups,
280-
)
231+
usage_stats=standardize_anthropic_usage(usage_stats),
232+
latency=latency,
233+
distinct_id=posthog_distinct_id,
234+
trace_id=posthog_trace_id,
235+
properties=posthog_properties,
236+
privacy_mode=posthog_privacy_mode,
237+
groups=posthog_groups,
238+
)
239+
240+
# Use the common capture function
241+
capture_streaming_event(self._client._ph_client, event_data)

posthog/ai/anthropic/anthropic_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ async def _create_streaming(
133133
content_blocks: List[Dict[str, Any]] = []
134134
tools_in_progress: Dict[str, Dict[str, Any]] = {}
135135
current_text_block: Optional[Dict[str, Any]] = None
136-
response = super().create(**kwargs)
136+
response = await super().create(**kwargs)
137137

138138
async def generator():
139139
nonlocal usage_stats

posthog/ai/anthropic/anthropic_converter.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
FormattedTextContent,
1616
StreamingContentBlock,
1717
StreamingUsageStats,
18+
TokenUsage,
1819
ToolInProgress,
1920
)
2021

@@ -320,3 +321,67 @@ def finalize_anthropic_tool_input(
320321
pass
321322

322323
del tools_in_progress[block["id"]]
324+
325+
326+
def standardize_anthropic_usage(usage: Dict[str, Any]) -> TokenUsage:
327+
"""
328+
Standardize Anthropic usage statistics to common TokenUsage format.
329+
330+
Anthropic already uses standard field names, so this mainly structures the data.
331+
332+
Args:
333+
usage: Raw usage statistics from Anthropic
334+
335+
Returns:
336+
Standardized TokenUsage dict
337+
"""
338+
return TokenUsage(
339+
input_tokens=usage.get("input_tokens", 0),
340+
output_tokens=usage.get("output_tokens", 0),
341+
cache_read_input_tokens=usage.get("cache_read_input_tokens"),
342+
cache_creation_input_tokens=usage.get("cache_creation_input_tokens"),
343+
)
344+
345+
346+
def format_anthropic_streaming_input(kwargs: Dict[str, Any]) -> Any:
347+
"""
348+
Format Anthropic streaming input using system prompt merging.
349+
350+
Args:
351+
kwargs: Keyword arguments passed to Anthropic API
352+
353+
Returns:
354+
Formatted input ready for PostHog tracking
355+
"""
356+
from posthog.ai.utils import merge_system_prompt
357+
358+
return merge_system_prompt(kwargs, "anthropic")
359+
360+
361+
def format_anthropic_streaming_output_complete(
362+
content_blocks: List[StreamingContentBlock], accumulated_content: str
363+
) -> List[FormattedMessage]:
364+
"""
365+
Format complete Anthropic streaming output.
366+
367+
Combines existing logic for formatting content blocks with fallback to accumulated content.
368+
369+
Args:
370+
content_blocks: List of content blocks accumulated during streaming
371+
accumulated_content: Raw accumulated text content as fallback
372+
373+
Returns:
374+
Formatted messages ready for PostHog tracking
375+
"""
376+
formatted_content = format_anthropic_streaming_content(content_blocks)
377+
378+
if formatted_content:
379+
return [{"role": "assistant", "content": formatted_content}]
380+
else:
381+
# Fallback to accumulated content if no blocks
382+
return [
383+
{
384+
"role": "assistant",
385+
"content": [{"type": "text", "text": accumulated_content}],
386+
}
387+
]

posthog/ai/gemini/gemini.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
from posthog import setup
1414
from posthog.ai.utils import (
1515
call_llm_and_track_usage,
16-
get_model_params,
17-
with_privacy_mode,
16+
capture_streaming_event,
1817
)
1918
from posthog.ai.gemini.gemini_converter import (
2019
format_gemini_input,
@@ -352,42 +351,28 @@ def _capture_streaming_event(
352351
latency: float,
353352
output: str,
354353
):
355-
if trace_id is None:
356-
trace_id = str(uuid.uuid4())
357-
358-
event_properties = {
359-
"$ai_provider": "gemini",
360-
"$ai_model": model,
361-
"$ai_model_parameters": get_model_params(kwargs),
362-
"$ai_input": with_privacy_mode(
363-
self._ph_client,
364-
privacy_mode,
365-
self._format_input(contents),
366-
),
367-
"$ai_output_choices": with_privacy_mode(
368-
self._ph_client,
369-
privacy_mode,
370-
format_gemini_streaming_output(output),
371-
),
372-
"$ai_http_status": 200,
373-
"$ai_input_tokens": usage_stats.get("input_tokens", 0),
374-
"$ai_output_tokens": usage_stats.get("output_tokens", 0),
375-
"$ai_latency": latency,
376-
"$ai_trace_id": trace_id,
377-
"$ai_base_url": self._base_url,
378-
**(properties or {}),
379-
}
380-
381-
if distinct_id is None:
382-
event_properties["$process_person_profile"] = False
383-
384-
if hasattr(self._ph_client, "capture"):
385-
self._ph_client.capture(
386-
distinct_id=distinct_id,
387-
event="$ai_generation",
388-
properties=event_properties,
389-
groups=groups,
390-
)
354+
from posthog.ai.types import StreamingEventData
355+
from posthog.ai.gemini.gemini_converter import standardize_gemini_usage
356+
357+
# Prepare standardized event data
358+
event_data = StreamingEventData(
359+
provider="gemini",
360+
model=model,
361+
base_url=self._base_url,
362+
kwargs=kwargs,
363+
formatted_input=self._format_input(contents),
364+
formatted_output=format_gemini_streaming_output(output),
365+
usage_stats=standardize_gemini_usage(usage_stats),
366+
latency=latency,
367+
distinct_id=distinct_id,
368+
trace_id=trace_id,
369+
properties=properties,
370+
privacy_mode=privacy_mode,
371+
groups=groups,
372+
)
373+
374+
# Use the common capture function
375+
capture_streaming_event(self._ph_client, event_data)
391376

392377
def _format_input(self, contents):
393378
"""Format input contents for PostHog tracking"""

posthog/ai/gemini/gemini_converter.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
FormattedMessage,
1414
FormattedTextContent,
1515
StreamingUsageStats,
16+
TokenUsage,
1617
)
1718

1819

@@ -344,3 +345,22 @@ def format_gemini_streaming_output(
344345
text = str(accumulated_content)
345346

346347
return [{"role": "assistant", "content": [{"type": "text", "text": text}]}]
348+
349+
350+
def standardize_gemini_usage(usage: Dict[str, Any]) -> TokenUsage:
351+
"""
352+
Standardize Gemini usage statistics to common TokenUsage format.
353+
354+
Gemini already uses standard field names (input_tokens/output_tokens).
355+
356+
Args:
357+
usage: Raw usage statistics from Gemini
358+
359+
Returns:
360+
Standardized TokenUsage dict
361+
"""
362+
return TokenUsage(
363+
input_tokens=usage.get("input_tokens", 0),
364+
output_tokens=usage.get("output_tokens", 0),
365+
# Gemini doesn't currently support cache or reasoning tokens
366+
)

0 commit comments

Comments
 (0)