Files
zeekay 3ca982343e rebrand: remove all compat aliases, zero posthog refs
- Remove Posthog = Insights alias from __init__.py
- Remove PosthogContextMiddleware alias from django.py
- Remove PostHogTracingProcessor alias from processor.py
- Change $lib from "posthog-python" to "insights-python"
- Change ingestion URLs from posthog.com to insights.hanzo.ai
- Rename all posthog_* kwargs to insights_* across AI wrappers
- Rename __posthog_exception_captured to __insights_exception_captured
- Rename posthog_context_stack contextvar to insights_context_stack
- Rename posthog🎏 Redis prefix to insights🎏
- Rename $$_posthog_redacted_* sentinels to $$_insights_redacted_*
- Remove POSTHOG_MW_* Django settings fallback, X-POSTHOG-* headers
- Rename Prompts(posthog=) param to Prompts(client=)
- Update APP_ENDPOINT to us.insights.hanzo.ai
- Update all tests, examples, docs, mypy config
2026-03-13 20:29:13 -07:00

854 lines
28 KiB
Python

from unittest.mock import AsyncMock, MagicMock, patch
import pytest
try:
from google import genai as google_genai
from hanzo_insights.ai.gemini import AsyncClient
GEMINI_AVAILABLE = True
except ImportError:
GEMINI_AVAILABLE = False
pytestmark = [
pytest.mark.skipif(
not GEMINI_AVAILABLE, reason="Google Gemini package is not available"
),
pytest.mark.asyncio,
]
@pytest.fixture
def mock_client():
with patch("hanzo_insights.client.Client") as mock_client:
mock_client.privacy_mode = False
yield mock_client
@pytest.fixture
def mock_gemini_response():
mock_response = MagicMock()
mock_response.text = "Test response from Gemini"
mock_usage = MagicMock()
mock_usage.prompt_token_count = 20
mock_usage.candidates_token_count = 10
# Ensure cache and reasoning tokens are not present (not MagicMock)
mock_usage.cached_content_token_count = 0
mock_usage.thoughts_token_count = 0
mock_response.usage_metadata = mock_usage
mock_candidate = MagicMock()
mock_candidate.text = "Test response from Gemini"
mock_content = MagicMock()
mock_part = MagicMock()
mock_part.text = "Test response from Gemini"
mock_content.parts = [mock_part]
mock_candidate.content = mock_content
mock_response.candidates = [mock_candidate]
return mock_response
@pytest.fixture
def mock_google_genai_client():
"""Mock for the google-genai Client with async support"""
with patch.object(google_genai, "Client") as mock_client_class:
mock_client_instance = MagicMock()
mock_models = MagicMock()
mock_aio = MagicMock()
mock_aio_models = MagicMock()
mock_client_instance.models = mock_models
mock_client_instance.aio = mock_aio
mock_aio.models = mock_aio_models
mock_client_class.return_value = mock_client_instance
yield mock_client_instance
@pytest.fixture
def mock_gemini_response_with_function_calls():
mock_response = MagicMock()
# Mock usage metadata
mock_usage = MagicMock()
mock_usage.prompt_token_count = 25
mock_usage.candidates_token_count = 15
mock_usage.cached_content_token_count = 0
mock_usage.thoughts_token_count = 0
mock_response.usage_metadata = mock_usage
# Mock function call
mock_function_call = MagicMock()
mock_function_call.name = "get_current_weather"
mock_function_call.args = {"location": "San Francisco"}
# Mock text part 1
mock_text_part1 = MagicMock()
mock_text_part1.text = "I'll check the weather for you."
type(mock_text_part1).text = mock_text_part1.text
# Mock text part 2
mock_text_part2 = MagicMock()
mock_text_part2.text = " Let me look that up."
type(mock_text_part2).text = mock_text_part2.text
# Mock function call part
mock_function_part = MagicMock()
mock_function_part.function_call = mock_function_call
type(mock_function_part).function_call = mock_function_part.function_call
del mock_function_part.text
# Mock content with 2 text parts and 1 function call part
mock_content = MagicMock()
mock_content.parts = [mock_text_part1, mock_text_part2, mock_function_part]
# Mock candidate
mock_candidate = MagicMock()
mock_candidate.content = mock_content
mock_response.candidates = [mock_candidate]
return mock_response
async def test_async_client_basic_generation(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test the async Client/AsyncModels API structure"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Tell me a fun fact about hedgehogs"],
insights_distinct_id="test-id",
insights_properties={"foo": "bar"},
)
assert response == mock_gemini_response
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert call_args["distinct_id"] == "test-id"
assert call_args["event"] == "$ai_generation"
assert props["$ai_provider"] == "gemini"
assert props["$ai_model"] == "gemini-2.0-flash"
assert props["$ai_input_tokens"] == 20
assert props["$ai_output_tokens"] == 10
assert props["foo"] == "bar"
assert "$ai_trace_id" in props
assert props["$ai_latency"] > 0
async def test_async_client_streaming_with_generate_content_stream(
mock_client, mock_google_genai_client
):
"""Test the async generate_content_stream method"""
async def mock_streaming_response():
mock_chunk1 = MagicMock()
mock_chunk1.text = "Hello "
mock_usage1 = MagicMock()
mock_usage1.prompt_token_count = 10
mock_usage1.candidates_token_count = 5
mock_usage1.cached_content_token_count = 0
mock_usage1.thoughts_token_count = 0
mock_chunk1.usage_metadata = mock_usage1
yield mock_chunk1
mock_chunk2 = MagicMock()
mock_chunk2.text = "world!"
mock_usage2 = MagicMock()
mock_usage2.prompt_token_count = 10
mock_usage2.candidates_token_count = 10
mock_usage2.cached_content_token_count = 0
mock_usage2.thoughts_token_count = 0
mock_chunk2.usage_metadata = mock_usage2
yield mock_chunk2
# Mock the async generate_content_stream method
mock_google_genai_client.aio.models.generate_content_stream = AsyncMock(
return_value=mock_streaming_response()
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content_stream(
model="gemini-2.0-flash",
contents=["Write a short story"],
insights_distinct_id="test-id",
insights_properties={"feature": "streaming"},
)
chunks = []
async for chunk in response:
chunks.append(chunk)
assert len(chunks) == 2
assert chunks[0].text == "Hello "
assert chunks[1].text == "world!"
# Check that the streaming event was captured
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert call_args["distinct_id"] == "test-id"
assert call_args["event"] == "$ai_generation"
assert props["$ai_provider"] == "gemini"
assert props["$ai_model"] == "gemini-2.0-flash"
assert props["$ai_input_tokens"] == 10
assert props["$ai_output_tokens"] == 10
assert props["feature"] == "streaming"
assert isinstance(props["$ai_latency"], float)
async def test_async_client_streaming_with_tools(mock_client, mock_google_genai_client):
"""Test that tools are captured in async streaming mode"""
async def mock_streaming_response():
mock_chunk1 = MagicMock()
mock_chunk1.text = "I'll check "
mock_usage1 = MagicMock()
mock_usage1.prompt_token_count = 15
mock_usage1.candidates_token_count = 5
mock_usage1.cached_content_token_count = 0
mock_usage1.thoughts_token_count = 0
mock_chunk1.usage_metadata = mock_usage1
yield mock_chunk1
mock_chunk2 = MagicMock()
mock_chunk2.text = "the weather"
mock_usage2 = MagicMock()
mock_usage2.prompt_token_count = 15
mock_usage2.candidates_token_count = 10
mock_usage2.cached_content_token_count = 0
mock_usage2.thoughts_token_count = 0
mock_chunk2.usage_metadata = mock_usage2
yield mock_chunk2
# Mock the async generate_content_stream method
mock_google_genai_client.aio.models.generate_content_stream = AsyncMock(
return_value=mock_streaming_response()
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
# Create mock tools configuration
mock_tool = MagicMock()
mock_tool.function_declarations = [
MagicMock(
name="get_current_weather",
description="Gets the current weather for a given location.",
parameters=MagicMock(
type="OBJECT",
properties={
"location": MagicMock(
type="STRING",
description="The city and state, e.g. San Francisco, CA",
)
},
required=["location"],
),
)
]
mock_config = MagicMock()
mock_config.tools = [mock_tool]
response = await client.models.generate_content_stream(
model="gemini-2.0-flash",
contents=["What's the weather in SF?"],
config=mock_config,
insights_distinct_id="test-id",
insights_properties={"feature": "streaming_with_tools"},
)
chunks = []
async for chunk in response:
chunks.append(chunk)
assert len(chunks) == 2
assert chunks[0].text == "I'll check "
assert chunks[1].text == "the weather"
# Check that the streaming event was captured with tools
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert call_args["distinct_id"] == "test-id"
assert call_args["event"] == "$ai_generation"
assert props["$ai_provider"] == "gemini"
assert props["$ai_model"] == "gemini-2.0-flash"
assert props["$ai_input_tokens"] == 15
assert props["$ai_output_tokens"] == 10
assert props["feature"] == "streaming_with_tools"
assert isinstance(props["$ai_latency"], float)
# Verify that tools are captured in the $ai_tools property in streaming mode
assert props["$ai_tools"] == [mock_tool]
async def test_async_client_groups(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test groups functionality with async Client API"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Hello"],
insights_distinct_id="test-id",
insights_groups={"company": "company_123"},
)
call_args = mock_client.capture.call_args[1]
assert call_args["groups"] == {"company": "company_123"}
async def test_async_client_privacy_mode_local(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test local privacy mode with async Client API"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Hello"],
insights_distinct_id="test-id",
insights_privacy_mode=True,
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] is None
assert props["$ai_output_choices"] is None
async def test_async_client_privacy_mode_global(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test global privacy mode with async Client API"""
mock_client.privacy_mode = True
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Hello"],
insights_distinct_id="test-id",
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] is None
assert props["$ai_output_choices"] is None
async def test_async_client_different_input_formats(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test different input formats with async Client API"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
# Test string input
await client.models.generate_content(
model="gemini-2.0-flash", contents="Hello", insights_distinct_id="test-id"
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] == [{"role": "user", "content": "Hello"}]
# Test Gemini-specific format with parts array
mock_client.reset_mock()
await client.models.generate_content(
model="gemini-2.0-flash",
contents=[{"role": "user", "parts": [{"text": "hey"}]}],
insights_distinct_id="test-id",
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] == [
{"role": "user", "content": [{"type": "text", "text": "hey"}]}
]
# Test multiple parts in the parts array
mock_client.reset_mock()
await client.models.generate_content(
model="gemini-2.0-flash",
contents=[{"role": "user", "parts": [{"text": "Hello "}, {"text": "world"}]}],
insights_distinct_id="test-id",
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] == [
{
"role": "user",
"content": [
{"type": "text", "text": "Hello "},
{"type": "text", "text": "world"},
],
}
]
# Test list input with string
mock_client.capture.reset_mock()
await client.models.generate_content(
model="gemini-2.0-flash", contents=["List item"], insights_distinct_id="test-id"
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_input"] == [{"role": "user", "content": "List item"}]
async def test_async_client_model_parameters(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test model parameters with async Client API"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Hello"],
insights_distinct_id="test-id",
temperature=0.7,
max_tokens=100,
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert props["$ai_model_parameters"]["temperature"] == 0.7
assert props["$ai_model_parameters"]["max_tokens"] == 100
async def test_async_client_default_settings(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test async client with default Insights settings"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(
api_key="test-key",
insights_client=mock_client,
insights_distinct_id="default_user",
insights_properties={"team": "ai"},
insights_privacy_mode=False,
insights_groups={"company": "acme_corp"},
)
# Call without overriding defaults
await client.models.generate_content(model="gemini-2.0-flash", contents=["Hello"])
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert call_args["distinct_id"] == "default_user"
assert call_args["groups"] == {"company": "acme_corp"}
assert props["team"] == "ai"
async def test_async_client_override_defaults(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test overriding async client defaults per call"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
client = AsyncClient(
api_key="test-key",
insights_client=mock_client,
insights_distinct_id="default_user",
insights_properties={"team": "ai"},
insights_privacy_mode=False,
insights_groups={"company": "acme_corp"},
)
# Override defaults in call
await client.models.generate_content(
model="gemini-2.0-flash",
contents=["Hello"],
insights_distinct_id="specific_user",
insights_properties={"feature": "chat", "urgent": True},
insights_privacy_mode=True,
insights_groups={"organization": "special_org"},
)
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
# Check overrides
assert call_args["distinct_id"] == "specific_user"
assert call_args["groups"] == {"organization": "special_org"}
assert props["$ai_input"] is None # privacy mode was overridden
# Check merged properties (defaults + call-specific)
assert props["team"] == "ai" # from defaults
assert props["feature"] == "chat" # from call
assert props["urgent"] is True # from call
async def test_async_vertex_ai_parameters_passed_through(
mock_client, mock_google_genai_client, mock_gemini_response
):
"""Test that Vertex AI parameters are properly passed to genai.Client"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response
)
# Mock credentials object
mock_credentials = MagicMock()
mock_debug_config = MagicMock()
mock_http_options = MagicMock()
# Create client with Vertex AI parameters
AsyncClient(
vertexai=True,
credentials=mock_credentials,
project="test-project",
location="us-central1",
debug_config=mock_debug_config,
http_options=mock_http_options,
insights_client=mock_client,
)
# Verify genai.Client was called with correct parameters
google_genai.Client.assert_called_once_with(
vertexai=True,
credentials=mock_credentials,
project="test-project",
location="us-central1",
debug_config=mock_debug_config,
http_options=mock_http_options,
)
async def test_async_api_key_mode(mock_client, mock_google_genai_client):
"""Test API key authentication mode with async client"""
# Create async client with just API key (traditional mode)
AsyncClient(
api_key="test-api-key",
insights_client=mock_client,
)
# Verify genai.Client was called with only api_key
google_genai.Client.assert_called_once_with(api_key="test-api-key")
async def test_async_function_calls_in_output_choices(
mock_client, mock_google_genai_client, mock_gemini_response_with_function_calls
):
"""Test that function calls are properly included in $ai_output_choices with async"""
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_gemini_response_with_function_calls
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content(
model="gemini-2.5-flash",
contents=["What's the weather in San Francisco?"],
insights_distinct_id="test-id",
)
assert response == mock_gemini_response_with_function_calls
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
assert call_args["distinct_id"] == "test-id"
assert call_args["event"] == "$ai_generation"
assert props["$ai_provider"] == "gemini"
assert props["$ai_model"] == "gemini-2.5-flash"
assert props["$ai_output_choices"] == [
{
"role": "assistant",
"content": [
{"type": "text", "text": "I'll check the weather for you."},
{"type": "text", "text": " Let me look that up."},
{
"type": "function",
"function": {
"name": "get_current_weather",
"arguments": {"location": "San Francisco"},
},
},
],
}
]
# Check token usage
assert props["$ai_input_tokens"] == 25
assert props["$ai_output_tokens"] == 15
assert props["$ai_http_status"] == 200
async def test_async_cache_and_reasoning_tokens(mock_client, mock_google_genai_client):
"""Test that cache and reasoning tokens are properly extracted with async"""
# Create a mock response with cache and reasoning tokens
mock_response = MagicMock()
mock_response.text = "Test response with cache"
mock_usage = MagicMock()
mock_usage.prompt_token_count = 100
mock_usage.candidates_token_count = 50
mock_usage.cached_content_token_count = 30 # Cache tokens
mock_usage.thoughts_token_count = 10 # Reasoning tokens
mock_response.usage_metadata = mock_usage
# Mock candidates
mock_candidate = MagicMock()
mock_candidate.text = "Test response with cache"
mock_response.candidates = [mock_candidate]
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content(
model="gemini-2.5-pro",
contents="Test with cache",
insights_distinct_id="test-id",
)
assert response == mock_response
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
# Check that all token types are present
assert props["$ai_input_tokens"] == 100
assert props["$ai_output_tokens"] == 50
assert props["$ai_cache_read_input_tokens"] == 30
assert props["$ai_reasoning_tokens"] == 10
async def test_async_streaming_cache_and_reasoning_tokens(
mock_client, mock_google_genai_client
):
"""Test that cache and reasoning tokens are properly extracted in async streaming"""
async def mock_streaming_response():
# Create mock chunks with cache and reasoning tokens
chunk1 = MagicMock()
chunk1.text = "Hello "
chunk1_usage = MagicMock()
chunk1_usage.prompt_token_count = 100
chunk1_usage.candidates_token_count = 5
chunk1_usage.cached_content_token_count = 30 # Cache tokens
chunk1_usage.thoughts_token_count = 0
chunk1.usage_metadata = chunk1_usage
yield chunk1
chunk2 = MagicMock()
chunk2.text = "world!"
chunk2_usage = MagicMock()
chunk2_usage.prompt_token_count = 100
chunk2_usage.candidates_token_count = 10
chunk2_usage.cached_content_token_count = 30 # Same cache tokens
chunk2_usage.thoughts_token_count = 5 # Reasoning tokens
chunk2.usage_metadata = chunk2_usage
yield chunk2
mock_google_genai_client.aio.models.generate_content_stream = AsyncMock(
return_value=mock_streaming_response()
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content_stream(
model="gemini-2.5-pro",
contents="Test streaming with cache",
insights_distinct_id="test-id",
)
# Consume the stream
result = []
async for chunk in response:
result.append(chunk)
assert len(result) == 2
# Check Insights capture was called
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
# Check that all token types are present (should use final chunk's usage)
assert props["$ai_input_tokens"] == 100
assert props["$ai_output_tokens"] == 10
assert props["$ai_cache_read_input_tokens"] == 30
assert props["$ai_reasoning_tokens"] == 5
async def test_async_web_search_grounding(mock_client, mock_google_genai_client):
"""Test async web search detection via grounding_metadata."""
# Create mock response with grounding metadata
mock_response = MagicMock()
# Mock usage metadata
mock_usage = MagicMock()
mock_usage.prompt_token_count = 60
mock_usage.candidates_token_count = 40
mock_usage.cached_content_token_count = 0
mock_usage.thoughts_token_count = 0
mock_response.usage_metadata = mock_usage
# Mock grounding metadata
mock_grounding_chunk = MagicMock()
mock_grounding_chunk.uri = "https://example.com"
mock_grounding_metadata = MagicMock()
mock_grounding_metadata.grounding_chunks = [mock_grounding_chunk]
# Mock text part
mock_text_part = MagicMock()
mock_text_part.text = "According to search results..."
type(mock_text_part).text = mock_text_part.text
# Mock content with parts
mock_content = MagicMock()
mock_content.parts = [mock_text_part]
# Mock candidate with grounding metadata
mock_candidate = MagicMock()
mock_candidate.content = mock_content
mock_candidate.grounding_metadata = mock_grounding_metadata
type(mock_candidate).grounding_metadata = mock_candidate.grounding_metadata
mock_response.candidates = [mock_candidate]
mock_response.text = "According to search results..."
# Mock the async generate_content method
mock_google_genai_client.aio.models.generate_content = AsyncMock(
return_value=mock_response
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content(
model="gemini-2.5-flash",
contents="What's the latest news?",
insights_distinct_id="test-id",
)
assert response == mock_response
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
# Verify web search count is detected (binary for grounding)
assert props["$ai_web_search_count"] == 1
assert props["$ai_input_tokens"] == 60
assert props["$ai_output_tokens"] == 40
async def test_async_streaming_with_web_search(mock_client, mock_google_genai_client):
"""Test that web search count is properly captured in async streaming mode."""
async def mock_streaming_response():
# Create chunk 1 with grounding metadata
mock_chunk1 = MagicMock()
mock_chunk1.text = "According to "
mock_usage1 = MagicMock()
mock_usage1.prompt_token_count = 30
mock_usage1.candidates_token_count = 5
mock_usage1.cached_content_token_count = 0
mock_usage1.thoughts_token_count = 0
mock_chunk1.usage_metadata = mock_usage1
# Add grounding metadata to first chunk
mock_grounding_chunk = MagicMock()
mock_grounding_chunk.uri = "https://example.com"
mock_grounding_metadata = MagicMock()
mock_grounding_metadata.grounding_chunks = [mock_grounding_chunk]
mock_candidate1 = MagicMock()
mock_candidate1.grounding_metadata = mock_grounding_metadata
type(mock_candidate1).grounding_metadata = mock_candidate1.grounding_metadata
mock_chunk1.candidates = [mock_candidate1]
yield mock_chunk1
# Create chunk 2
mock_chunk2 = MagicMock()
mock_chunk2.text = "search results..."
mock_usage2 = MagicMock()
mock_usage2.prompt_token_count = 30
mock_usage2.candidates_token_count = 15
mock_usage2.cached_content_token_count = 0
mock_usage2.thoughts_token_count = 0
mock_chunk2.usage_metadata = mock_usage2
mock_candidate2 = MagicMock()
mock_chunk2.candidates = [mock_candidate2]
yield mock_chunk2
# Mock the async generate_content_stream method
mock_google_genai_client.aio.models.generate_content_stream = AsyncMock(
return_value=mock_streaming_response()
)
client = AsyncClient(api_key="test-key", insights_client=mock_client)
response = await client.models.generate_content_stream(
model="gemini-2.5-flash",
contents="What's the latest news?",
insights_distinct_id="test-id",
)
chunks = []
async for chunk in response:
chunks.append(chunk)
assert len(chunks) == 2
assert mock_client.capture.call_count == 1
call_args = mock_client.capture.call_args[1]
props = call_args["properties"]
# Verify web search count is detected (binary for grounding)
assert props["$ai_web_search_count"] == 1
assert props["$ai_input_tokens"] == 30
assert props["$ai_output_tokens"] == 15