Merge pull request #38418 from BerriAI/litellm_gemini_maps_grounding_cost

fix(gemini): bill Google Maps grounding as its own SKU
This commit is contained in:
Mateo Wang 2026-08-26 16:47:05 -07:00 committed by GitHub
commit ad00d90b99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 889 additions and 126 deletions

View file

@ -105,13 +105,13 @@
"limit": 109
},
"reportUnknownMemberType": {
"limit": 38808
"limit": 38804
},
"reportUnknownParameterType": {
"limit": 19829
},
"reportUnknownVariableType": {
"limit": 30356
"limit": 30355
},
"reportUnnecessaryCast": {
"limit": 117

View file

@ -157,6 +157,9 @@ COST_DESCRIPTIONS: dict[str, str] = {
"input_cost_per_token": "USD per prompt token.",
"output_cost_per_token": "USD per generated token.",
"output_cost_per_reasoning_token": "USD per reasoning/thinking token, when billed separately.",
"google_maps_grounding_cost_per_query": (
"USD per Grounding with Google Maps request; billed per query or per prompt per web_search_billing_unit."
),
"cache_creation_input_token_cost": "USD per token written to the provider's prompt cache.",
"cache_read_input_token_cost": "USD per prompt token served from the provider's prompt cache.",
"input_cost_per_token_batches": "USD per prompt token via the provider's batch API.",

View file

@ -64,11 +64,17 @@ class StandardBuiltInToolCostTracking:
"""
standard_built_in_tools_params = standard_built_in_tools_params or {}
google_maps_grounding_cost: Final = StandardBuiltInToolCostTracking._handle_google_maps_grounding_cost(
model=model,
custom_llm_provider=custom_llm_provider,
usage=usage,
)
# Handle web search
if StandardBuiltInToolCostTracking.response_object_includes_web_search_call(
response_object=response_object, usage=usage
):
return StandardBuiltInToolCostTracking._handle_web_search_cost(
return google_maps_grounding_cost + StandardBuiltInToolCostTracking._handle_web_search_cost(
model=model,
custom_llm_provider=custom_llm_provider,
usage=usage,
@ -78,19 +84,56 @@ class StandardBuiltInToolCostTracking:
# Handle file search
if StandardBuiltInToolCostTracking.response_object_includes_file_search_call(response_object=response_object):
return StandardBuiltInToolCostTracking._handle_file_search_cost(
return google_maps_grounding_cost + StandardBuiltInToolCostTracking._handle_file_search_cost(
model=model,
custom_llm_provider=custom_llm_provider,
standard_built_in_tools_params=standard_built_in_tools_params,
)
# Handle Azure assistant features
return StandardBuiltInToolCostTracking._handle_azure_assistant_costs(
return google_maps_grounding_cost + StandardBuiltInToolCostTracking._handle_azure_assistant_costs(
model=model,
custom_llm_provider=custom_llm_provider,
standard_built_in_tools_params=standard_built_in_tools_params,
)
@staticmethod
def _resolve_model_info(model: str, custom_llm_provider: str | None) -> tuple[ModelInfo | None, str | None]:
direct: Final = StandardBuiltInToolCostTracking._safe_get_model_info(
model=model, custom_llm_provider=custom_llm_provider
)
if direct is not None:
return direct, custom_llm_provider or direct["litellm_provider"]
if "/" not in model:
return None, custom_llm_provider
by_prefix: Final = StandardBuiltInToolCostTracking._safe_get_model_info(model=model)
if by_prefix is None:
return None, custom_llm_provider
return by_prefix, by_prefix["litellm_provider"]
@staticmethod
def _handle_google_maps_grounding_cost(
model: str,
custom_llm_provider: str | None,
usage: Usage | None,
) -> float:
from litellm.llms import get_cost_for_google_maps_grounding_request
from litellm.llms.gemini.cost_calculator import google_maps_grounding_requests
if usage is None or google_maps_grounding_requests(usage) is None:
return 0.0
model_info, resolved_provider = StandardBuiltInToolCostTracking._resolve_model_info(
model=model, custom_llm_provider=custom_llm_provider
)
if model_info is None or resolved_provider is None:
return 0.0
return (
get_cost_for_google_maps_grounding_request(
custom_llm_provider=resolved_provider, usage=usage, model_info=model_info
)
or 0.0
)
@staticmethod
def _handle_web_search_cost(
model: str,
@ -102,29 +145,21 @@ class StandardBuiltInToolCostTracking:
"""Handle web search cost calculation."""
from litellm.llms import get_cost_for_web_search_request
model_info = StandardBuiltInToolCostTracking._safe_get_model_info(
# A provider-prefixed model (e.g. gemini/gemini-3.1-flash-lite) may not map under the
# request's custom_llm_provider. _resolve_model_info re-resolves from the prefix and adopts
# that provider so the cost is routed and priced with the model_info that was actually
# resolved, instead of feeding a re-resolved model into the original provider's calculator.
model_info, resolved_provider = StandardBuiltInToolCostTracking._resolve_model_info(
model=model, custom_llm_provider=custom_llm_provider
)
# A provider-prefixed model (e.g. gemini/gemini-3.1-flash-lite) may not map under the
# request's custom_llm_provider. Re-resolve from the prefix and adopt that provider so the
# cost is routed and priced with the model_info that was actually resolved, instead of
# feeding a re-resolved model into the original provider's calculator.
if model_info is None and "/" in model:
model_info = StandardBuiltInToolCostTracking._safe_get_model_info(model=model)
if model_info is not None:
custom_llm_provider = model_info["litellm_provider"]
if custom_llm_provider is None and model_info is not None:
custom_llm_provider = model_info["litellm_provider"]
resolved_usage: Final = StandardBuiltInToolCostTracking._usage_with_anthropic_web_search(
usage=usage, response_object=response_object
)
if model_info is not None and resolved_usage is not None and custom_llm_provider is not None:
if model_info is not None and resolved_usage is not None and resolved_provider is not None:
result: Final = get_cost_for_web_search_request(
custom_llm_provider=custom_llm_provider,
custom_llm_provider=resolved_provider,
usage=resolved_usage,
model_info=model_info,
)

View file

@ -28,6 +28,7 @@ PTU_ZEROED_PRICING_FIELDS: Final = tuple(f for f in MirroredPricingParams.model_
"cache_creation_input_token_cost_above_1hr",
"cache_creation_input_token_cost_above_200k_tokens",
"cache_read_input_token_cost_above_200k_tokens",
"google_maps_grounding_cost_per_query",
)
# tiered_pricing is emptied rather than zeroed: its tiers outrank the zeros written beside
# them, so a zero here would leave the cost map's tiers billing the traffic the reserved

View file

@ -173,6 +173,27 @@ def attach_cache_creation_token_details(
return prompt_tokens_details.model_copy(update={"cache_creation_token_details": cache_creation_token_details})
def apply_grounding_request_counts(
prompt_tokens_details: PromptTokensDetailsWrapper | None,
web_search_requests: int | None,
google_maps_grounding_requests: int | None,
) -> PromptTokensDetailsWrapper | None:
updates: Final = MappingProxyType(
{
field: value
for field, value in (
("web_search_requests", web_search_requests),
("google_maps_grounding_requests", google_maps_grounding_requests),
)
if value is not None
}
)
if not updates:
return prompt_tokens_details
counted: Final = prompt_tokens_details if prompt_tokens_details is not None else PromptTokensDetailsWrapper()
return counted.model_copy(update=updates)
class ChunkProcessor:
def __init__(self, chunks: list, messages: list | None = None):
self.chunks = self._sort_chunks(chunks)
@ -778,6 +799,7 @@ class ChunkProcessor:
server_tool_use: ServerToolUse | None = None
web_search_requests: int | None = None
google_maps_grounding_requests: int | None = None
completion_tokens_details: CompletionTokensDetails | None = None
prompt_tokens_details: PromptTokensDetailsWrapper | None = None
# Anthropic emits the cache-creation TTL breakdown (5m/1h split) only on
@ -827,6 +849,13 @@ class ChunkProcessor:
)
if chunk_web_search_requests is not None:
web_search_requests = chunk_web_search_requests
chunk_google_maps_grounding_requests: int | None = getattr(
usage_chunk_dict["prompt_tokens_details"],
"google_maps_grounding_requests",
None,
)
if chunk_google_maps_grounding_requests is not None:
google_maps_grounding_requests = chunk_google_maps_grounding_requests
prompt_tokens_details = usage_chunk_dict["prompt_tokens_details"] or prompt_tokens_details
@ -852,6 +881,7 @@ class ChunkProcessor:
cache_read_input_tokens=cache_read_input_tokens,
server_tool_use=server_tool_use,
web_search_requests=web_search_requests,
google_maps_grounding_requests=google_maps_grounding_requests,
completion_tokens_details=completion_tokens_details,
prompt_tokens_details=prompt_tokens_details,
cost=cost,
@ -939,6 +969,7 @@ class ChunkProcessor:
server_tool_use: Final[ServerToolUse | None] = calculated_usage_per_chunk["server_tool_use"]
web_search_requests: Final[int | None] = calculated_usage_per_chunk["web_search_requests"]
google_maps_grounding_requests: Final[int | None] = calculated_usage_per_chunk["google_maps_grounding_requests"]
completion_tokens_details: Final[CompletionTokensDetails | None] = calculated_usage_per_chunk[
"completion_tokens_details"
]
@ -998,13 +1029,11 @@ class ChunkProcessor:
if server_tool_use is not None:
returned_usage.server_tool_use = server_tool_use
if web_search_requests is not None:
if returned_usage.prompt_tokens_details is None:
returned_usage.prompt_tokens_details = PromptTokensDetailsWrapper(
web_search_requests=web_search_requests
)
else:
returned_usage.prompt_tokens_details.web_search_requests = web_search_requests
returned_usage.prompt_tokens_details = apply_grounding_request_counts(
returned_usage.prompt_tokens_details,
web_search_requests,
google_maps_grounding_requests,
)
if cost is not None:
setattr(returned_usage, "cost", cost)

View file

@ -14,6 +14,21 @@ if TYPE_CHECKING:
from litellm.types.utils import ModelInfo, Usage
def get_cost_for_google_maps_grounding_request(
custom_llm_provider: str, usage: "Usage", model_info: "ModelInfo"
) -> float | None:
"""
Get the cost of Grounding with Google Maps for a given model. Only Gemini models on the
Gemini API and Vertex AI can populate the Maps grounding counter, so every other provider
returns None.
"""
if custom_llm_provider != "gemini" and not custom_llm_provider.startswith("vertex_ai"):
return None
from .gemini.cost_calculator import cost_per_google_maps_grounding_request
return cost_per_google_maps_grounding_request(usage=usage, model_info=model_info)
def get_cost_for_web_search_request(custom_llm_provider: str, usage: "Usage", model_info: "ModelInfo") -> float | None:
"""
Get the cost for a web search request for a given model.

View file

@ -61,3 +61,42 @@ def cost_per_web_search_request(usage: "Usage", model_info: "ModelInfo") -> floa
number_of_web_search_requests = 1
return _cost * number_of_web_search_requests
GOOGLE_MAPS_GROUNDING_DEFAULT_COST_PER_QUERY: Final = 14e-3
GOOGLE_MAPS_GROUNDING_DEFAULT_COST_PER_PROMPT: Final = 25e-3
def google_maps_grounding_requests(usage: "Usage | None") -> int | None:
from litellm.types.utils import PromptTokensDetailsWrapper
details: Final = usage.prompt_tokens_details if usage is not None else None
if not isinstance(details, PromptTokensDetailsWrapper) or not hasattr(details, "google_maps_grounding_requests"):
return None
return details.google_maps_grounding_requests
def cost_per_google_maps_grounding_request(usage: "Usage", model_info: "ModelInfo") -> float:
"""
Calculates the cost of Grounding with Google Maps.
Billing follows ``web_search_billing_unit`` in model_info the same way Google Search grounding
does: ``"per_query"`` (Gemini 3.x) multiplies the executed Maps queries, ``"per_prompt"``
(default, Gemini 2.x) charges one flat fee per grounded prompt.
The rate comes from ``google_maps_grounding_cost_per_query`` in ``model_info``, falling back
to Google's list price for that billing unit when the pricing JSON has no entry yet.
"""
requests: Final = google_maps_grounding_requests(usage)
if not requests or requests <= 0:
return 0.0
billing_mode: Final = model_info.get("web_search_billing_unit") or "per_prompt"
default_cost: Final = (
GOOGLE_MAPS_GROUNDING_DEFAULT_COST_PER_QUERY
if billing_mode == "per_query"
else GOOGLE_MAPS_GROUNDING_DEFAULT_COST_PER_PROMPT
)
configured_cost: Final = model_info.get("google_maps_grounding_cost_per_query")
cost: Final = default_cost if configured_cost is None else configured_cost
billed_requests: Final = requests if billing_mode == "per_query" else 1
return cost * billed_requests

View file

@ -0,0 +1,56 @@
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Final
@dataclass(frozen=True, slots=True)
class GroundingRequests:
web_search_requests: int | None
google_maps_grounding_requests: int | None
def has_billable_grounding(self) -> bool:
return bool(self.web_search_requests or self.google_maps_grounding_requests)
def _chunk_kinds(item: Mapping[str, object]) -> frozenset[str]:
chunks: Final = item.get("groundingChunks")
if not isinstance(chunks, list):
return frozenset()
return frozenset(kind for chunk in chunks if isinstance(chunk, Mapping) for kind in chunk)
def _queries(item: Mapping[str, object]) -> frozenset[str]:
queries: Final = item.get("webSearchQueries")
if not isinstance(queries, list):
return frozenset()
return frozenset(query for query in queries if isinstance(query, str) and query)
def _is_maps_item(item: Mapping[str, object]) -> bool:
return "maps" in _chunk_kinds(item) or bool(item.get("googleMapsWidgetContextToken"))
def _attributes_queries_to_maps(item: Mapping[str, object]) -> bool:
return _is_maps_item(item) and "web" not in _chunk_kinds(item)
def calculate_grounding_requests(grounding_metadata: Sequence[Mapping[str, object]]) -> GroundingRequests:
"""Billable grounding requests across candidates, counting each distinct query once.
Duplicate queries within and across grounding metadata items collapse to the
distinct-query count (#36377), and empty strings are ignored. Maps grounding is
floored at one request whenever a candidate carries maps chunks or a widget token,
since per-prompt billing charges the prompt even when no query is reported.
"""
items: Final = tuple(item for item in grounding_metadata if isinstance(item, Mapping))
web_queries: Final = frozenset(
query for item in items if not _attributes_queries_to_maps(item) for query in _queries(item)
)
maps_queries: Final = frozenset(
query for item in items if _attributes_queries_to_maps(item) for query in _queries(item)
)
has_maps: Final = any(_is_maps_item(item) for item in items)
return GroundingRequests(
web_search_requests=len(web_queries) or None,
google_maps_grounding_requests=max(len(maps_queries), 1) if has_maps else None,
)

View file

@ -89,6 +89,7 @@ from ..common_utils import (
supports_response_json_schema,
)
from ..vertex_llm_base import VertexBase
from .grounding_requests import calculate_grounding_requests
from .transformation import (
_gemini_convert_messages_with_history,
async_transform_request_body,
@ -1717,14 +1718,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
completion_response: GenerateContentResponseBody | BidiGenerateContentServerMessage,
) -> bool:
"""
Whether the response used Grounding with Google Search, detected via
groundingMetadata.webSearchQueries (an actual web search was performed).
Whether the response used Grounding with Google Search or Grounding with Google Maps,
detected via groundingMetadata.webSearchQueries (an actual web search was performed) or
groundingMetadata.groundingChunks[].maps (a Maps lookup was performed).
Google bills grounding-with-Google-Search retrieved tokens separately (a per-request /
per-query search fee) and excludes them from input token billing, unlike URL context /
File Search / code execution whose tool-use tokens are charged at the input token rate.
URL context also emits groundingMetadata (with groundingChunks but no webSearchQueries),
so presence of groundingMetadata alone is not a sufficient signal.
Google bills both groundings separately (a per-request / per-query fee) and excludes their
retrieved tokens from input token billing, unlike URL context / File Search / code execution
whose tool-use tokens are charged at the input token rate. URL context also emits
groundingMetadata (with web groundingChunks but no webSearchQueries), so presence of
groundingMetadata alone is not a sufficient signal.
See https://ai.google.dev/gemini-api/docs/pricing and
https://github.com/BerriAI/litellm/discussions/33198
"""
@ -1732,7 +1734,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
return False
for candidate in completion_response["candidates"] or []:
grounding_metadata, _, _, _ = VertexGeminiConfig._extract_candidate_metadata(candidate)
if VertexGeminiConfig._calculate_web_search_requests(grounding_metadata):
if calculate_grounding_requests(grounding_metadata).has_billable_grounding():
return True
return False
@ -1979,15 +1981,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
@staticmethod
def _calculate_web_search_requests(grounding_metadata: list[dict]) -> int | None:
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
return calculate_grounding_requests(grounding_metadata).web_search_requests
@staticmethod
def _set_grounding_usage_counters(usage: Usage, grounding_metadata: Sequence[Mapping[str, object]]) -> None:
grounding_requests: Final = calculate_grounding_requests(grounding_metadata)
details: Final = cast(PromptTokensDetailsWrapper, usage.prompt_tokens_details)
if grounding_requests.web_search_requests is not None:
details.web_search_requests = grounding_requests.web_search_requests
if grounding_requests.google_maps_grounding_requests is not None:
details.google_maps_grounding_requests = grounding_requests.google_maps_grounding_requests
@staticmethod
def _create_streaming_choice(
@ -2453,9 +2456,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
usage: Final = VertexGeminiConfig._calculate_usage(completion_response=completion_response)
web_search_requests: Final = VertexGeminiConfig._calculate_web_search_requests(grounding_metadata)
if web_search_requests is not None:
cast(PromptTokensDetailsWrapper, usage.prompt_tokens_details).web_search_requests = web_search_requests
VertexGeminiConfig._set_grounding_usage_counters(usage, grounding_metadata)
setattr(model_response, "usage", usage)
@ -3220,9 +3221,7 @@ class ModelResponseIterator:
completion_response=processed_chunk,
)
web_search_requests: Final = VertexGeminiConfig._calculate_web_search_requests(grounding_metadata)
if web_search_requests is not None:
cast(PromptTokensDetailsWrapper, usage.prompt_tokens_details).web_search_requests = web_search_requests
VertexGeminiConfig._set_grounding_usage_counters(usage, grounding_metadata)
traffic_type: Final = processed_chunk.get("usageMetadata", {}).get("trafficType")
if traffic_type:

View file

@ -19796,6 +19796,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-image": {
@ -20085,7 +20086,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.1-flash-lite": {
"deprecation_date": "2027-05-07",
@ -20142,7 +20144,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.5-flash-lite": {
"deprecation_date": "2027-07-21",
@ -20198,7 +20201,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"deep-research-pro-preview-12-2025": {
"input_cost_per_image": 0.0011,
@ -20278,6 +20282,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-lite-preview-09-2025": {
@ -20323,6 +20328,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-preview-09-2025": {
@ -20368,6 +20374,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-live-2.5-flash-preview-native-audio-09-2025": {
@ -20504,6 +20511,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-pro": {
@ -20549,7 +20557,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-3-pro-preview": {
"deprecation_date": "2026-03-26",
@ -20663,7 +20672,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -20715,7 +20725,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3-pro-preview": {
"cache_read_input_token_cost": 2e-07,
@ -20818,7 +20829,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.5-flash": {
"prompt_cache_min_tokens": 4096,
@ -20873,6 +20885,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -20933,7 +20946,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -20989,7 +21003,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-pro-preview": {
"prompt_cache_min_tokens": 4096,
@ -21047,7 +21062,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -21105,7 +21121,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-2.5-pro-preview-tts": {
"cache_read_input_token_cost": 1.25e-07,
@ -21644,6 +21661,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-image": {
@ -21991,6 +22009,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-lite-preview-09-2025": {
@ -22039,6 +22058,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-preview-09-2025": {
@ -22087,6 +22107,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-flash-latest": {
@ -22133,7 +22154,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-flash-lite-latest": {
"cache_read_input_token_cost": 2.5e-08,
@ -22179,7 +22201,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-2.5-flash-lite-preview-06-17": {
"deprecation_date": "2025-11-18",
@ -22227,6 +22250,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-preview-tts": {
@ -22288,7 +22312,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-2.5-computer-use-preview-10-2025": {
"input_cost_per_token": 1.25e-06,
@ -22425,7 +22450,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.1-flash-lite": {
"cache_read_input_token_cost": 2.5e-08,
@ -22484,7 +22510,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.5-flash-lite": {
"cache_read_input_token_cost": 3e-08,
@ -22541,7 +22568,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3-flash-preview": {
"cache_read_input_token_cost": 5e-08,
@ -22593,7 +22621,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.5-flash": {
"prompt_cache_min_tokens": 4096,
@ -22649,6 +22678,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -22711,7 +22741,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -22769,7 +22800,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-omni-flash-preview": {
"input_cost_per_audio_token": 1.5e-06,
@ -22860,7 +22892,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -22918,7 +22951,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3-flash-preview": {
"cache_read_input_token_cost": 5e-08,
@ -22968,7 +23002,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-omni-flash-preview": {
"input_cost_per_audio_token": 1.5e-06,
@ -23054,6 +23089,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -23114,7 +23150,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -23170,7 +23207,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-2.5-pro-preview-tts": {
"cache_read_input_token_cost": 1.25e-07,
@ -41924,7 +41962,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-flash-lite": {
"deprecation_date": "2027-05-07",
@ -41982,7 +42021,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.5-flash-lite": {
"deprecation_date": "2027-07-21",
@ -42039,7 +42079,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/deep-research-pro-preview-12-2025": {
"input_cost_per_image": 0.0011,
@ -49135,7 +49176,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-flash-lite-latest": {
"cache_read_input_token_cost": 1e-08,
@ -49181,7 +49223,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-pro-latest": {
"cache_read_input_token_cost": 1.25e-07,
@ -49226,7 +49269,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-pro-latest": {
"cache_read_input_token_cost": 1.25e-07,
@ -49271,7 +49315,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-exp-1206": {
"cache_read_input_token_cost": 3e-08,

View file

@ -358,9 +358,10 @@ def _validate_ptu_model_info(model_info: Mapping[str, object]) -> None:
raise HTTPException(status_code=400, detail=error)
# The mirrored per-token pricing fields plus the three remaining fields
# Router._inherit_builtin_cache_pricing back-fills from the public cost map. An unset field is
# what that back-fill targets, so a field left out here is one a PTU deployment still bills.
# The mirrored per-token pricing fields plus the remaining rates the public cost map or a
# provider default would otherwise supply (the cache back-fills, the Maps grounding rate). An
# unset field falls back to those sources, so a field left out here is one a PTU deployment
# still bills.
# tiered_pricing is the one mirrored field that is a table of ranges, not a rate, so it is stored
# empty (see _PTU_EMPTIED_PRICING_FIELDS): its tiers outrank the zeros written beside them, so
# dropping it would leave the cost map's tiers billing the traffic the reserved capacity covers.

View file

@ -1,4 +1,4 @@
from typing_extensions import TypedDict
from typing_extensions import ReadOnly, TypedDict
from ..utils import CompletionTokensDetails, PromptTokensDetailsWrapper, ServerToolUse
@ -10,6 +10,7 @@ class UsagePerChunk(TypedDict):
cache_read_input_tokens: int | None
server_tool_use: ServerToolUse | None
web_search_requests: int | None
google_maps_grounding_requests: ReadOnly[int | None]
completion_tokens_details: CompletionTokensDetails | None
prompt_tokens_details: PromptTokensDetailsWrapper | None
cost: float | None

View file

@ -287,6 +287,7 @@ class ModelInfoBase(ProviderSpecificModelInfo, total=False):
web_search_billing_unit: (
Literal["per_query", "per_prompt"] | None
) # "per_query" (Gemini 3.x) or "per_prompt" (Gemini 2.x)
google_maps_grounding_cost_per_query: ReadOnly[float | None]
citation_cost_per_token: float | None # Cost per citation token for Perplexity
tiered_pricing: list[dict[str, Any]] | None # Tiered pricing structure for models like Dashscope
litellm_provider: Required[str]
@ -1613,6 +1614,9 @@ class PromptTokensDetailsWrapper(
web_search_requests: int | None = None
"""Number of web search requests made by the tool call. Used for Anthropic to calculate web search cost."""
google_maps_grounding_requests: int | None = None
"""Number of Grounding with Google Maps requests made by the tool call. Used for Gemini to calculate Maps cost."""
tool_use_tokens: int | None = None
"""Prompt tokens consumed by server-side tool use (e.g. Gemini grounding via googleSearch)."""
@ -1671,6 +1675,8 @@ class PromptTokensDetailsWrapper(
del self.audio_length_seconds
if self.web_search_requests is None:
del self.web_search_requests
if self.google_maps_grounding_requests is None:
del self.google_maps_grounding_requests
if self.tool_use_tokens is None:
del self.tool_use_tokens
if self.cache_write_tokens is None:
@ -3405,6 +3411,7 @@ class CustomPricingLiteLLMParams(MirroredPricingParams):
output_cost_per_video_per_second: float | None = None
output_cost_per_audio_per_second: float | None = None
search_context_cost_per_query: dict[str, Any] | None = None
google_maps_grounding_cost_per_query: float | None = None
citation_cost_per_token: float | None = None
cache_read_input_token_cost_above_272k_tokens: float | None = None
cache_read_input_token_cost_above_512k_tokens: float | None = None

View file

@ -5878,6 +5878,7 @@ def _get_model_info_helper(
supports_computer_use=_model_info.get("supports_computer_use", None),
search_context_cost_per_query=_model_info.get("search_context_cost_per_query", None),
web_search_billing_unit=_model_info.get("web_search_billing_unit", None),
google_maps_grounding_cost_per_query=_model_info.get("google_maps_grounding_cost_per_query", None),
tpm=_model_info.get("tpm", None),
rpm=_model_info.get("rpm", None),
ocr_cost_per_page=_model_info.get("ocr_cost_per_page", None),

View file

@ -19796,6 +19796,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-image": {
@ -20085,7 +20086,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.1-flash-lite": {
"deprecation_date": "2027-05-07",
@ -20142,7 +20144,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.5-flash-lite": {
"deprecation_date": "2027-07-21",
@ -20198,7 +20201,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"deep-research-pro-preview-12-2025": {
"input_cost_per_image": 0.0011,
@ -20278,6 +20282,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-lite-preview-09-2025": {
@ -20323,6 +20328,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-flash-preview-09-2025": {
@ -20368,6 +20374,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-live-2.5-flash-preview-native-audio-09-2025": {
@ -20504,6 +20511,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini-2.5-pro": {
@ -20549,7 +20557,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-3-pro-preview": {
"deprecation_date": "2026-03-26",
@ -20663,7 +20672,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -20715,7 +20725,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3-pro-preview": {
"cache_read_input_token_cost": 2e-07,
@ -20818,7 +20829,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.5-flash": {
"prompt_cache_min_tokens": 4096,
@ -20873,6 +20885,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -20933,7 +20946,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -20989,7 +21003,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-pro-preview": {
"prompt_cache_min_tokens": 4096,
@ -21047,7 +21062,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -21105,7 +21121,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-2.5-pro-preview-tts": {
"cache_read_input_token_cost": 1.25e-07,
@ -21644,6 +21661,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-image": {
@ -21991,6 +22009,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-lite-preview-09-2025": {
@ -22039,6 +22058,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-preview-09-2025": {
@ -22087,6 +22107,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-flash-latest": {
@ -22133,7 +22154,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-flash-lite-latest": {
"cache_read_input_token_cost": 2.5e-08,
@ -22179,7 +22201,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-2.5-flash-lite-preview-06-17": {
"deprecation_date": "2025-11-18",
@ -22227,6 +22250,7 @@
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
},
"google_maps_grounding_cost_per_query": 0.025,
"supports_image_size": false
},
"gemini/gemini-2.5-flash-preview-tts": {
@ -22288,7 +22312,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-2.5-computer-use-preview-10-2025": {
"input_cost_per_token": 1.25e-06,
@ -22425,7 +22450,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.1-flash-lite": {
"cache_read_input_token_cost": 2.5e-08,
@ -22484,7 +22510,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.5-flash-lite": {
"cache_read_input_token_cost": 3e-08,
@ -22541,7 +22568,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3-flash-preview": {
"cache_read_input_token_cost": 5e-08,
@ -22593,7 +22621,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.5-flash": {
"prompt_cache_min_tokens": 4096,
@ -22649,6 +22678,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -22711,7 +22741,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -22769,7 +22800,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-omni-flash-preview": {
"input_cost_per_audio_token": 1.5e-06,
@ -22860,7 +22892,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-3.1-pro-preview-customtools": {
"prompt_cache_min_tokens": 4096,
@ -22918,7 +22951,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3-flash-preview": {
"cache_read_input_token_cost": 5e-08,
@ -22968,7 +23002,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-omni-flash-preview": {
"input_cost_per_audio_token": 1.5e-06,
@ -23054,6 +23089,7 @@
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
"input_cost_per_token_batches": 7.5e-07,
"output_cost_per_token_batches": 4.5e-06,
"input_cost_per_token_flex": 7.5e-07,
@ -23114,7 +23150,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini-3.7-flash": {
"prompt_cache_min_tokens": 4096,
@ -23170,7 +23207,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"gemini/gemini-2.5-pro-preview-tts": {
"cache_read_input_token_cost": 1.25e-07,
@ -41924,7 +41962,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.1-flash-lite": {
"deprecation_date": "2027-05-07",
@ -41982,7 +42021,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/gemini-3.5-flash-lite": {
"deprecation_date": "2027-07-21",
@ -42039,7 +42079,8 @@
"search_context_size_medium": 0.014,
"search_context_size_high": 0.014
},
"web_search_billing_unit": "per_query"
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014
},
"vertex_ai/deep-research-pro-preview-12-2025": {
"input_cost_per_image": 0.0011,
@ -49135,7 +49176,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-flash-lite-latest": {
"cache_read_input_token_cost": 1e-08,
@ -49181,7 +49223,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-pro-latest": {
"cache_read_input_token_cost": 1.25e-07,
@ -49226,7 +49269,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini/gemini-pro-latest": {
"cache_read_input_token_cost": 1.25e-07,
@ -49271,7 +49315,8 @@
"search_context_size_low": 0.035,
"search_context_size_medium": 0.035,
"search_context_size_high": 0.035
}
},
"google_maps_grounding_cost_per_query": 0.025
},
"gemini-exp-1206": {
"cache_read_input_token_cost": 3e-08,

View file

@ -191,6 +191,11 @@
"gemini_native_audio": {
"type": "boolean"
},
"google_maps_grounding_cost_per_query": {
"type": "number",
"minimum": 0,
"description": "USD per Grounding with Google Maps request; billed per query or per prompt per web_search_billing_unit."
},
"guardrail_cost_per_unit": {
"type": "object",
"description": "USD cost per billable guardrail unit, keyed by the provider's usage counter name (e.g. Bedrock's contentPolicyUnits).",

View file

@ -512,6 +512,95 @@ def test_gemini_3x_web_search_billed_per_query(model, local_model_cost_map):
)
@pytest.mark.parametrize(
"model,custom_llm_provider",
[
("gemini/gemini-2.5-flash", "gemini"),
("vertex_ai/gemini-2.5-flash", "vertex_ai"),
],
)
def test_gemini_2x_maps_grounding_billed_at_maps_rate(model, custom_llm_provider, local_model_cost_map):
"""
Grounding with Google Maps is its own SKU: a Maps-only grounded prompt on Gemini 2.x bills the
$0.025 Maps per-prompt fee, not the $0.035 Google Search fee it was previously conflated with,
and not $0 as on Vertex AI where webSearchQueries is never populated for Maps.
Regression for https://github.com/BerriAI/litellm/issues/35906
"""
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
model_info = litellm.get_model_info(model)
expected_cost = model_info["google_maps_grounding_cost_per_query"]
assert expected_cost == pytest.approx(0.025)
usage = Usage(
prompt_tokens=15,
completion_tokens=100,
total_tokens=115,
prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=15, google_maps_grounding_requests=1),
)
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
model=model,
usage=usage,
response_object=None,
custom_llm_provider=custom_llm_provider,
standard_built_in_tools_params=None,
)
assert cost == pytest.approx(expected_cost)
def test_gemini_3x_maps_grounding_billed_per_query(local_model_cost_map):
"""Gemini 3.x bills Maps grounding per executed query: N queries cost N * $0.014."""
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
model = "vertex_ai/gemini-3.5-flash"
model_info = litellm.get_model_info(model)
assert model_info["web_search_billing_unit"] == "per_query"
expected_cost = model_info["google_maps_grounding_cost_per_query"] * 2
usage = Usage(
prompt_tokens=15,
completion_tokens=100,
total_tokens=115,
prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=15, google_maps_grounding_requests=2),
)
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
model=model,
usage=usage,
response_object=None,
custom_llm_provider="vertex_ai",
standard_built_in_tools_params=None,
)
assert cost == pytest.approx(expected_cost)
assert cost == pytest.approx(0.028)
def test_gemini_combined_search_and_maps_costs_are_additive(local_model_cost_map):
"""A prompt grounded with both Google Search and Google Maps pays both fees."""
from litellm.types.utils import PromptTokensDetailsWrapper, Usage
model = "gemini/gemini-3.5-flash"
model_info = litellm.get_model_info(model)
search_rate = model_info["search_context_cost_per_query"]["search_context_size_medium"]
maps_rate = model_info["google_maps_grounding_cost_per_query"]
usage = Usage(
prompt_tokens=15,
completion_tokens=100,
total_tokens=115,
prompt_tokens_details=PromptTokensDetailsWrapper(
text_tokens=15, web_search_requests=2, google_maps_grounding_requests=1
),
)
cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools(
model=model,
usage=usage,
response_object=None,
custom_llm_provider="gemini",
standard_built_in_tools_params=None,
)
assert cost == pytest.approx(search_rate * 2 + maps_rate)
def test_gemini_2x_web_search_still_billed_per_prompt(local_model_cost_map):
"""
Gemini 2.x bills web search per grounded prompt: multiple internal queries are one flat

View file

@ -134,6 +134,15 @@ def test_the_search_context_table_is_zeroed_in_place_on_every_deployment():
assert dict(override[field]) == dict.fromkeys(SEARCH_CONTEXT_SIZES, 0.0)
def test_the_maps_grounding_rate_is_zeroed_on_every_deployment():
"""An absent rate falls back to the Maps default rather than free, so it is written
even when the deployment never declared one."""
override = _with_flag(_VALID)
assert override is not None
assert override["google_maps_grounding_cost_per_query"] == 0.0
def test_a_declared_table_does_not_become_a_scalar():
"""Zeroing it as a plain 0.0 would leave the provider's reader without a table to
consult, which is the same as absent."""

View file

@ -711,6 +711,66 @@ def test_stream_chunk_builder_anthropic_web_search():
assert usage.server_tool_use.web_search_requests == 2
def test_calculate_usage_carries_google_maps_grounding_requests():
"""
The Maps grounding counter set on a streamed usage chunk must survive the stream rebuild even
when a later chunk carries its own prompt_tokens_details, or Maps grounding on streaming
requests silently bills $0.
"""
from litellm.types.utils import PromptTokensDetailsWrapper
chunk1 = ModelResponseStream(
id="chatcmpl-maps-usage-0",
created=1745513207,
model="gemini-2.5-flash",
object="chat.completion.chunk",
choices=[
StreamingChoices(
finish_reason=None,
index=0,
delta=Delta(content="Here"),
logprobs=None,
)
],
stream_options={"include_usage": True},
usage=Usage(
completion_tokens=0,
prompt_tokens=15,
total_tokens=15,
prompt_tokens_details=PromptTokensDetailsWrapper(google_maps_grounding_requests=1),
),
)
chunk2 = ModelResponseStream(
id="chatcmpl-maps-usage-0",
created=1745513207,
model="gemini-2.5-flash",
object="chat.completion.chunk",
choices=[
StreamingChoices(
finish_reason="stop",
index=0,
delta=Delta(content=None),
logprobs=None,
)
],
stream_options={"include_usage": True},
usage=Usage(
completion_tokens=27,
prompt_tokens=0,
total_tokens=27,
prompt_tokens_details=PromptTokensDetailsWrapper(text_tokens=0),
),
)
chunks = [chunk1, chunk2]
processor = ChunkProcessor(chunks=chunks)
usage = processor.calculate_usage(chunks=chunks, model="gemini-2.5-flash", completion_output="")
assert usage.prompt_tokens_details.google_maps_grounding_requests == 1
def test_sort_chunks_handles_dict_hidden_params_created_at():
chunks = [
{

View file

@ -3,7 +3,10 @@ import os
import pytest
import litellm
from litellm.llms.gemini.cost_calculator import cost_per_web_search_request
from litellm.llms.gemini.cost_calculator import (
cost_per_google_maps_grounding_request,
cost_per_web_search_request,
)
from litellm.llms.gemini.image_edit.cost_calculator import (
cost_calculator as gemini_image_edit_cost_calculator,
)
@ -81,6 +84,63 @@ def test_no_usage_details():
assert cost == 0.0
def _make_maps_usage(google_maps_grounding_requests: int) -> Usage:
return Usage(
prompt_tokens=100,
completion_tokens=50,
total_tokens=150,
prompt_tokens_details=PromptTokensDetailsWrapper(
google_maps_grounding_requests=google_maps_grounding_requests,
),
)
def test_maps_per_query_billing():
"""web_search_billing_unit=per_query charges per Maps query."""
model_info = {
"key": "gemini/gemini-3.5-flash",
"web_search_billing_unit": "per_query",
"google_maps_grounding_cost_per_query": 0.014,
}
cost = cost_per_google_maps_grounding_request(usage=_make_maps_usage(3), model_info=model_info)
assert cost == pytest.approx(0.014 * 3)
def test_maps_per_prompt_billing_clamps_to_one():
"""Without web_search_billing_unit, Maps grounding is one flat fee per grounded prompt."""
model_info = {
"key": "gemini/gemini-2.5-flash",
"google_maps_grounding_cost_per_query": 0.025,
}
cost = cost_per_google_maps_grounding_request(usage=_make_maps_usage(3), model_info=model_info)
assert cost == pytest.approx(0.025)
def test_maps_default_rate_per_query():
"""A per_query model missing the pricing key falls back to Google's $14/1K queries."""
model_info = {"key": "gemini/gemini-3.9-flash", "web_search_billing_unit": "per_query"}
cost = cost_per_google_maps_grounding_request(usage=_make_maps_usage(2), model_info=model_info)
assert cost == pytest.approx(0.014 * 2)
def test_maps_default_rate_per_prompt():
"""A per_prompt model missing the pricing key falls back to Google's $25/1K grounded prompts."""
model_info = {"key": "gemini/gemini-2.6-flash"}
cost = cost_per_google_maps_grounding_request(usage=_make_maps_usage(2), model_info=model_info)
assert cost == pytest.approx(0.025)
def test_maps_zero_requests():
model_info = {"key": "gemini/gemini-3.5-flash", "web_search_billing_unit": "per_query"}
assert cost_per_google_maps_grounding_request(usage=_make_maps_usage(0), model_info=model_info) == 0.0
def test_maps_no_usage_details():
usage = Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150)
model_info = {"key": "gemini/gemini-3.5-flash"}
assert cost_per_google_maps_grounding_request(usage=usage, model_info=model_info) == 0.0
def test_gemini_image_edit_cost_prefers_token_usage_metadata(monkeypatch):
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
litellm.model_cost = litellm.get_model_cost_map(url="")

View file

@ -0,0 +1,102 @@
from litellm.llms.vertex_ai.gemini.grounding_requests import (
GroundingRequests,
calculate_grounding_requests,
)
def test_search_only_counts_non_empty_queries_as_web_requests():
result = calculate_grounding_requests(
[
{
"webSearchQueries": ["", "capital of France", "France capital"],
"groundingChunks": [{"web": {"uri": "https://example.com", "title": "Example"}}],
}
]
)
assert result == GroundingRequests(web_search_requests=2, google_maps_grounding_requests=None)
def test_gemini_api_maps_only_counts_queries_as_maps_requests():
result = calculate_grounding_requests(
[
{
"webSearchQueries": ["coffee shops near the Louvre"],
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}],
}
]
)
assert result == GroundingRequests(web_search_requests=None, google_maps_grounding_requests=1)
def test_vertex_maps_only_without_queries_counts_one_maps_request():
result = calculate_grounding_requests(
[
{
"groundingChunks": [
{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}},
{"maps": {"uri": "https://maps.google.com/?cid=2", "placeId": "p2"}},
],
"groundingSupports": [],
}
]
)
assert result == GroundingRequests(web_search_requests=None, google_maps_grounding_requests=1)
def test_widget_context_token_alone_counts_one_maps_request():
result = calculate_grounding_requests([{"googleMapsWidgetContextToken": "widget-token"}])
assert result == GroundingRequests(web_search_requests=None, google_maps_grounding_requests=1)
def test_combined_web_and_maps_chunks_split_between_both_counters():
result = calculate_grounding_requests(
[
{
"webSearchQueries": ["q1", "q2"],
"groundingChunks": [
{"web": {"uri": "https://example.com"}},
{"maps": {"uri": "https://maps.google.com/?cid=1"}},
],
}
]
)
assert result == GroundingRequests(web_search_requests=2, google_maps_grounding_requests=1)
def test_url_context_grounding_chunks_without_queries_count_nothing():
result = calculate_grounding_requests([{"groundingChunks": [{"web": {"uri": "https://example.com"}}]}])
assert result == GroundingRequests(web_search_requests=None, google_maps_grounding_requests=None)
def test_counters_count_distinct_queries_across_candidates():
result = calculate_grounding_requests(
[
{"webSearchQueries": ["a"]},
{"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1"}}]},
{"webSearchQueries": ["b", "c"], "groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=2"}}]},
]
)
assert result == GroundingRequests(web_search_requests=1, google_maps_grounding_requests=2)
def test_duplicate_queries_across_candidates_collapse_per_bucket():
result = calculate_grounding_requests(
[
{"webSearchQueries": ["shared", "web only"], "groundingChunks": [{"web": {"uri": "https://e.com"}}]},
{"webSearchQueries": ["shared"], "groundingChunks": [{"web": {"uri": "https://e.com"}}]},
{"webSearchQueries": ["maps q", "maps q"], "groundingChunks": [{"maps": {"uri": "https://m.com"}}]},
{"webSearchQueries": ["maps q"], "groundingChunks": [{"maps": {"uri": "https://m.com"}}]},
]
)
assert result == GroundingRequests(web_search_requests=2, google_maps_grounding_requests=1)
def test_empty_metadata_counts_nothing():
result = calculate_grounding_requests([])
assert result == GroundingRequests(web_search_requests=None, google_maps_grounding_requests=None)
def test_has_billable_grounding():
assert GroundingRequests(web_search_requests=None, google_maps_grounding_requests=1).has_billable_grounding()
assert GroundingRequests(web_search_requests=1, google_maps_grounding_requests=None).has_billable_grounding()
assert not GroundingRequests(web_search_requests=None, google_maps_grounding_requests=None).has_billable_grounding()

View file

@ -549,9 +549,10 @@ def test_vertex_ai_non_grounded_usage_omits_tool_use_tokens():
def test_response_has_search_grounding_detection():
"""
Only groundingMetadata.webSearchQueries signals an actual Google Search. URL context also
emits groundingMetadata (groundingChunks but no webSearchQueries) and must not be treated
as search grounding.
groundingMetadata.webSearchQueries signals an actual Google Search and
groundingMetadata.groundingChunks[].maps signals a Google Maps lookup. URL context also
emits groundingMetadata (web groundingChunks but no webSearchQueries) and must not be
treated as billable grounding.
"""
assert (
VertexGeminiConfig._response_has_search_grounding(
@ -580,6 +581,101 @@ def test_response_has_search_grounding_detection():
)
assert VertexGeminiConfig._response_has_search_grounding({"candidates": []}) is False
assert VertexGeminiConfig._response_has_search_grounding({}) is False
assert (
VertexGeminiConfig._response_has_search_grounding(
{
"candidates": [
{
"groundingMetadata": {
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}]
}
}
]
}
)
is True
)
def test_vertex_ai_maps_grounding_tool_use_tokens_excluded_from_prompt_tokens():
"""
Grounding with Google Maps retrieved tokens are billed like Google Search grounding: a
separate per-request / per-query fee, with toolUsePromptTokenCount surfaced on
prompt_tokens_details.tool_use_tokens but excluded from prompt_tokens. Before Maps detection
existed, a Vertex AI Maps-only response folded the 120 tool-use tokens into prompt_tokens.
Regression for https://github.com/BerriAI/litellm/issues/35906
"""
v = VertexGeminiConfig()
completion_response = {
"candidates": [
{
"groundingMetadata": {
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}]
}
}
],
"usageMetadata": UsageMetadata(
promptTokenCount=15,
candidatesTokenCount=100,
toolUsePromptTokenCount=120,
totalTokenCount=235,
),
}
usage = v._calculate_usage(completion_response=completion_response)
assert usage.prompt_tokens == 15
assert usage.completion_tokens == 100
assert usage.total_tokens == 235
assert usage.prompt_tokens_details.tool_use_tokens == 120
def test_vertex_ai_maps_grounding_sets_google_maps_grounding_requests_non_streaming():
"""
A Vertex AI Maps-only response (groundingChunks[].maps, no webSearchQueries) must set
google_maps_grounding_requests and leave web_search_requests unset, so the Maps fee is
billed instead of nothing (Vertex) or the Google Search fee (Gemini API).
"""
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
VertexGeminiConfig,
)
completion_response = {
"candidates": [
{
"content": {"parts": [{"text": "Here are some coffee shops"}], "role": "model"},
"finishReason": "STOP",
"groundingMetadata": {
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}],
"groundingSupports": [],
},
}
],
"usageMetadata": {
"promptTokenCount": 15,
"candidatesTokenCount": 100,
"totalTokenCount": 115,
},
}
raw_response = MagicMock()
raw_response.json.return_value = completion_response
result = VertexGeminiConfig().transform_response(
model="gemini-2.5-flash",
raw_response=raw_response,
model_response=ModelResponse(),
logging_obj=MagicMock(),
request_data={},
messages=[],
optional_params={},
litellm_params={},
encoding=None,
)
usage = result.usage
assert usage.prompt_tokens_details.google_maps_grounding_requests == 1
assert not hasattr(usage.prompt_tokens_details, "web_search_requests")
def test_vertex_ai_search_grounding_tool_use_tokens_excluded_from_prompt_tokens():
@ -1292,6 +1388,66 @@ def test_vertex_ai_streaming_usage_web_search_calculation():
assert usage.prompt_tokens_details.web_search_requests == 2
def test_vertex_ai_maps_grounding_chunk_parser_sets_maps_requests():
"""A Vertex-shaped Maps-only streaming chunk sets the Maps counter and not the Search one."""
from unittest.mock import MagicMock
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
ModelResponseIterator,
)
chunk = {
"candidates": [
{
"content": {"parts": [{"text": "Here"}]},
"groundingMetadata": {
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}],
"groundingSupports": [],
},
}
],
"usageMetadata": {"promptTokenCount": 15, "candidatesTokenCount": 10, "totalTokenCount": 25},
}
iterator = ModelResponseIterator(streaming_response=[], sync_stream=True, logging_obj=MagicMock())
completed_response = iterator.chunk_parser(chunk)
usage = completed_response.usage
assert usage.prompt_tokens_details.google_maps_grounding_requests == 1
assert not hasattr(usage.prompt_tokens_details, "web_search_requests")
def test_gemini_api_maps_grounding_chunk_parser_counts_queries_as_maps_requests():
"""A Gemini-API-shaped Maps chunk (webSearchQueries plus maps chunks) bills Maps, not Search."""
from unittest.mock import MagicMock
from litellm.llms.vertex_ai.gemini.vertex_and_google_ai_studio_gemini import (
ModelResponseIterator,
)
chunk = {
"candidates": [
{
"content": {"parts": [{"text": "Here"}]},
"groundingMetadata": [
{
"webSearchQueries": ["coffee shops near the Louvre"],
"groundingChunks": [{"maps": {"uri": "https://maps.google.com/?cid=1", "placeId": "p1"}}],
}
],
}
],
"usageMetadata": {"promptTokenCount": 15, "candidatesTokenCount": 10, "totalTokenCount": 25},
}
iterator = ModelResponseIterator(streaming_response=[], sync_stream=True, logging_obj=MagicMock())
completed_response = iterator.chunk_parser(chunk)
usage = completed_response.usage
assert usage.prompt_tokens_details.google_maps_grounding_requests == 1
assert not hasattr(usage.prompt_tokens_details, "web_search_requests")
def test_vertex_ai_transform_parts():
"""
Test the _transform_parts method for converting Vertex AI function calls

View file

@ -874,6 +874,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"deprecation_date": {"type": "string"},
"input_cost_per_audio_per_second": {"type": "number"},
"input_cost_per_audio_per_second_above_128k_tokens": {"type": "number"},
"google_maps_grounding_cost_per_query": {"type": "number"},
"input_cost_per_audio_token": {"type": "number"},
"input_cost_per_image_token": {"type": "number"},
"input_cost_per_character": {"type": "number"},

View file

@ -3,7 +3,7 @@
"limit": 22733
},
"LIT002": {
"limit": 26864
"limit": 26863
},
"LIT003": {
"limit": 269
@ -15,7 +15,7 @@
"limit": 0
},
"LIT006": {
"limit": 1066
"limit": 1065
},
"LIT007": {
"limit": 0
@ -27,10 +27,10 @@
"limit": 0
},
"LIT010": {
"limit": 16620
"limit": 16619
},
"LIT011": {
"limit": 5585
"limit": 5583
},
"LIT012": {
"limit": 4510

View file

@ -27550,6 +27550,8 @@ export interface components {
default_api_key_tpm_limit?: number | null;
/** Gcs Bucket Name */
gcs_bucket_name?: string | null;
/** Google Maps Grounding Cost Per Query */
google_maps_grounding_cost_per_query?: number | null;
/** Input Cost Per Audio Per Second */
input_cost_per_audio_per_second?: number | null;
/** Input Cost Per Audio Per Second Above 128K Tokens */
@ -36772,6 +36774,8 @@ export interface components {
default_api_key_tpm_limit?: number | null;
/** Gcs Bucket Name */
gcs_bucket_name?: string | null;
/** Google Maps Grounding Cost Per Query */
google_maps_grounding_cost_per_query?: number | null;
/** Input Cost Per Audio Per Second */
input_cost_per_audio_per_second?: number | null;
/** Input Cost Per Audio Per Second Above 128K Tokens */