diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index a5dddf4e666..73c8063fdae 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -375,26 +375,28 @@ def _get_max_input_tokens_for_cost_fallback( return None -def _usage_has_token_counts(usage_object: Optional[Usage]) -> bool: +def _usage_has_prompt_token_counts(usage_object: Optional[Usage]) -> bool: if usage_object is None: return False - for attr in ("prompt_tokens", "completion_tokens", "total_tokens"): - if _is_positive_finite_number(getattr(usage_object, attr, 0)): - return True + if _is_positive_finite_number(getattr(usage_object, "prompt_tokens", 0)): + return True prompt_details = getattr(usage_object, "prompt_tokens_details", None) if prompt_details is not None: - for attr in ("audio_tokens", "cached_tokens", "text_tokens"): + for attr in ( + "audio_tokens", + "cached_tokens", + "cache_creation_tokens", + "character_count", + "image_count", + "image_tokens", + "text_tokens", + "video_length_seconds", + ): if _is_positive_finite_number(getattr(prompt_details, attr, 0)): return True - completion_details = getattr(usage_object, "completion_tokens_details", None) - if completion_details is not None: - for attr in ("audio_tokens", "reasoning_tokens", "text_tokens"): - if _is_positive_finite_number(getattr(completion_details, attr, 0)): - return True - return False @@ -1684,7 +1686,7 @@ def completion_cost( # noqa: PLR0915 if ( call_type in _CHAT_COMPLETION_CALL_TYPES - and not _usage_has_token_counts(cost_per_token_usage_object) + and not _usage_has_prompt_token_counts(cost_per_token_usage_object) ): # Video metadata is client-provided. When provider usage is # missing, keep spend reconciliation conservative. diff --git a/tests/local_testing/test_completion_cost.py b/tests/local_testing/test_completion_cost.py index 3ca4ad3461c..a5e1afccc61 100644 --- a/tests/local_testing/test_completion_cost.py +++ b/tests/local_testing/test_completion_cost.py @@ -1031,6 +1031,69 @@ def test_completion_cost_uses_conservative_video_fallback_without_usage(): assert cost == pytest.approx(max_input_tokens * input_cost_per_token) +@pytest.mark.parametrize( + "usage,expected_completion_tokens", + [ + ({"total_tokens": 5}, 0), + ({"completion_tokens": 3, "total_tokens": 3}, 3), + ], +) +def test_completion_cost_uses_video_fallback_without_prompt_usage( + usage, + expected_completion_tokens, +): + model = "openai/test-video-cost-fallback-no-prompt-usage" + input_cost_per_token = 0.25 + output_cost_per_token = 0.5 + max_input_tokens = 16 + litellm.register_model( + model_cost={ + model: { + "input_cost_per_token": input_cost_per_token, + "output_cost_per_token": output_cost_per_token, + "max_tokens": max_input_tokens, + "max_input_tokens": max_input_tokens, + "max_output_tokens": 4, + "litellm_provider": "openai", + "mode": "chat", + } + } + ) + messages = [ + { + "role": "user", + "content": [ + { + "type": "video_url", + "video_url": { + "url": "https://example.com/video.mp4", + "video_metadata": { + "duration_seconds": 0, + "fps": 0, + "has_audio": False, + }, + }, + } + ], + } + ] + + try: + cost = completion_cost( + completion_response={"model": model, "usage": usage}, + model=model, + messages=messages, + custom_llm_provider="openai", + ) + finally: + litellm.model_cost.pop(model, None) + + assert cost == pytest.approx( + (max_input_tokens * input_cost_per_token) + + (expected_completion_tokens * output_cost_per_token) + ) + + @pytest.mark.parametrize("metadata_key", ["metadata", "litellm_metadata"]) def test_completion_cost_ignores_client_metadata_for_video_fallback_limit( metadata_key,