diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 19e3f624268..ad64ab1488b 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -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( diff --git a/tests/test_litellm/test_zero_cost_metric_selection.py b/tests/test_litellm/test_zero_cost_metric_selection.py new file mode 100644 index 00000000000..0d666d8daa4 --- /dev/null +++ b/tests/test_litellm/test_zero_cost_metric_selection.py @@ -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} + )