From 45be10a8e4802b7081f1f0caffa17673cc7d43e2 Mon Sep 17 00:00:00 2001 From: soroush5 Date: Thu, 3 Sep 2026 23:16:48 +0330 Subject: [PATCH 1/5] fix(cost): coerce None/string token counts in cost_per_token --- litellm/cost_calculator.py | 14 ++++++++++++++ tests/test_litellm/test_cost_calculator.py | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 814eaaf76f7..0be166310e0 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -310,6 +310,17 @@ def _transcription_usage_has_token_details( return (prompt_tokens_val > 0) or (completion_tokens_val > 0) +def _coerce_token_count(value: float | str | None) -> int: + if value is None: + return 0 + if isinstance(value, str): + try: + return int(value) + except ValueError: + return 0 + return int(value) + + def cost_per_token( model: str = "", prompt_tokens: int = 0, @@ -367,6 +378,9 @@ def cost_per_token( if model is None: raise Exception("Invalid arg. Model cannot be none.") + prompt_tokens = _coerce_token_count(prompt_tokens) + completion_tokens = _coerce_token_count(completion_tokens) + ## RECONSTRUCT USAGE BLOCK ## if usage_object is not None: usage_block = usage_object diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index f610821e06a..293ca9ac398 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -4768,3 +4768,20 @@ def test_collect_and_combine_realtime_usage_stores_partitioned_text_tokens() -> assert combined.completion_tokens_details.reasoning_tokens == 95 assert combined.completion_tokens_details.text_tokens == 38 assert combined.completion_tokens_details.audio_tokens == 0 +def test_cost_per_token_none_token_counts_cost_zero(): + prompt_cost, completion_cost = cost_per_token( + model="gpt-4o-mini", prompt_tokens=None, completion_tokens=None + ) + assert prompt_cost == 0.0 + assert completion_cost == 0.0 + + +def test_cost_per_token_string_token_counts_coerced(): + prompt_cost, completion_cost = cost_per_token( + model="gpt-4o-mini", prompt_tokens="10", completion_tokens=5 + ) + expected_prompt, expected_completion = cost_per_token( + model="gpt-4o-mini", prompt_tokens=10, completion_tokens=5 + ) + assert prompt_cost == expected_prompt + assert completion_cost == expected_completion From ab3f12410e4a2b57d6e193432d1bb0c184b9ee45 Mon Sep 17 00:00:00 2001 From: soroush5 Date: Thu, 3 Sep 2026 23:24:25 +0330 Subject: [PATCH 2/5] fix(cost): avoid parameter reassignment in token coercion --- litellm/cost_calculator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 0be166310e0..64fc255d6d3 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -378,17 +378,17 @@ def cost_per_token( if model is None: raise Exception("Invalid arg. Model cannot be none.") - prompt_tokens = _coerce_token_count(prompt_tokens) - completion_tokens = _coerce_token_count(completion_tokens) + coerced_prompt_tokens = _coerce_token_count(prompt_tokens) + coerced_completion_tokens = _coerce_token_count(completion_tokens) ## RECONSTRUCT USAGE BLOCK ## if usage_object is not None: usage_block = usage_object else: usage_block = Usage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - total_tokens=prompt_tokens + completion_tokens, + prompt_tokens=coerced_prompt_tokens, + completion_tokens=coerced_completion_tokens, + total_tokens=coerced_prompt_tokens + coerced_completion_tokens, cache_creation_input_tokens=cache_creation_input_tokens, cache_read_input_tokens=cache_read_input_tokens, ) From cc2d9f10df77bb79b6fd2815daa3a53a6ef7719a Mon Sep 17 00:00:00 2001 From: soroush5 Date: Thu, 3 Sep 2026 23:25:47 +0330 Subject: [PATCH 3/5] fix(cost): coerce at parameter so all downstream uses see clean counts --- litellm/cost_calculator.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 64fc255d6d3..0be166310e0 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -378,17 +378,17 @@ def cost_per_token( if model is None: raise Exception("Invalid arg. Model cannot be none.") - coerced_prompt_tokens = _coerce_token_count(prompt_tokens) - coerced_completion_tokens = _coerce_token_count(completion_tokens) + prompt_tokens = _coerce_token_count(prompt_tokens) + completion_tokens = _coerce_token_count(completion_tokens) ## RECONSTRUCT USAGE BLOCK ## if usage_object is not None: usage_block = usage_object else: usage_block = Usage( - prompt_tokens=coerced_prompt_tokens, - completion_tokens=coerced_completion_tokens, - total_tokens=coerced_prompt_tokens + coerced_completion_tokens, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=prompt_tokens + completion_tokens, cache_creation_input_tokens=cache_creation_input_tokens, cache_read_input_tokens=cache_read_input_tokens, ) From 5573c446fcb22f0a14f0c72e2e84a5acadd188fe Mon Sep 17 00:00:00 2001 From: soroush5 Date: Thu, 3 Sep 2026 23:39:50 +0330 Subject: [PATCH 4/5] test(cost): cover garbage-string token counts --- tests/test_litellm/test_cost_calculator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 293ca9ac398..ef4dc002b58 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -4785,3 +4785,13 @@ def test_cost_per_token_string_token_counts_coerced(): ) assert prompt_cost == expected_prompt assert completion_cost == expected_completion + + +def test_cost_per_token_garbage_string_counts_cost_zero(): + prompt_cost, completion_cost = cost_per_token( + model="gpt-4o-mini", prompt_tokens="abc", completion_tokens=5 + ) + expected_prompt, _ = cost_per_token( + model="gpt-4o-mini", prompt_tokens=0, completion_tokens=5 + ) + assert prompt_cost == expected_prompt From d7295c15a30d78096dcdebc779085fc31d6cba3b Mon Sep 17 00:00:00 2001 From: soroush5 Date: Fri, 4 Sep 2026 12:53:32 +0330 Subject: [PATCH 5/5] fix(cost): avoid parameter rebinding for type-discipline gate --- litellm/cost_calculator.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 0be166310e0..5d8e471abaa 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -378,17 +378,17 @@ def cost_per_token( if model is None: raise Exception("Invalid arg. Model cannot be none.") - prompt_tokens = _coerce_token_count(prompt_tokens) - completion_tokens = _coerce_token_count(completion_tokens) + prompt_token_count = _coerce_token_count(prompt_tokens) + completion_token_count = _coerce_token_count(completion_tokens) ## RECONSTRUCT USAGE BLOCK ## if usage_object is not None: usage_block = usage_object else: usage_block = Usage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - total_tokens=prompt_tokens + completion_tokens, + prompt_tokens=prompt_token_count, + completion_tokens=completion_token_count, + total_tokens=prompt_token_count + completion_token_count, cache_creation_input_tokens=cache_creation_input_tokens, cache_read_input_tokens=cache_read_input_tokens, ) @@ -432,13 +432,13 @@ def cost_per_token( # Anthropic reports prompt_tokens as input_tokens (excluding cache tokens). # Adjust so the helper's "prompt_tokens includes cache tokens" invariant holds. - _normalized_prompt_tokens = float(prompt_tokens) + _normalized_prompt_tokens = float(prompt_token_count) if _is_anthropic_style: _normalized_prompt_tokens += _cache_read_tokens + _cache_creation_tokens response_cost: Final = _cost_per_token_custom_pricing_helper( prompt_tokens=_normalized_prompt_tokens, - completion_tokens=completion_tokens, + completion_tokens=completion_token_count, response_time_ms=response_time_ms, cached_tokens=_cache_read_tokens, cache_creation_tokens=_cache_creation_tokens,