diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index e5977ca4156..c6922a4dcce 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -922,7 +922,13 @@ def _calculate_input_cost( ### AUDIO COST if prompt_tokens_details["audio_tokens"]: - audio_cost_key: Final = _get_service_tier_cost_key("input_cost_per_audio_token", service_tier) + tier_audio_cost_key: Final = _get_service_tier_cost_key("input_cost_per_audio_token", service_tier) + has_audio_price: Final = ( + model_info.get(tier_audio_cost_key) is not None + or model_info.get("input_cost_per_audio_token") is not None + or model_info.get("input_cost_per_audio_per_second") is not None + ) + audio_cost_key: Final = tier_audio_cost_key if has_audio_price else "input_cost_per_token" prompt_cost += calculate_cost_component(model_info, audio_cost_key, prompt_tokens_details["audio_tokens"]) ### IMAGE TOKEN COST diff --git a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py index cbe6fe198c9..17ae322c8c9 100644 --- a/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py +++ b/tests/test_litellm/litellm_core_utils/llm_cost_calc/test_llm_cost_calc_utils.py @@ -2525,6 +2525,59 @@ def test_string_cost_values_edge_cases(): assert round(completion_cost, 12) == round(expected_completion_cost, 12) +def test_audio_input_tokens_fall_back_to_base_rate_when_no_audio_price(): + """Audio input tokens must bill at input_cost_per_token when the model has no audio rate. + + Regression: models like gemini-2.5-pro report audio prompt tokens separately from text + but ship without input_cost_per_audio_token, which previously billed those tokens at $0. + """ + model_info: ModelInfo = { + "input_cost_per_token": 1.25e-6, + "output_cost_per_token": 1e-5, + } + + usage = Usage( + prompt_tokens=9700, + completion_tokens=0, + total_tokens=9700, + prompt_tokens_details=PromptTokensDetailsWrapper( + audio_tokens=9600, cached_tokens=0, text_tokens=100, image_tokens=None + ), + ) + + prompt_cost, _ = generic_cost_per_token( + model="gemini-2.5-pro", usage=usage, custom_llm_provider="vertex_ai", model_info=model_info + ) + + expected_prompt_cost = (100 + 9600) * 1.25e-6 + assert round(prompt_cost, 12) == round(expected_prompt_cost, 12) + + +def test_audio_input_tokens_use_audio_rate_when_present(): + """When input_cost_per_audio_token exists it takes precedence over the base rate.""" + model_info: ModelInfo = { + "input_cost_per_token": 1.25e-6, + "input_cost_per_audio_token": 5e-6, + "output_cost_per_token": 1e-5, + } + + usage = Usage( + prompt_tokens=9700, + completion_tokens=0, + total_tokens=9700, + prompt_tokens_details=PromptTokensDetailsWrapper( + audio_tokens=9600, cached_tokens=0, text_tokens=100, image_tokens=None + ), + ) + + prompt_cost, _ = generic_cost_per_token( + model="gemini-2.5-pro", usage=usage, custom_llm_provider="vertex_ai", model_info=model_info + ) + + expected_prompt_cost = 100 * 1.25e-6 + 9600 * 5e-6 + assert round(prompt_cost, 12) == round(expected_prompt_cost, 12) + + def test_string_cost_values_with_threshold(): """Test that string cost values work correctly with threshold pricing.""" from unittest.mock import patch