diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index 4c77f7e5c7c..efdaa8d5b83 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -668,29 +668,18 @@ def generic_cost_per_token( # noqa: PLR0915 image_tokens = prompt_tokens_details["image_tokens"] # Check for double-counting: sum of details > prompt_tokens means overlap - accounted_tokens = ( - text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens - ) + accounted_tokens = text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens has_double_counting = cache_hit > 0 and accounted_tokens > usage.prompt_tokens - - # Some models use alternative billing dimensions (image_count, character_count, - # video_length_seconds) that account for prompt_tokens without being included - # in the per-token detail fields. When these are active, a gap between - # accounted_tokens and prompt_tokens is expected and must NOT be filled. - has_image_count_billing = prompt_tokens_details["image_count"] > 0 + + # Some models use alternative billing dimensions (character_count, video_length_seconds) + # that account for prompt_tokens without being included in the per-token detail fields. + # When these are active, a gap between accounted_tokens and prompt_tokens is expected + # and must NOT be filled. Note: image_count is additive billing, not alternative. has_non_token_alternative_billing = ( prompt_tokens_details["character_count"] > 0 or prompt_tokens_details["video_length_seconds"] > 0 ) - has_alternative_billing = ( - has_image_count_billing or has_non_token_alternative_billing - ) - should_fill_unaccounted_tokens = ( - accounted_tokens < usage.prompt_tokens - and not has_non_token_alternative_billing - and not has_image_count_billing - ) - + if has_double_counting: # Double-counting fix (xAI etc.): recalculate text_tokens from scratch # Clamp to 0 to prevent negative cost when cache_hit exceeds prompt_tokens @@ -703,30 +692,23 @@ def generic_cost_per_token( # noqa: PLR0915 - image_tokens, ) prompt_tokens_details["text_tokens"] = text_tokens - elif text_tokens == 0 and not has_alternative_billing: - # text_tokens not set by provider and no alternative billing dimensions: - # calculate text_tokens as the remainder of prompt_tokens - text_tokens = max( - 0, - usage.prompt_tokens - - cache_hit - - audio_tokens - - cache_creation - - image_tokens, - ) - prompt_tokens_details["text_tokens"] = text_tokens - elif should_fill_unaccounted_tokens: + elif accounted_tokens < usage.prompt_tokens and not has_non_token_alternative_billing: # Unaccounted tokens fix: inline documents (PDF, DOCX, etc.) are counted # in prompt_tokens by the provider but not broken out into any detail field. # Add the gap to text_tokens so they are costed at input_cost_per_token. # - # The guard above skips filling when ANY alternative pricing dimension is - # present (character_count, video_length_seconds, or image_count), since - # the gap may represent tokens already billed through those dimensions - # and converting them to text_tokens would cause double-billing. + # This handles both cases: + # 1. text_tokens=0 (provider didn't set it) - gap fills the entire remainder + # 2. text_tokens>0 but < total (mixed content like PDF+text) - gap fills remainder + # + # Skip filling only when non-token alternative billing is active + # (character_count, video_length_seconds), since those dimensions + # replace token-based billing entirely. image_count is additive and should + # not prevent gap-filling for unaccounted document tokens. unaccounted_tokens = usage.prompt_tokens - accounted_tokens prompt_tokens_details["text_tokens"] += unaccounted_tokens - + + ( prompt_base_cost, completion_base_cost, @@ -736,7 +718,7 @@ def generic_cost_per_token( # noqa: PLR0915 ) = _get_token_base_cost( model_info=model_info, usage=usage, service_tier=service_tier ) - + prompt_cost = _calculate_input_cost( prompt_tokens_details=prompt_tokens_details, model_info=model_info, @@ -746,7 +728,6 @@ def generic_cost_per_token( # noqa: PLR0915 cache_creation_cost_above_1hr=cache_creation_cost_above_1hr, service_tier=service_tier, ) - ## CALCULATE OUTPUT COST text_tokens = 0 audio_tokens = 0