From 4c520f30644e3d4ccba1b630d7cfa6208a30bd10 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 20 May 2026 12:54:47 +0200 Subject: [PATCH 1/2] fix: handle Gemini responses without candidates --- .sampo/changesets/doughty-warden-otso.md | 5 +++ posthog/ai/gemini/gemini_converter.py | 2 +- posthog/test/ai/gemini/test_gemini.py | 39 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .sampo/changesets/doughty-warden-otso.md diff --git a/.sampo/changesets/doughty-warden-otso.md b/.sampo/changesets/doughty-warden-otso.md new file mode 100644 index 00000000..7b13b9f5 --- /dev/null +++ b/.sampo/changesets/doughty-warden-otso.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Fix Gemini web search extraction when response candidates are null. diff --git a/posthog/ai/gemini/gemini_converter.py b/posthog/ai/gemini/gemini_converter.py index ae6255e9..efac3243 100644 --- a/posthog/ai/gemini/gemini_converter.py +++ b/posthog/ai/gemini/gemini_converter.py @@ -438,7 +438,7 @@ def extract_gemini_web_search_count(response: Any) -> int: """ # Check for grounding_metadata in candidates - if hasattr(response, "candidates"): + if hasattr(response, "candidates") and response.candidates: for candidate in response.candidates: if ( hasattr(candidate, "grounding_metadata") diff --git a/posthog/test/ai/gemini/test_gemini.py b/posthog/test/ai/gemini/test_gemini.py index 673f488a..fdf59a73 100644 --- a/posthog/test/ai/gemini/test_gemini.py +++ b/posthog/test/ai/gemini/test_gemini.py @@ -1170,6 +1170,45 @@ def test_empty_array_grounding_metadata_no_web_search( assert props["$ai_output_tokens"] == 12 +def test_none_candidates_no_web_search(mock_client, mock_google_genai_client): + """Test that response with candidates=None does not crash web search extraction.""" + + mock_response = MagicMock() + + # Mock usage metadata + mock_usage = MagicMock() + mock_usage.prompt_token_count = 5 + mock_usage.candidates_token_count = 8 + mock_usage.cached_content_token_count = 0 + mock_usage.thoughts_token_count = 0 + mock_response.usage_metadata = mock_usage + + # candidates attribute exists but is None + mock_response.candidates = None + mock_response.text = "Hello!" + + mock_google_genai_client.models.generate_content.return_value = mock_response + + client = Client(api_key="test-key", posthog_client=mock_client) + + response = client.models.generate_content( + model="gemini-2.5-flash", + contents="Hi", + posthog_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"] + + # Should not crash and web search count should not be present + assert "$ai_web_search_count" not in props + assert props["$ai_input_tokens"] == 5 + assert props["$ai_output_tokens"] == 8 + + @pytest.fixture def mock_embed_content_response(): mock_response = MagicMock() From bca5e8643df40e8ecbf58b38e1cbacc65bfad1e2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 20 May 2026 13:06:56 +0200 Subject: [PATCH 2/2] test: cover falsy Gemini candidates --- posthog/test/ai/gemini/test_gemini.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/posthog/test/ai/gemini/test_gemini.py b/posthog/test/ai/gemini/test_gemini.py index fdf59a73..9bdb7831 100644 --- a/posthog/test/ai/gemini/test_gemini.py +++ b/posthog/test/ai/gemini/test_gemini.py @@ -1170,8 +1170,11 @@ def test_empty_array_grounding_metadata_no_web_search( assert props["$ai_output_tokens"] == 12 -def test_none_candidates_no_web_search(mock_client, mock_google_genai_client): - """Test that response with candidates=None does not crash web search extraction.""" +@pytest.mark.parametrize("candidates_value", [None, []]) +def test_falsy_candidates_no_web_search( + mock_client, mock_google_genai_client, candidates_value +): + """Test that response with falsy candidates does not crash web search extraction.""" mock_response = MagicMock() @@ -1183,8 +1186,8 @@ def test_none_candidates_no_web_search(mock_client, mock_google_genai_client): mock_usage.thoughts_token_count = 0 mock_response.usage_metadata = mock_usage - # candidates attribute exists but is None - mock_response.candidates = None + # candidates attribute exists but is falsy + mock_response.candidates = candidates_value mock_response.text = "Hello!" mock_google_genai_client.models.generate_content.return_value = mock_response