Skip to content

Commit b921fe3

Browse files
feat: add Gemini embed_content tracking (#498)
* feat: add gemini embed_content tracking support * chore: add sampo changeset for gemini embed_content * chore: fix black formatting
1 parent 44b92a8 commit b921fe3

File tree

6 files changed

+708
-0
lines changed

6 files changed

+708
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
pypi/posthog: minor
3+
---
4+
5+
Add Gemini `embed_content` tracking support for both sync and async clients

posthog/ai/gemini/gemini.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
merge_usage_stats,
2121
)
2222
from posthog.ai.gemini.gemini_converter import (
23+
extract_gemini_embedding_token_count,
2324
extract_gemini_usage_from_chunk,
2425
extract_gemini_content_from_chunk,
2526
extract_gemini_stop_reason_from_chunk,
2627
format_gemini_streaming_output,
2728
)
29+
from posthog.ai.utils import with_privacy_mode
2830
from posthog.ai.sanitization import sanitize_gemini
2931
from posthog.client import Client as PostHogClient
3032

@@ -429,3 +431,88 @@ def generate_content_stream(
429431
groups,
430432
**kwargs,
431433
)
434+
435+
def embed_content(
436+
self,
437+
model: str,
438+
contents,
439+
posthog_distinct_id: Optional[str] = None,
440+
posthog_trace_id: Optional[str] = None,
441+
posthog_properties: Optional[Dict[str, Any]] = None,
442+
posthog_privacy_mode: Optional[bool] = None,
443+
posthog_groups: Optional[Dict[str, Any]] = None,
444+
**kwargs: Any,
445+
):
446+
"""
447+
Create embeddings using Gemini's API while tracking usage in PostHog.
448+
449+
Args:
450+
model: The model to use (e.g., 'gemini-embedding-001')
451+
contents: The input content for embedding
452+
posthog_distinct_id: ID to associate with the usage event (overrides client default)
453+
posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
454+
posthog_properties: Extra properties to include in the event (merged with client defaults)
455+
posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
456+
posthog_groups: Group analytics properties (overrides client default)
457+
**kwargs: Arguments passed to Gemini's embed_content (e.g., config)
458+
"""
459+
distinct_id, trace_id, properties, privacy_mode, groups = (
460+
self._merge_posthog_params(
461+
posthog_distinct_id,
462+
posthog_trace_id,
463+
posthog_properties,
464+
posthog_privacy_mode,
465+
posthog_groups,
466+
)
467+
)
468+
469+
start_time = time.time()
470+
response = None
471+
error = None
472+
http_status = 200
473+
474+
try:
475+
response = self._client.models.embed_content(
476+
model=model, contents=contents, **kwargs
477+
)
478+
except Exception as exc:
479+
error = exc
480+
http_status = getattr(exc, "status_code", 0)
481+
finally:
482+
end_time = time.time()
483+
latency = end_time - start_time
484+
485+
input_tokens = (
486+
extract_gemini_embedding_token_count(response) if response else 0
487+
)
488+
489+
event_properties = {
490+
"$ai_provider": "gemini",
491+
"$ai_model": model,
492+
"$ai_input": with_privacy_mode(self._ph_client, privacy_mode, contents),
493+
"$ai_http_status": http_status,
494+
"$ai_input_tokens": input_tokens,
495+
"$ai_latency": latency,
496+
"$ai_trace_id": trace_id,
497+
"$ai_base_url": self._base_url,
498+
**(properties or {}),
499+
}
500+
501+
if error:
502+
event_properties["$ai_is_error"] = True
503+
event_properties["$ai_error"] = str(error)
504+
505+
if distinct_id is None:
506+
event_properties["$process_person_profile"] = False
507+
508+
self._ph_client.capture(
509+
distinct_id=distinct_id or trace_id,
510+
event="$ai_embedding",
511+
properties=event_properties,
512+
groups=groups,
513+
)
514+
515+
if error:
516+
raise error
517+
518+
return response

posthog/ai/gemini/gemini_async.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020
merge_usage_stats,
2121
)
2222
from posthog.ai.gemini.gemini_converter import (
23+
extract_gemini_embedding_token_count,
2324
extract_gemini_usage_from_chunk,
2425
extract_gemini_content_from_chunk,
2526
extract_gemini_stop_reason_from_chunk,
2627
format_gemini_streaming_output,
2728
)
29+
from posthog.ai.utils import with_privacy_mode
2830
from posthog.ai.sanitization import sanitize_gemini
2931
from posthog.client import Client as PostHogClient
3032

@@ -432,3 +434,88 @@ async def generate_content_stream(
432434
groups,
433435
**kwargs,
434436
)
437+
438+
async def embed_content(
439+
self,
440+
model: str,
441+
contents,
442+
posthog_distinct_id: Optional[str] = None,
443+
posthog_trace_id: Optional[str] = None,
444+
posthog_properties: Optional[Dict[str, Any]] = None,
445+
posthog_privacy_mode: Optional[bool] = None,
446+
posthog_groups: Optional[Dict[str, Any]] = None,
447+
**kwargs: Any,
448+
):
449+
"""
450+
Create embeddings using Gemini's API while tracking usage in PostHog.
451+
452+
Args:
453+
model: The model to use (e.g., 'gemini-embedding-001')
454+
contents: The input content for embedding
455+
posthog_distinct_id: ID to associate with the usage event (overrides client default)
456+
posthog_trace_id: Trace UUID for linking events (auto-generated if not provided)
457+
posthog_properties: Extra properties to include in the event (merged with client defaults)
458+
posthog_privacy_mode: Whether to redact sensitive information (overrides client default)
459+
posthog_groups: Group analytics properties (overrides client default)
460+
**kwargs: Arguments passed to Gemini's embed_content (e.g., config)
461+
"""
462+
distinct_id, trace_id, properties, privacy_mode, groups = (
463+
self._merge_posthog_params(
464+
posthog_distinct_id,
465+
posthog_trace_id,
466+
posthog_properties,
467+
posthog_privacy_mode,
468+
posthog_groups,
469+
)
470+
)
471+
472+
start_time = time.time()
473+
response = None
474+
error = None
475+
http_status = 200
476+
477+
try:
478+
response = await self._client.aio.models.embed_content(
479+
model=model, contents=contents, **kwargs
480+
)
481+
except Exception as exc:
482+
error = exc
483+
http_status = getattr(exc, "status_code", 0)
484+
finally:
485+
end_time = time.time()
486+
latency = end_time - start_time
487+
488+
input_tokens = (
489+
extract_gemini_embedding_token_count(response) if response else 0
490+
)
491+
492+
event_properties = {
493+
"$ai_provider": "gemini",
494+
"$ai_model": model,
495+
"$ai_input": with_privacy_mode(self._ph_client, privacy_mode, contents),
496+
"$ai_http_status": http_status,
497+
"$ai_input_tokens": input_tokens,
498+
"$ai_latency": latency,
499+
"$ai_trace_id": trace_id,
500+
"$ai_base_url": self._base_url,
501+
**(properties or {}),
502+
}
503+
504+
if error:
505+
event_properties["$ai_is_error"] = True
506+
event_properties["$ai_error"] = str(error)
507+
508+
if distinct_id is None:
509+
event_properties["$process_person_profile"] = False
510+
511+
self._ph_client.capture(
512+
distinct_id=distinct_id or trace_id,
513+
event="$ai_embedding",
514+
properties=event_properties,
515+
groups=groups,
516+
)
517+
518+
if error:
519+
raise error
520+
521+
return response

posthog/ai/gemini/gemini_converter.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,19 @@ def format_gemini_streaming_output(
675675

676676
# Fallback for empty or unexpected input
677677
return [{"role": "assistant", "content": [{"type": "text", "text": ""}]}]
678+
679+
680+
def extract_gemini_embedding_token_count(response) -> int:
681+
"""
682+
Extract total token count from a Gemini embed_content response.
683+
Token counts are only available per-embedding via Vertex AI's statistics.token_count.
684+
Returns 0 if no token counts are available.
685+
"""
686+
total = 0
687+
if hasattr(response, "embeddings") and response.embeddings:
688+
for embedding in response.embeddings:
689+
if hasattr(embedding, "statistics") and embedding.statistics:
690+
token_count = getattr(embedding.statistics, "token_count", None)
691+
if token_count is not None:
692+
total += int(token_count)
693+
return total

0 commit comments

Comments
 (0)