fix(cost): bill Batch cached tokens at the Standard cache price when a row has no Batch cache price (#50062)

* fix(cost): bill Batch cached tokens at the Standard cache price when a row has no Batch cache price

* fix(cost): resolve the Standard cache price for the prompt length in the Batch fallback

---------

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Kerry Lu 2026-11-04 15:47:35 -08:00 • committed by GitHub
parent 85fdea0fd1
commit b401ac6aaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 125 additions and 4 deletions

View file

@ -3323,15 +3323,35 @@ def batch_cost_calculator(
return total_prompt_cost * vertex_uplift, total_completion_cost * vertex_uplift
def _standard_rate(model_info: ModelInfo, key: str, prompt_tokens: int) -> float | None:
return get_cost_per_unit(model_info, get_modality_token_cost_key(model_info, key, prompt_tokens), None)
def _cached_audio_batch_fallback(
model_info: ModelInfo, batch_cache_read_rate: float | None, cache_read_rate: float | None, prompt_tokens: int
) -> float:
if batch_cache_read_rate is not None:
return batch_cache_read_rate
standard_audio_rate: Final = _standard_rate(model_info, "cache_read_input_audio_token_cost", prompt_tokens)
if standard_audio_rate is not None:
return standard_audio_rate
return 0.0 if cache_read_rate is None else cache_read_rate
def _batch_prompt_cost(
usage: Usage,
model_info: ModelInfo,
input_rate: float,
cache_read_rate: float | None,
batch_cache_read_rate: float | None,
cache_creation_rate: float | None,
cache_creation_rates_by_ttl: Mapping[str, float],
) -> float:
details: Final = parse_prompt_tokens_details(usage)
cache_read_rate: Final = (
_standard_rate(model_info, "cache_read_input_token_cost", usage.prompt_tokens)
if batch_cache_read_rate is None
else batch_cache_read_rate
)
cached_tokens: Final = details["cache_hit_tokens"] if cache_read_rate is not None else 0
cached_audio_tokens: Final = details["cache_hit_audio_tokens"] if cache_read_rate is not None else 0
bills_cache_writes: Final = cache_creation_rate is not None or bool(cache_creation_rates_by_ttl)
@ -3352,7 +3372,11 @@ def _batch_prompt_cost(
+ video_tokens * _batch_rate(model_info, "input_cost_per_video_token_batches", input_rate)
+ (cached_tokens - cached_audio_tokens) * (cache_read_rate or 0.0)
+ cached_audio_tokens
* _batch_rate(model_info, "cache_read_input_audio_token_cost_batches", cache_read_rate or 0.0)
* _batch_rate(
model_info,
"cache_read_input_audio_token_cost_batches",
_cached_audio_batch_fallback(model_info, batch_cache_read_rate, cache_read_rate, usage.prompt_tokens),
)
+ (
calculate_cache_writing_cost(
written_tokens,

View file

@ -4531,6 +4531,103 @@ def test_batch_cost_calculator_falls_back_to_text_batch_rate_for_modalities():
assert prompt_cost == pytest.approx(100 * 1e-7)
def _cached_audio_batch_usage() -> Usage:
return Usage(
prompt_tokens=1000,
completion_tokens=0,
total_tokens=1000,
prompt_tokens_details=PromptTokensDetailsWrapper(
cached_tokens=1000, audio_tokens=1000, cached_tokens_details=CachedTokensDetails(audio_tokens=1000)
),
)
def _cached_text_batch_usage() -> Usage:
return Usage(
prompt_tokens=1000,
completion_tokens=0,
total_tokens=1000,
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=1000),
)
@pytest.mark.parametrize(
("audio_batch_cache_price", "usage", "expected_rate"),
[
pytest.param(None, _cached_audio_batch_usage(), 1e-7, id="cached-audio-bills-the-standard-audio-cache-price"),
pytest.param(None, _cached_text_batch_usage(), 3e-8, id="cached-text-bills-the-standard-cache-price"),
pytest.param(8e-8, _cached_audio_batch_usage(), 8e-8, id="an-explicit-audio-batch-cache-price-still-wins"),
],
)
def test_batch_cost_calculator_bills_cached_tokens_at_the_standard_cache_price_when_the_row_has_no_batch_cache_price(
audio_batch_cache_price: float | None, usage: Usage, expected_rate: float
) -> None:
model_info: Final[ModelInfo] = {
"input_cost_per_token": 3e-7,
"input_cost_per_audio_token": 1e-6,
"cache_read_input_token_cost": 3e-8,
"cache_read_input_audio_token_cost": 1e-7,
"input_cost_per_token_batches": 1.5e-7,
"input_cost_per_audio_token_batches": 5e-7,
"cache_read_input_audio_token_cost_batches": audio_batch_cache_price,
}
prompt_cost, _ = batch_cost_calculator(
usage=usage, model="gemini-2.5-flash", custom_llm_provider="vertex_ai", model_info=model_info
)
assert prompt_cost == pytest.approx(1000 * expected_rate)
def test_batch_cost_calculator_bills_long_context_cached_tokens_at_the_standard_long_context_cache_price() -> None:
model_info: Final[ModelInfo] = {
"input_cost_per_token": 1.25e-6,
"input_cost_per_token_above_200k_tokens": 2.5e-6,
"cache_read_input_token_cost": 1.25e-7,
"cache_read_input_token_cost_above_200k_tokens": 2.5e-7,
"input_cost_per_token_batches": 6.25e-7,
"input_cost_per_token_above_200k_tokens_batches": 1.25e-6,
}
usage: Final = Usage(
prompt_tokens=300_000,
completion_tokens=0,
total_tokens=300_000,
prompt_tokens_details=PromptTokensDetailsWrapper(cached_tokens=300_000),
)
prompt_cost, _ = batch_cost_calculator(
usage=usage, model="gemini-2.5-pro", custom_llm_provider="vertex_ai", model_info=model_info
)
assert prompt_cost == pytest.approx(300_000 * 2.5e-7)
def _batch_and_standard_cached_audio_cost(model: str, provider: str) -> tuple[str, float, float]:
batch_prompt_cost, _ = batch_cost_calculator(_cached_audio_batch_usage(), model, provider)
standard_prompt_cost, _ = litellm.cost_per_token(
model=model, custom_llm_provider=provider, usage_object=_cached_audio_batch_usage()
)
return model, batch_prompt_cost, standard_prompt_cost
def test_batch_cached_audio_never_costs_more_than_standard_on_any_row_with_a_batch_input_price(
_local_model_cost_map: None,
) -> None:
rows: Final = tuple(
(name, entry["litellm_provider"])
for name, entry in litellm.model_cost.items()
if isinstance(entry, dict)
and entry.get("input_cost_per_token_batches") is not None
and entry.get("cache_read_input_audio_token_cost") is not None
)
assert rows, "no cost map row prices both Batch input and Standard cached audio"
costs: Final = tuple(_batch_and_standard_cached_audio_cost(name, provider) for name, provider in rows)
overpriced: Final = tuple(cost for cost in costs if cost[1] > cost[2])
assert overpriced == ()
@pytest.mark.parametrize(
("image_batch_rate", "image_tokens", "expected_completion_cost"),
(
@ -6254,7 +6351,7 @@ def test_batch_cost_calculator_bills_cached_tokens_at_the_flat_batch_cached_rate
assert prompt_cost == pytest.approx(100 * 1e-6 + 900 * 1e-7)
def test_batch_cost_calculator_bills_cached_tokens_at_the_batch_input_rate_without_a_cached_batch_rate(
def test_batch_cost_calculator_bills_cached_tokens_at_the_standard_cache_rate_without_a_cached_batch_rate(
_tiered_batch_models: None,
) -> None:
from litellm.cost_calculator import batch_cost_calculator
@ -6263,7 +6360,7 @@ def test_batch_cost_calculator_bills_cached_tokens_at_the_batch_input_rate_witho
usage=_cached_usage(300_048, 300_045, 11), model=_FLAT_CACHE_BATCH_MODEL, custom_llm_provider="openai"
)
assert prompt_cost == pytest.approx(300_048 * 3e-6)
assert prompt_cost == pytest.approx(3 * 3e-6 + 300_045 * 2e-7)
def _cache_write_usage(prompt_tokens: int, cache_write_tokens: int, completion_tokens: int) -> Usage: