diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 440d97d13be..82ada6dc9f0 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1496,7 +1496,13 @@ def completion_cost( video_resolution=video_resolution, ) elif call_type in _SPEECH_CALL_TYPES: - prompt_characters = litellm.utils._count_characters(text=prompt) + # Vertex AI bills TTS characters excluding whitespace; every other + # character-priced TTS provider (OpenAI, Azure, ElevenLabs, Groq, + # Minimax, AWS Polly) bills the raw character count. + if custom_llm_provider == "vertex_ai": + prompt_characters = litellm.utils._count_characters(text=prompt) + else: + prompt_characters = len(prompt) elif call_type in _TRANSCRIPTION_CALL_TYPES: # Check _hidden_params first (duration stored there to # avoid polluting the response body), then fall back to diff --git a/tests/local_testing/test_completion_cost.py b/tests/local_testing/test_completion_cost.py index f47b40f2ef1..f3a9d467ae8 100644 --- a/tests/local_testing/test_completion_cost.py +++ b/tests/local_testing/test_completion_cost.py @@ -863,6 +863,39 @@ def test_completion_cost_tts(model): assert cost > 0 +@pytest.mark.parametrize("model", ["tts-1", "azure/tts-1"]) +def test_completion_cost_tts_bills_whitespace_characters(model): + """ + Non-Vertex TTS providers (OpenAI, Azure, ElevenLabs, Groq, Minimax, AWS Polly) + bill on the raw character count, including whitespace. Only Vertex AI's + character-counting convention excludes whitespace. Regression test for a bug + where every TTS provider incorrectly had whitespace stripped before billing, + undercounting cost for inputs containing spaces. + """ + os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" + litellm.model_cost = litellm.get_model_cost_map(url="") + + prompt_with_whitespace = "a b c d e" # 9 chars, 4 of them whitespace + prompt_without_whitespace = "abcde" # 5 chars, matches non-whitespace count above + + cost_with_whitespace = completion_cost( + model=model, + prompt=prompt_with_whitespace, + call_type="speech", + ) + cost_without_whitespace = completion_cost( + model=model, + prompt=prompt_without_whitespace, + call_type="speech", + ) + + input_cost_per_character = litellm.model_cost[model]["input_cost_per_character"] + + assert cost_with_whitespace == pytest.approx(len(prompt_with_whitespace) * input_cost_per_character) + # billing must scale with the raw string length, not the whitespace-stripped length + assert cost_with_whitespace > cost_without_whitespace + + def test_completion_cost_anthropic(): """ model_name: claude-3-haiku-20240307