Merge pull request #36397 from ousamabenyounes/litellm_fix_gemini_web_search_unique_queries_36377

fix(vertex_ai): bill Gemini grounding per unique web search query
This commit is contained in:
Mateo Wang 2026-08-26 16:02:07 -07:00 committed by GitHub
commit 855a8bc764
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 11 deletions

View file

@ -1979,16 +1979,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
@staticmethod
def _calculate_web_search_requests(grounding_metadata: list[dict]) -> int | None:
web_search_requests: int | None = None
if grounding_metadata and isinstance(grounding_metadata, list) and len(grounding_metadata) > 0:
for grounding_metadata_item in grounding_metadata:
web_search_queries = grounding_metadata_item.get("webSearchQueries")
if web_search_queries and web_search_requests:
web_search_requests += len([q for q in web_search_queries if q])
elif web_search_queries:
web_search_requests = len([q for q in web_search_queries if q])
return web_search_requests
if not (grounding_metadata and isinstance(grounding_metadata, list)):
return None
unique_queries: Final = {
query
for grounding_metadata_item in grounding_metadata
for query in (grounding_metadata_item.get("webSearchQueries") or [])
if query
}
return len(unique_queries) or None
@staticmethod
def _create_streaming_choice(

View file

@ -2,7 +2,7 @@ import asyncio
import json
import re
from copy import deepcopy
from typing import List, cast
from typing import Final, List, cast
from unittest.mock import MagicMock, patch
import pytest
@ -5579,3 +5579,25 @@ def test_accumulated_json_async_end_of_stream_drains_buffered_value():
result = asyncio.run(iterator.__anext__())
assert result is not None
assert result.choices[0].delta.content == "a"
def test_calculate_web_search_requests_counts_unique_queries():
"""Gemini 3 per_query billing charges per unique query executed, not per emitted string.
Regression for #36377: duplicate webSearchQueries within and across grounding
metadata items must collapse to the distinct-query count, and empty strings must
be ignored, matching Google's documented Grounding-with-Search billing rule.
"""
duplicates_in_one_item: Final = [
{"webSearchQueries": ["euro 2024 winner", "euro 2024 winner", "spain england final", ""]}
]
assert VertexGeminiConfig._calculate_web_search_requests(duplicates_in_one_item) == 2
duplicates_across_items: Final = [
{"webSearchQueries": ["euro 2024 winner"]},
{"webSearchQueries": ["euro 2024 winner", "spain england final"]},
]
assert VertexGeminiConfig._calculate_web_search_requests(duplicates_across_items) == 2
assert VertexGeminiConfig._calculate_web_search_requests([]) is None
assert VertexGeminiConfig._calculate_web_search_requests([{"webSearchQueries": ["", ""]}]) is None