From 70e3f1653f9180a613a2a58a63b9e1b102dcbf58 Mon Sep 17 00:00:00 2001 From: Anuj7411 Date: Wed, 26 Aug 2026 19:06:44 +0530 Subject: [PATCH] fix(cost): bill non-Vertex TTS calls on raw character count, not whitespace-stripped completion_cost() stripped whitespace from the billed character count for every character-priced text-to-speech call, regardless of provider. That convention is specific to Vertex AI's character-counting rules; OpenAI, Azure, ElevenLabs, Groq, Minimax, and AWS Polly all bill on the raw string length. This undercounted cost for any TTS input containing spaces, which is effectively every real request. Gate the whitespace-stripping to the vertex_ai provider only, matching the same gate already used for chat/completion cost a few lines below. --- litellm/cost_calculator.py | 8 ++++- tests/local_testing/test_completion_cost.py | 33 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 8f7cd09d364..27b5da54224 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1411,7 +1411,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