diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 814eaaf76f7..5d8e471abaa 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,14 +378,17 @@ def cost_per_token( if model is None: raise Exception("Invalid arg. Model cannot be none.") + 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, ) @@ -418,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, diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index f610821e06a..ef4dc002b58 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -4768,3 +4768,30 @@ 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 + + +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