fix(cost): treat a zero rate as a price when selecting a cost metric

`select_cost_metric_for_model` picked its metric by truthiness:

    if model_info.get("input_cost_per_character"):
        return "cost_per_character"
    elif model_info.get("input_cost_per_token"):
        return "cost_per_token"
    else:
        raise ValueError(f"Model {...} does not have ...")

A rate of 0 is a valid price -- it declares the model free -- but it is falsy,
so a free model fell through to the raise and reported that fields it had
explicitly set were missing:

    {"key": "my-free-tts", "input_cost_per_character": 0}
      -> ValueError: Model my-free-tts does not have 'input_cost_per_character'
         or 'input_cost_per_token'

The single caller is the speech/aspeech branch of `completion_cost`
(cost_calculator.py), so a TTS model priced at 0 could not be costed at all,
and the error was indistinguishable from a genuinely unpriced model.

Selection now tests presence with `is not None`. This matches `tier_rate` in
this same package, which already documents the convention: "A rate that is
explicitly present wins over the fallback, an explicit zero included, so a tier
can declare a token type free."

Zero-cost models are a supported concept elsewhere in the codebase (the
router's `_zero_cost_cache`, the auth layer's zero-cost budget bypass), so a
free TTS model is a configuration users can reasonably expect to work.

Behaviour is unchanged for priced models, for a model with both fields set
(character pricing still wins), and for a model with neither -- which still
raises, as does one that sets both to an explicit None.

Adds tests/test_litellm/test_zero_cost_metric_selection.py: 9 tests, 4 of which
fail without this change.
This commit is contained in:
Muhammad Hassaan 2026-08-30 02:54:57 +05:00
parent 5c034fda74
commit 416d15d67a
2 changed files with 71 additions and 2 deletions

View file

@ -137,10 +137,16 @@ def select_cost_metric_for_model(
"""
Select 'cost_per_character' if model_info has 'input_cost_per_character'
Select 'cost_per_token' if model_info has 'input_cost_per_token'
Presence is what selects the metric, not truthiness: a rate of ``0`` is a
valid price, declaring the model free. Testing truthiness treated such a
model as unpriced and raised below, reporting that fields it had explicitly
set were missing. This matches ``tier_rate`` in this package, where an
explicit zero already wins over the fallback.
"""
if model_info.get("input_cost_per_character"):
if model_info.get("input_cost_per_character") is not None:
return "cost_per_character"
elif model_info.get("input_cost_per_token"):
elif model_info.get("input_cost_per_token") is not None:
return "cost_per_token"
else:
raise ValueError(

View file

@ -0,0 +1,63 @@
"""Tests for cost-metric selection on models priced at zero.
`select_cost_metric_for_model` chose a metric by truthiness, so a rate of 0 --
a valid price declaring the model free -- was indistinguishable from an absent
one, and the speech cost path raised ValueError claiming fields the model had
explicitly set were missing.
"""
import pytest
from litellm.litellm_core_utils.llm_cost_calc.utils import (
select_cost_metric_for_model,
)
@pytest.mark.parametrize(
"model_info, expected",
[
({"key": "tts-1", "input_cost_per_token": 1.5e-05}, "cost_per_token"),
({"key": "tts-1", "input_cost_per_character": 1.5e-05}, "cost_per_character"),
# character pricing wins when both are present
(
{
"key": "tts-1",
"input_cost_per_character": 1.5e-05,
"input_cost_per_token": 3.0e-05,
},
"cost_per_character",
),
],
)
def test_priced_models_select_their_metric(model_info, expected):
assert select_cost_metric_for_model(model_info) == expected
@pytest.mark.parametrize(
"model_info, expected",
[
({"key": "free-tts", "input_cost_per_character": 0}, "cost_per_character"),
({"key": "free-tts", "input_cost_per_token": 0}, "cost_per_token"),
({"key": "free-tts", "input_cost_per_character": 0.0}, "cost_per_character"),
(
{"key": "free-tts", "input_cost_per_character": 0, "input_cost_per_token": 0},
"cost_per_character",
),
],
)
def test_zero_is_a_price_not_an_absent_field(model_info, expected):
# A zero rate declares the model free. Selecting on truthiness dropped
# through to the ValueError below, so a free TTS model could not be costed.
assert select_cost_metric_for_model(model_info) == expected
def test_missing_cost_fields_still_raise():
with pytest.raises(ValueError, match="does not have"):
select_cost_metric_for_model({"key": "unpriced-tts"})
def test_explicit_none_is_treated_as_absent():
with pytest.raises(ValueError, match="does not have"):
select_cost_metric_for_model(
{"key": "unpriced-tts", "input_cost_per_character": None, "input_cost_per_token": None}
)