mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-07 07:58:09 +00:00
fix(exa_ai): estimate unreported search cost from Exa list price
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a955a0ef2a
commit
d0a37501fc
5 changed files with 147 additions and 36 deletions
51
litellm/llms/exa_ai/search/cost_calculator.py
Normal file
51
litellm/llms/exa_ai/search/cost_calculator.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from litellm.utils import get_model_info
|
||||
|
||||
EXA_AI_INCLUDED_RESULTS: Final = 10
|
||||
EXA_AI_ADDITIONAL_RESULT_COST: Final = 0.001
|
||||
EXA_AI_STANDARD_SEARCH_MODEL: Final = "exa_ai/search"
|
||||
EXA_AI_PRICING_MODEL_BY_TYPE: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
"deep-lite": "exa_ai/search-deep",
|
||||
"deep": "exa_ai/search-deep",
|
||||
"deep-reasoning": "exa_ai/search-deep-reasoning",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _non_negative_number(value: object) -> float | None:
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or value < 0:
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
def _requested_results(optional_params: Mapping[str, object]) -> int:
|
||||
for key in ("numResults", "max_results"):
|
||||
requested = _non_negative_number(optional_params.get(key))
|
||||
if requested is not None:
|
||||
return int(requested)
|
||||
return EXA_AI_INCLUDED_RESULTS
|
||||
|
||||
|
||||
def _base_request_cost(search_type: object) -> float:
|
||||
pricing_model: Final = (
|
||||
EXA_AI_PRICING_MODEL_BY_TYPE.get(search_type, EXA_AI_STANDARD_SEARCH_MODEL)
|
||||
if isinstance(search_type, str)
|
||||
else EXA_AI_STANDARD_SEARCH_MODEL
|
||||
)
|
||||
model_info: Final = get_model_info(model=pricing_model, custom_llm_provider="exa_ai")
|
||||
return float(model_info.get("input_cost_per_query") or 0.0)
|
||||
|
||||
|
||||
def exa_ai_list_price(optional_params: Mapping[str, object]) -> float:
|
||||
"""
|
||||
List price of one Exa /search request, for responses that carry no `costDollars.total`.
|
||||
|
||||
Multi-query searches are joined into a single request, so this prices one request:
|
||||
the per-type base price plus a per-result charge above the results that price includes.
|
||||
"""
|
||||
additional_results: Final = max(_requested_results(optional_params) - EXA_AI_INCLUDED_RESULTS, 0)
|
||||
return _base_request_cost(optional_params.get("type")) + additional_results * EXA_AI_ADDITIONAL_RESULT_COST
|
||||
|
|
@ -23885,24 +23885,28 @@
|
|||
"output_vector_size": 2560
|
||||
},
|
||||
"exa_ai/search": {
|
||||
"input_cost_per_query": 0.007,
|
||||
"litellm_provider": "exa_ai",
|
||||
"mode": "search",
|
||||
"tiered_pricing": [
|
||||
{
|
||||
"input_cost_per_query": 0.005,
|
||||
"max_results_range": [
|
||||
0,
|
||||
25
|
||||
]
|
||||
},
|
||||
{
|
||||
"input_cost_per_query": 0.025,
|
||||
"max_results_range": [
|
||||
26,
|
||||
100
|
||||
]
|
||||
}
|
||||
]
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request, covering up to 10 results. Each result above 10 adds $0.001. Exa's reported costDollars.total takes precedence when present. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"exa_ai/search-deep": {
|
||||
"input_cost_per_query": 0.012,
|
||||
"litellm_provider": "exa_ai",
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request with type deep or deep-lite, covering up to 10 results. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"exa_ai/search-deep-reasoning": {
|
||||
"input_cost_per_query": 0.015,
|
||||
"litellm_provider": "exa_ai",
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request with type deep-reasoning, covering up to 10 results. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"firecrawl/search": {
|
||||
"litellm_provider": "firecrawl",
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def search_provider_cost_per_query(
|
|||
Calculate cost for search-only providers.
|
||||
|
||||
Returns (input_cost, output_cost) where input_cost = queries * cost_per_query
|
||||
Uses Exa response cost when available, otherwise tiered pricing based on max_results.
|
||||
Uses Exa's reported cost when available, otherwise its list price; other providers use tiered pricing based on max_results.
|
||||
|
||||
Args:
|
||||
model: Model name (e.g., "exa_ai/search", "tavily/search")
|
||||
|
|
@ -65,6 +65,10 @@ def search_provider_cost_per_query(
|
|||
if isinstance(total, (int, float)) and not isinstance(total, bool) and math.isfinite(total) and total >= 0:
|
||||
return (float(total), 0.0)
|
||||
|
||||
from litellm.llms.exa_ai.search.cost_calculator import exa_ai_list_price
|
||||
|
||||
return (exa_ai_list_price(optional_params if optional_params is not None else EMPTY_OPTIONAL_PARAMS), 0.0)
|
||||
|
||||
if custom_llm_provider == "parallel_ai":
|
||||
from litellm.llms.parallel_ai.search.cost_calculator import (
|
||||
PARALLEL_AI_USAGE_PARAM,
|
||||
|
|
@ -79,7 +83,7 @@ def search_provider_cost_per_query(
|
|||
|
||||
model_info: Final = get_model_info(model=model, custom_llm_provider=custom_llm_provider)
|
||||
|
||||
# Check for tiered pricing (e.g., Exa AI based on max_results)
|
||||
# Check for tiered pricing based on max_results
|
||||
tiered_pricing: Final = model_info.get("tiered_pricing")
|
||||
if tiered_pricing and isinstance(tiered_pricing, list):
|
||||
max_results: Final = (optional_params or {}).get("max_results", 10) # default 10 results
|
||||
|
|
|
|||
|
|
@ -23885,24 +23885,28 @@
|
|||
"output_vector_size": 2560
|
||||
},
|
||||
"exa_ai/search": {
|
||||
"input_cost_per_query": 0.007,
|
||||
"litellm_provider": "exa_ai",
|
||||
"mode": "search",
|
||||
"tiered_pricing": [
|
||||
{
|
||||
"input_cost_per_query": 0.005,
|
||||
"max_results_range": [
|
||||
0,
|
||||
25
|
||||
]
|
||||
},
|
||||
{
|
||||
"input_cost_per_query": 0.025,
|
||||
"max_results_range": [
|
||||
26,
|
||||
100
|
||||
]
|
||||
}
|
||||
]
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request, covering up to 10 results. Each result above 10 adds $0.001. Exa's reported costDollars.total takes precedence when present. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"exa_ai/search-deep": {
|
||||
"input_cost_per_query": 0.012,
|
||||
"litellm_provider": "exa_ai",
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request with type deep or deep-lite, covering up to 10 results. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"exa_ai/search-deep-reasoning": {
|
||||
"input_cost_per_query": 0.015,
|
||||
"litellm_provider": "exa_ai",
|
||||
"metadata": {
|
||||
"notes": "Base price per /search request with type deep-reasoning, covering up to 10 results. Source: https://exa.ai/pricing (2026-10-15)"
|
||||
},
|
||||
"mode": "search"
|
||||
},
|
||||
"firecrawl/search": {
|
||||
"litellm_provider": "firecrawl",
|
||||
|
|
|
|||
|
|
@ -437,3 +437,51 @@ def test_exa_missing_or_invalid_total_uses_estimate(reported: object) -> None:
|
|||
)
|
||||
== estimate
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
def test_exa_estimate_adds_each_result_above_the_included_ten() -> None:
|
||||
from litellm.llms.exa_ai.search.cost_calculator import EXA_AI_ADDITIONAL_RESULT_COST
|
||||
from litellm.search.cost_calculator import search_provider_cost_per_query
|
||||
|
||||
base, _ = search_provider_cost_per_query("exa_ai/search", "exa_ai")
|
||||
|
||||
assert base > 0.0
|
||||
assert search_provider_cost_per_query("exa_ai/search", "exa_ai", optional_params={"max_results": 5}) == (base, 0.0)
|
||||
assert search_provider_cost_per_query(
|
||||
"exa_ai/search", "exa_ai", optional_params={"max_results": 25}
|
||||
) == pytest.approx((base + 15 * EXA_AI_ADDITIONAL_RESULT_COST, 0.0))
|
||||
assert search_provider_cost_per_query(
|
||||
"exa_ai/search", "exa_ai", optional_params={"numResults": 12}
|
||||
) == pytest.approx((base + 2 * EXA_AI_ADDITIONAL_RESULT_COST, 0.0))
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
def test_exa_estimate_prices_one_request_however_many_queries_it_joins() -> None:
|
||||
from litellm.search.cost_calculator import search_provider_cost_per_query
|
||||
|
||||
assert search_provider_cost_per_query(
|
||||
"exa_ai/search", "exa_ai", number_of_queries=3
|
||||
) == search_provider_cost_per_query("exa_ai/search", "exa_ai")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map")
|
||||
@pytest.mark.parametrize(
|
||||
("search_type", "pricing_model"),
|
||||
[
|
||||
("auto", "exa_ai/search"),
|
||||
("fast", "exa_ai/search"),
|
||||
("deep-lite", "exa_ai/search-deep"),
|
||||
("deep", "exa_ai/search-deep"),
|
||||
("deep-reasoning", "exa_ai/search-deep-reasoning"),
|
||||
],
|
||||
)
|
||||
def test_exa_estimate_uses_the_price_of_its_search_type(search_type: str, pricing_model: str) -> None:
|
||||
from litellm.search.cost_calculator import search_provider_cost_per_query
|
||||
|
||||
model_info: Final = litellm.get_model_info(model=pricing_model, custom_llm_provider="exa_ai")
|
||||
|
||||
assert search_provider_cost_per_query("exa_ai/search", "exa_ai", optional_params={"type": search_type}) == (
|
||||
model_info["input_cost_per_query"],
|
||||
0.0,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue