mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(gemini): bill Google Maps grounding as its own SKU
Gemini API Maps-grounded prompts were billed as web search and Vertex AI Maps-grounded prompts were not billed at all. Classify grounding metadata per candidate into web search vs Maps requests, carry a distinct google_maps_grounding_requests usage counter through non-streaming and streaming paths, and price it via the new google_maps_grounding_cost_per_query cost map key with per-query and per-prompt defaults keyed off web_search_billing_unit. Fixes #35906
This commit is contained in:
parent
f57e4b812c
commit
b9a790899a
21 changed files with 854 additions and 123 deletions
|
|
@ -105,13 +105,13 @@
|
|||
"limit": 109
|
||||
},
|
||||
"reportUnknownMemberType": {
|
||||
"limit": 38808
|
||||
"limit": 38804
|
||||
},
|
||||
"reportUnknownParameterType": {
|
||||
"limit": 19829
|
||||
},
|
||||
"reportUnknownVariableType": {
|
||||
"limit": 30356
|
||||
"limit": 30355
|
||||
},
|
||||
"reportUnnecessaryCast": {
|
||||
"limit": 117
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
49
litellm/llms/vertex_ai/gemini/grounding_requests.py
Normal file
49
litellm/llms/vertex_ai/gemini/grounding_requests.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
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 _query_count(item: Mapping[str, object]) -> int:
|
||||
queries: Final = item.get("webSearchQueries")
|
||||
if not isinstance(queries, list):
|
||||
return 0
|
||||
return len([query for query in queries if query])
|
||||
|
||||
|
||||
def grounding_item_requests(item: Mapping[str, object]) -> GroundingRequests:
|
||||
kinds: Final = _chunk_kinds(item)
|
||||
queries: Final = _query_count(item)
|
||||
if "maps" not in kinds and not item.get("googleMapsWidgetContextToken"):
|
||||
return GroundingRequests(web_search_requests=queries or None, google_maps_grounding_requests=None)
|
||||
if "web" in kinds:
|
||||
return GroundingRequests(web_search_requests=queries or None, google_maps_grounding_requests=1)
|
||||
return GroundingRequests(web_search_requests=None, google_maps_grounding_requests=max(queries, 1))
|
||||
|
||||
|
||||
def _total(counts: Sequence[int | None]) -> int | None:
|
||||
present: Final = tuple(count for count in counts if count is not None)
|
||||
return sum(present) if present else None
|
||||
|
||||
|
||||
def calculate_grounding_requests(grounding_metadata: Sequence[Mapping[str, object]]) -> GroundingRequests:
|
||||
per_item: Final = tuple(grounding_item_requests(item) for item in grounding_metadata if isinstance(item, Mapping))
|
||||
return GroundingRequests(
|
||||
web_search_requests=_total(tuple(item.web_search_requests for item in per_item)),
|
||||
google_maps_grounding_requests=_total(tuple(item.google_maps_grounding_requests for item in per_item)),
|
||||
)
|
||||
|
|
@ -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,16 +1981,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig):
|
|||
|
||||
@staticmethod
|
||||
def _calculate_web_search_requests(grounding_metadata: list[dict]) -> int | None:
|
||||
web_search_requests: int | None = None
|
||||
return calculate_grounding_requests(grounding_metadata).web_search_requests
|
||||
|
||||
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
|
||||
@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(
|
||||
|
|
@ -2454,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)
|
||||
|
||||
|
|
@ -3221,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:
|
||||
|
|
|
|||
|
|
@ -19778,6 +19778,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": {
|
||||
|
|
@ -20067,7 +20068,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",
|
||||
|
|
@ -20124,7 +20126,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",
|
||||
|
|
@ -20180,7 +20183,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,
|
||||
|
|
@ -20260,6 +20264,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": {
|
||||
|
|
@ -20305,6 +20310,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": {
|
||||
|
|
@ -20350,6 +20356,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": {
|
||||
|
|
@ -20486,6 +20493,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": {
|
||||
|
|
@ -20531,7 +20539,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",
|
||||
|
|
@ -20645,7 +20654,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,
|
||||
|
|
@ -20697,7 +20707,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,
|
||||
|
|
@ -20800,7 +20811,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,
|
||||
|
|
@ -20855,6 +20867,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,
|
||||
|
|
@ -20915,7 +20928,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,
|
||||
|
|
@ -20971,7 +20985,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,
|
||||
|
|
@ -21029,7 +21044,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,
|
||||
|
|
@ -21087,7 +21103,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,
|
||||
|
|
@ -21626,6 +21643,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": {
|
||||
|
|
@ -21973,6 +21991,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": {
|
||||
|
|
@ -22021,6 +22040,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": {
|
||||
|
|
@ -22069,6 +22089,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": {
|
||||
|
|
@ -22115,7 +22136,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,
|
||||
|
|
@ -22161,7 +22183,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",
|
||||
|
|
@ -22209,6 +22232,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": {
|
||||
|
|
@ -22270,7 +22294,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,
|
||||
|
|
@ -22407,7 +22432,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,
|
||||
|
|
@ -22466,7 +22492,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,
|
||||
|
|
@ -22523,7 +22550,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,
|
||||
|
|
@ -22575,7 +22603,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,
|
||||
|
|
@ -22631,6 +22660,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,
|
||||
|
|
@ -22693,7 +22723,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,
|
||||
|
|
@ -22751,7 +22782,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,
|
||||
|
|
@ -22842,7 +22874,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,
|
||||
|
|
@ -22900,7 +22933,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,
|
||||
|
|
@ -22950,7 +22984,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,
|
||||
|
|
@ -23036,6 +23071,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,
|
||||
|
|
@ -23096,7 +23132,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,
|
||||
|
|
@ -23152,7 +23189,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,
|
||||
|
|
@ -41885,7 +41923,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",
|
||||
|
|
@ -41943,7 +41982,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",
|
||||
|
|
@ -42000,7 +42040,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,
|
||||
|
|
@ -49096,7 +49137,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,
|
||||
|
|
@ -49142,7 +49184,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,
|
||||
|
|
@ -49187,7 +49230,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,
|
||||
|
|
@ -49232,7 +49276,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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -19778,6 +19778,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": {
|
||||
|
|
@ -20067,7 +20068,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",
|
||||
|
|
@ -20124,7 +20126,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",
|
||||
|
|
@ -20180,7 +20183,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,
|
||||
|
|
@ -20260,6 +20264,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": {
|
||||
|
|
@ -20305,6 +20310,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": {
|
||||
|
|
@ -20350,6 +20356,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": {
|
||||
|
|
@ -20486,6 +20493,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": {
|
||||
|
|
@ -20531,7 +20539,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",
|
||||
|
|
@ -20645,7 +20654,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,
|
||||
|
|
@ -20697,7 +20707,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,
|
||||
|
|
@ -20800,7 +20811,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,
|
||||
|
|
@ -20855,6 +20867,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,
|
||||
|
|
@ -20915,7 +20928,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,
|
||||
|
|
@ -20971,7 +20985,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,
|
||||
|
|
@ -21029,7 +21044,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,
|
||||
|
|
@ -21087,7 +21103,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,
|
||||
|
|
@ -21626,6 +21643,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": {
|
||||
|
|
@ -21973,6 +21991,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": {
|
||||
|
|
@ -22021,6 +22040,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": {
|
||||
|
|
@ -22069,6 +22089,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": {
|
||||
|
|
@ -22115,7 +22136,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,
|
||||
|
|
@ -22161,7 +22183,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",
|
||||
|
|
@ -22209,6 +22232,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": {
|
||||
|
|
@ -22270,7 +22294,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,
|
||||
|
|
@ -22407,7 +22432,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,
|
||||
|
|
@ -22466,7 +22492,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,
|
||||
|
|
@ -22523,7 +22550,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,
|
||||
|
|
@ -22575,7 +22603,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,
|
||||
|
|
@ -22631,6 +22660,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,
|
||||
|
|
@ -22693,7 +22723,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,
|
||||
|
|
@ -22751,7 +22782,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,
|
||||
|
|
@ -22842,7 +22874,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,
|
||||
|
|
@ -22900,7 +22933,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,
|
||||
|
|
@ -22950,7 +22984,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,
|
||||
|
|
@ -23036,6 +23071,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,
|
||||
|
|
@ -23096,7 +23132,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,
|
||||
|
|
@ -23152,7 +23189,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,
|
||||
|
|
@ -41885,7 +41923,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",
|
||||
|
|
@ -41943,7 +41982,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",
|
||||
|
|
@ -42000,7 +42040,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,
|
||||
|
|
@ -49096,7 +49137,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,
|
||||
|
|
@ -49142,7 +49184,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,
|
||||
|
|
@ -49187,7 +49230,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,
|
||||
|
|
@ -49232,7 +49276,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,
|
||||
|
|
|
|||
|
|
@ -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).",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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="")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
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_sum_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=3)
|
||||
|
||||
|
||||
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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": 16621
|
||||
"limit": 16619
|
||||
},
|
||||
"LIT011": {
|
||||
"limit": 5585
|
||||
"limit": 5583
|
||||
},
|
||||
"LIT012": {
|
||||
"limit": 4510
|
||||
|
|
|
|||
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
4
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -27552,6 +27552,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 */
|
||||
|
|
@ -36774,6 +36776,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 */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue