diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 6354bf44943..265aa629ba4 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -28,6 +28,7 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import ( _parse_prompt_tokens_details, calculate_cost_component, generic_cost_per_token, + generic_cost_per_token_with_breakdown, get_billable_input_tokens, select_cost_metric_for_model, ) @@ -941,6 +942,22 @@ def _store_cost_breakdown_in_logging_obj( margin_percent: Optional[float] = None, margin_fixed_amount: Optional[float] = None, margin_total_amount: Optional[float] = None, + input_cost_text: Optional[float] = None, + input_cost_cache_read: Optional[float] = None, + input_cost_cache_creation: Optional[float] = None, + input_cost_audio: Optional[float] = None, + output_cost_text: Optional[float] = None, + output_cost_reasoning: Optional[float] = None, + output_cost_audio: Optional[float] = None, + input_tokens_text: Optional[int] = None, + input_tokens_cache_read: Optional[int] = None, + input_tokens_cache_creation: Optional[int] = None, + input_tokens_audio: Optional[int] = None, + output_tokens_text: Optional[int] = None, + output_tokens_reasoning: Optional[int] = None, + output_tokens_audio: Optional[int] = None, + above_128k_tokens: Optional[bool] = None, + above_200k_tokens: Optional[bool] = None, ) -> None: """ Helper function to store cost breakdown in the logging object. @@ -958,12 +975,22 @@ def _store_cost_breakdown_in_logging_obj( margin_percent: Margin percentage applied (0.10 = 10%) margin_fixed_amount: Fixed margin amount in USD margin_total_amount: Total margin added in USD + input_cost_text: Cost for non-cached text input tokens + input_cost_cache_read: Cost for cache read/hit tokens + input_cost_cache_creation: Cost for cache creation/write tokens + input_cost_audio: Cost for audio input tokens + output_cost_text: Cost for text output tokens + output_cost_reasoning: Cost for reasoning output tokens + output_cost_audio: Cost for audio output tokens + input_tokens_*: Token counts for each input category + output_tokens_*: Token counts for each output category + above_128k_tokens: Whether above-128K token pricing was used + above_200k_tokens: Whether above-200K token pricing was used """ if litellm_logging_obj is None: return try: - # Store the cost breakdown litellm_logging_obj.set_cost_breakdown( input_cost=prompt_tokens_cost_usd_dollar, output_cost=completion_tokens_cost_usd_dollar, @@ -976,11 +1003,26 @@ def _store_cost_breakdown_in_logging_obj( margin_percent=margin_percent, margin_fixed_amount=margin_fixed_amount, margin_total_amount=margin_total_amount, + input_cost_text=input_cost_text, + input_cost_cache_read=input_cost_cache_read, + input_cost_cache_creation=input_cost_cache_creation, + input_cost_audio=input_cost_audio, + output_cost_text=output_cost_text, + output_cost_reasoning=output_cost_reasoning, + output_cost_audio=output_cost_audio, + input_tokens_text=input_tokens_text, + input_tokens_cache_read=input_tokens_cache_read, + input_tokens_cache_creation=input_tokens_cache_creation, + input_tokens_audio=input_tokens_audio, + output_tokens_text=output_tokens_text, + output_tokens_reasoning=output_tokens_reasoning, + output_tokens_audio=output_tokens_audio, + above_128k_tokens=above_128k_tokens, + above_200k_tokens=above_200k_tokens, ) except Exception as breakdown_error: verbose_logger.debug(f"Error storing cost breakdown: {str(breakdown_error)}") - # Don't fail the main cost calculation if breakdown storage fails pass @@ -1535,6 +1577,49 @@ def completion_cost( # noqa: PLR0915 margin_fixed_amount = 0.0 margin_total_amount = 0.0 + # Compute granular breakdown if we have the usage object + _granular_kwargs: dict = {} + verbose_logger.debug( + "Granular breakdown check: usage_obj=%s, provider=%s, model=%s", + type(cost_per_token_usage_object).__name__ if cost_per_token_usage_object else None, + custom_llm_provider, + model, + ) + if ( + cost_per_token_usage_object is not None + and custom_llm_provider is not None + ): + try: + _breakdown = generic_cost_per_token_with_breakdown( + model=model, + usage=cost_per_token_usage_object, + custom_llm_provider=custom_llm_provider, + service_tier=service_tier, + ) + _granular_kwargs = dict( + input_cost_text=_breakdown["input_breakdown"]["text_cost"], + input_cost_cache_read=_breakdown["input_breakdown"]["cache_read_cost"], + input_cost_cache_creation=_breakdown["input_breakdown"]["cache_creation_cost"], + input_cost_audio=_breakdown["input_breakdown"]["audio_cost"], + output_cost_text=_breakdown["output_breakdown"]["text_cost"], + output_cost_reasoning=_breakdown["output_breakdown"]["reasoning_cost"], + output_cost_audio=_breakdown["output_breakdown"]["audio_cost"], + input_tokens_text=_breakdown["prompt_tokens_details"]["text_tokens"], + input_tokens_cache_read=_breakdown["prompt_tokens_details"]["cache_hit_tokens"], + input_tokens_cache_creation=_breakdown["prompt_tokens_details"]["cache_creation_tokens"], + input_tokens_audio=_breakdown["prompt_tokens_details"]["audio_tokens"], + output_tokens_text=_breakdown["completion_tokens_details_parsed"]["text_tokens"], + output_tokens_reasoning=_breakdown["completion_tokens_details_parsed"]["reasoning_tokens"], + output_tokens_audio=_breakdown["completion_tokens_details_parsed"]["audio_tokens"], + above_128k_tokens=_breakdown["above_128k_tokens"], + above_200k_tokens=_breakdown["above_200k_tokens"], + ) + except Exception as _granular_err: + verbose_logger.debug( + "Error computing granular cost breakdown: %s", + str(_granular_err), + ) + # Store cost breakdown in logging object if available if litellm_logging_obj is not None: _store_cost_breakdown_in_logging_obj( @@ -1550,6 +1635,7 @@ def completion_cost( # noqa: PLR0915 margin_percent=margin_percent, margin_fixed_amount=margin_fixed_amount, margin_total_amount=margin_total_amount, + **_granular_kwargs, ) return _final_cost diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 6f587abcdf1..0089f002b3f 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1322,6 +1322,22 @@ class Logging(LiteLLMLoggingBaseClass): margin_percent: Optional[float] = None, margin_fixed_amount: Optional[float] = None, margin_total_amount: Optional[float] = None, + input_cost_text: Optional[float] = None, + input_cost_cache_read: Optional[float] = None, + input_cost_cache_creation: Optional[float] = None, + input_cost_audio: Optional[float] = None, + output_cost_text: Optional[float] = None, + output_cost_reasoning: Optional[float] = None, + output_cost_audio: Optional[float] = None, + input_tokens_text: Optional[int] = None, + input_tokens_cache_read: Optional[int] = None, + input_tokens_cache_creation: Optional[int] = None, + input_tokens_audio: Optional[int] = None, + output_tokens_text: Optional[int] = None, + output_tokens_reasoning: Optional[int] = None, + output_tokens_audio: Optional[int] = None, + above_128k_tokens: Optional[bool] = None, + above_200k_tokens: Optional[bool] = None, ) -> None: """ Helper method to store cost breakdown in the logging object. @@ -1331,13 +1347,24 @@ class Logging(LiteLLMLoggingBaseClass): output_cost: Cost of output/completion tokens cost_for_built_in_tools_cost_usd_dollar: Cost of built-in tools total_cost: Total cost of request - additional_costs: Free-form additional costs dict (e.g., {"azure_model_router_flat_cost": 0.00014}) + additional_costs: Free-form additional costs dict original_cost: Cost before discount discount_percent: Discount percentage (0.05 = 5%) discount_amount: Discount amount in USD margin_percent: Margin percentage applied (0.10 = 10%) margin_fixed_amount: Fixed margin amount in USD margin_total_amount: Total margin added in USD + input_cost_text: Cost for non-cached text input tokens + input_cost_cache_read: Cost for cache read/hit tokens + input_cost_cache_creation: Cost for cache creation/write tokens + input_cost_audio: Cost for audio input tokens + output_cost_text: Cost for text output tokens + output_cost_reasoning: Cost for reasoning output tokens + output_cost_audio: Cost for audio output tokens + input_tokens_*: Token counts for each input category + output_tokens_*: Token counts for each output category + above_128k_tokens: Whether above-128K token pricing was used + above_200k_tokens: Whether above-200K token pricing was used """ self.cost_breakdown = CostBreakdown( @@ -1371,6 +1398,129 @@ class Logging(LiteLLMLoggingBaseClass): if margin_total_amount is not None: self.cost_breakdown["margin_total_amount"] = margin_total_amount + # Store granular input cost breakdown + if input_cost_text is not None: + self.cost_breakdown["input_cost_text"] = input_cost_text + if input_cost_cache_read is not None: + self.cost_breakdown["input_cost_cache_read"] = input_cost_cache_read + if input_cost_cache_creation is not None: + self.cost_breakdown["input_cost_cache_creation"] = input_cost_cache_creation + if input_cost_audio is not None: + self.cost_breakdown["input_cost_audio"] = input_cost_audio + + # Store granular output cost breakdown + if output_cost_text is not None: + self.cost_breakdown["output_cost_text"] = output_cost_text + if output_cost_reasoning is not None: + self.cost_breakdown["output_cost_reasoning"] = output_cost_reasoning + if output_cost_audio is not None: + self.cost_breakdown["output_cost_audio"] = output_cost_audio + + # Store token counts + if input_tokens_text is not None: + self.cost_breakdown["input_tokens_text"] = input_tokens_text + if input_tokens_cache_read is not None: + self.cost_breakdown["input_tokens_cache_read"] = input_tokens_cache_read + if input_tokens_cache_creation is not None: + self.cost_breakdown["input_tokens_cache_creation"] = input_tokens_cache_creation + if input_tokens_audio is not None: + self.cost_breakdown["input_tokens_audio"] = input_tokens_audio + if output_tokens_text is not None: + self.cost_breakdown["output_tokens_text"] = output_tokens_text + if output_tokens_reasoning is not None: + self.cost_breakdown["output_tokens_reasoning"] = output_tokens_reasoning + if output_tokens_audio is not None: + self.cost_breakdown["output_tokens_audio"] = output_tokens_audio + + # Store pricing tier indicators + if above_128k_tokens is not None: + self.cost_breakdown["above_128k_tokens"] = above_128k_tokens + if above_200k_tokens is not None: + self.cost_breakdown["above_200k_tokens"] = above_200k_tokens + + def _populate_granular_cost_breakdown(self, result: Any) -> None: + """ + Compute and store granular cost breakdown (cache read/write, reasoning, + web search, etc.) when it wasn't set by completion_cost (e.g., when + response_cost was pre-computed in _hidden_params). + """ + try: + from litellm.litellm_core_utils.llm_cost_calc.utils import ( + generic_cost_per_token_with_breakdown, + ) + from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( + StandardBuiltInToolCostTracking, + ) + from litellm.responses.utils import ResponseAPILoggingUtils + + usage_obj = getattr(result, "usage", None) + if usage_obj is None: + return + + # Ensure usage is in the chat usage format (with prompt_tokens) + if isinstance(usage_obj, dict): + if ResponseAPILoggingUtils._is_response_api_usage(usage_obj): + usage_obj = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(usage_obj) + elif "prompt_tokens" not in usage_obj: + return + elif hasattr(usage_obj, "input_tokens") and not hasattr(usage_obj, "prompt_tokens"): + if ResponseAPILoggingUtils._is_response_api_usage(usage_obj): + usage_obj = ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(usage_obj) + else: + return + + if not hasattr(usage_obj, "prompt_tokens"): + return + + custom_llm_provider = self.model_call_details.get("custom_llm_provider") + model = self.model + if custom_llm_provider is None or model is None: + return + + breakdown = generic_cost_per_token_with_breakdown( + model=model, + usage=usage_obj, + custom_llm_provider=custom_llm_provider, + ) + + # Compute tool costs + tool_cost = StandardBuiltInToolCostTracking.get_cost_for_built_in_tools( + model=model, + response_object=result, + usage=usage_obj, + standard_built_in_tools_params=self.standard_built_in_tools_params, + custom_llm_provider=custom_llm_provider, + ) + + total_cost = breakdown["prompt_cost"] + breakdown["completion_cost"] + tool_cost + + self.set_cost_breakdown( + input_cost=breakdown["prompt_cost"], + output_cost=breakdown["completion_cost"], + total_cost=total_cost, + cost_for_built_in_tools_cost_usd_dollar=tool_cost, + input_cost_text=breakdown["input_breakdown"]["text_cost"], + input_cost_cache_read=breakdown["input_breakdown"]["cache_read_cost"], + input_cost_cache_creation=breakdown["input_breakdown"]["cache_creation_cost"], + input_cost_audio=breakdown["input_breakdown"]["audio_cost"], + output_cost_text=breakdown["output_breakdown"]["text_cost"], + output_cost_reasoning=breakdown["output_breakdown"]["reasoning_cost"], + output_cost_audio=breakdown["output_breakdown"]["audio_cost"], + input_tokens_text=breakdown["prompt_tokens_details"]["text_tokens"], + input_tokens_cache_read=breakdown["prompt_tokens_details"]["cache_hit_tokens"], + input_tokens_cache_creation=breakdown["prompt_tokens_details"]["cache_creation_tokens"], + input_tokens_audio=breakdown["prompt_tokens_details"]["audio_tokens"], + output_tokens_text=breakdown["completion_tokens_details_parsed"]["text_tokens"], + output_tokens_reasoning=breakdown["completion_tokens_details_parsed"]["reasoning_tokens"], + output_tokens_audio=breakdown["completion_tokens_details_parsed"]["audio_tokens"], + above_128k_tokens=breakdown["above_128k_tokens"], + above_200k_tokens=breakdown["above_200k_tokens"], + ) + except Exception as e: + verbose_logger.debug( + "Failed to populate granular cost breakdown: %s", str(e) + ) + def _response_cost_calculator( self, result: Union[ @@ -1652,6 +1802,10 @@ class Logging(LiteLLMLoggingBaseClass): result=logging_result ) + # Populate granular cost breakdown if not already set by completion_cost + if self.cost_breakdown is None: + self._populate_granular_cost_breakdown(logging_result) + self.model_call_details["standard_logging_object"] = ( self._build_standard_logging_payload(logging_result, start_time, end_time) ) @@ -5210,6 +5364,21 @@ def _extract_response_obj_and_hidden_params( return response_obj, hidden_params +def _compute_cost_breakdown_fallback( + logging_obj: "Logging", response_obj: Any +) -> Optional[CostBreakdown]: + """ + Compute granular cost breakdown when it wasn't set by the completion_cost + path (e.g., when response_cost was pre-computed in _hidden_params). + """ + try: + logging_obj._populate_granular_cost_breakdown(response_obj) + return logging_obj.cost_breakdown + except Exception as e: + verbose_logger.debug("_compute_cost_breakdown_fallback failed: %s", str(e)) + return None + + def get_standard_logging_object_payload( kwargs: Optional[dict], init_response_obj: Union[Any, BaseModel, dict], @@ -5374,7 +5543,7 @@ def get_standard_logging_object_payload( metadata=clean_metadata, cache_key=clean_hidden_params["cache_key"], response_cost=response_cost, - cost_breakdown=logging_obj.cost_breakdown, + cost_breakdown=logging_obj.cost_breakdown if logging_obj.cost_breakdown is not None else _compute_cost_breakdown_fallback(logging_obj, init_response_obj), total_tokens=usage_dict.get("total_tokens", 0), prompt_tokens=usage_dict.get("prompt_tokens", 0), completion_tokens=usage_dict.get("completion_tokens", 0), diff --git a/litellm/litellm_core_utils/llm_cost_calc/utils.py b/litellm/litellm_core_utils/llm_cost_calc/utils.py index bf0b2709365..0e7e767a087 100644 --- a/litellm/litellm_core_utils/llm_cost_calc/utils.py +++ b/litellm/litellm_core_utils/llm_cost_calc/utils.py @@ -538,6 +538,21 @@ def _parse_completion_tokens_details(usage: Usage) -> CompletionTokensDetailsRes ) +class InputCostBreakdownResult(TypedDict): + total: float + text_cost: float + cache_read_cost: float + cache_creation_cost: float + audio_cost: float + + +class OutputCostBreakdownResult(TypedDict): + total: float + text_cost: float + reasoning_cost: float + audio_cost: float + + def _calculate_input_cost( prompt_tokens_details: PromptTokensDetailsResult, model_info: ModelInfo, @@ -550,34 +565,58 @@ def _calculate_input_cost( """ Calculates the input cost for a given model, prompt tokens, and completion tokens. """ - prompt_cost = float(prompt_tokens_details["text_tokens"]) * prompt_base_cost + result = _calculate_input_cost_breakdown( + prompt_tokens_details=prompt_tokens_details, + model_info=model_info, + prompt_base_cost=prompt_base_cost, + cache_read_cost=cache_read_cost, + cache_creation_cost=cache_creation_cost, + cache_creation_cost_above_1hr=cache_creation_cost_above_1hr, + service_tier=service_tier, + ) + return result["total"] - ### CACHE READ COST - Now uses tiered pricing - prompt_cost += float(prompt_tokens_details["cache_hit_tokens"]) * cache_read_cost + +def _calculate_input_cost_breakdown( + prompt_tokens_details: PromptTokensDetailsResult, + model_info: ModelInfo, + prompt_base_cost: float, + cache_read_cost: float, + cache_creation_cost: float, + cache_creation_cost_above_1hr: float, + service_tier: Optional[str] = None, +) -> InputCostBreakdownResult: + """ + Calculates the input cost with a granular breakdown by token type. + """ + text_cost = float(prompt_tokens_details["text_tokens"]) * prompt_base_cost + + ### CACHE READ COST + _cache_read_cost = float(prompt_tokens_details["cache_hit_tokens"]) * cache_read_cost ### AUDIO COST + _audio_cost = 0.0 if prompt_tokens_details["audio_tokens"]: audio_cost_key = _get_service_tier_cost_key( "input_cost_per_audio_token", service_tier ) - prompt_cost += calculate_cost_component( + _audio_cost = calculate_cost_component( model_info, audio_cost_key, prompt_tokens_details["audio_tokens"] ) - ### IMAGE TOKEN COST + ### IMAGE TOKEN COST (folded into text_cost for simplicity) if prompt_tokens_details["image_tokens"]: - # For image token costs: - # First check if input_cost_per_image_token is available. If not, default to generic input_cost_per_token. image_token_cost_key = "input_cost_per_image_token" if model_info.get(image_token_cost_key) is None: image_token_cost_key = "input_cost_per_token" - prompt_cost += calculate_cost_component( + text_cost += calculate_cost_component( model_info, image_token_cost_key, prompt_tokens_details["image_tokens"] ) - ### CACHE WRITING COST - Now uses tiered pricing + ### CACHE WRITING COST + _cache_creation_cost = 0.0 if prompt_tokens_details["cache_creation_tokens"] or prompt_tokens_details["cache_creation_token_details"] is not None: - prompt_cost += calculate_cache_writing_cost( + _cache_creation_cost = calculate_cache_writing_cost( cache_creation_tokens=prompt_tokens_details["cache_creation_tokens"], cache_creation_token_details=prompt_tokens_details[ "cache_creation_token_details" @@ -586,27 +625,35 @@ def _calculate_input_cost( cache_creation_cost=cache_creation_cost, ) - ### CHARACTER COST + ### CHARACTER COST (folded into text_cost) if prompt_tokens_details["character_count"]: - prompt_cost += calculate_cost_component( + text_cost += calculate_cost_component( model_info, "input_cost_per_character", prompt_tokens_details["character_count"] ) - ### IMAGE COUNT COST + ### IMAGE COUNT COST (folded into text_cost) if prompt_tokens_details["image_count"]: - prompt_cost += calculate_cost_component( + text_cost += calculate_cost_component( model_info, "input_cost_per_image", prompt_tokens_details["image_count"] ) - ### VIDEO LENGTH COST + ### VIDEO LENGTH COST (folded into text_cost) if prompt_tokens_details["video_length_seconds"]: - prompt_cost += calculate_cost_component( + text_cost += calculate_cost_component( model_info, "input_cost_per_video_per_second", prompt_tokens_details["video_length_seconds"], ) - return prompt_cost + total = text_cost + _cache_read_cost + _cache_creation_cost + _audio_cost + + return InputCostBreakdownResult( + total=total, + text_cost=text_cost, + cache_read_cost=_cache_read_cost, + cache_creation_cost=_cache_creation_cost, + audio_cost=_audio_cost, + ) def generic_cost_per_token( # noqa: PLR0915 @@ -769,6 +816,176 @@ def generic_cost_per_token( # noqa: PLR0915 return prompt_cost, completion_cost +class CostPerTokenBreakdown(TypedDict): + prompt_cost: float + completion_cost: float + input_breakdown: InputCostBreakdownResult + output_breakdown: OutputCostBreakdownResult + prompt_tokens_details: PromptTokensDetailsResult + completion_tokens_details_parsed: CompletionTokensDetailsResult + above_128k_tokens: bool + above_200k_tokens: bool + + +def generic_cost_per_token_with_breakdown( + model: str, + usage: Usage, + custom_llm_provider: str, + service_tier: Optional[str] = None, +) -> CostPerTokenBreakdown: + """ + Same as generic_cost_per_token but returns a detailed breakdown of costs + by token type (text, cache read, cache creation, reasoning, audio, etc.). + """ + model_info = get_model_info(model=model, custom_llm_provider=custom_llm_provider) + + prompt_tokens_details = PromptTokensDetailsResult( + cache_hit_tokens=0, + cache_creation_tokens=0, + cache_creation_token_details=None, + text_tokens=usage.prompt_tokens, + audio_tokens=0, + image_tokens=0, + character_count=0, + image_count=0, + video_length_seconds=0.0, + ) + if usage.prompt_tokens_details: + prompt_tokens_details = _parse_prompt_tokens_details(usage) + + cache_hit = prompt_tokens_details["cache_hit_tokens"] + text_tokens = prompt_tokens_details["text_tokens"] + audio_tokens = prompt_tokens_details["audio_tokens"] + cache_creation = prompt_tokens_details["cache_creation_tokens"] + image_tokens = prompt_tokens_details["image_tokens"] + + total_details = text_tokens + cache_hit + audio_tokens + cache_creation + image_tokens + has_double_counting = cache_hit > 0 and total_details > usage.prompt_tokens + + if (text_tokens == 0 and prompt_tokens_details["image_count"] == 0) or has_double_counting: + text_tokens = ( + usage.prompt_tokens + - cache_hit + - audio_tokens + - cache_creation + - image_tokens + ) + prompt_tokens_details["text_tokens"] = text_tokens + + ( + prompt_base_cost, + completion_base_cost, + cache_creation_cost, + cache_creation_cost_above_1hr, + cache_read_cost, + ) = _get_token_base_cost( + model_info=model_info, usage=usage, service_tier=service_tier + ) + + # Detect pricing tier + above_128k = False + above_200k = False + threshold_keys = [ + k for k in model_info + if k.startswith("input_cost_per_token_above_") + and not any(k.endswith(f"_{st.value}") for st in ServiceTier) + ] + for key in sorted(threshold_keys, reverse=True): + value = model_info.get(key) + if value is not None: + try: + threshold_str = key.split("_above_")[1].split("_tokens")[0] + threshold = float(threshold_str.replace("k", "")) * ( + 1000 if "k" in threshold_str else 1 + ) + if usage.prompt_tokens > threshold: + if threshold >= 200000: + above_200k = True + if threshold >= 128000: + above_128k = True + break + except (IndexError, ValueError): + continue + + input_breakdown = _calculate_input_cost_breakdown( + prompt_tokens_details=prompt_tokens_details, + model_info=model_info, + prompt_base_cost=prompt_base_cost, + cache_read_cost=cache_read_cost, + cache_creation_cost=cache_creation_cost, + cache_creation_cost_above_1hr=cache_creation_cost_above_1hr, + service_tier=service_tier, + ) + + # Output cost breakdown + out_text_tokens = 0 + out_audio_tokens = 0 + out_reasoning_tokens = 0 + out_image_tokens = 0 + is_text_tokens_total = False + if usage.completion_tokens_details is not None: + ctd = _parse_completion_tokens_details(usage) + out_audio_tokens = ctd["audio_tokens"] + out_text_tokens = ctd["text_tokens"] + out_reasoning_tokens = ctd["reasoning_tokens"] + out_image_tokens = ctd["image_tokens"] + + has_token_breakdown = out_image_tokens > 0 or out_audio_tokens > 0 or out_reasoning_tokens > 0 + if out_text_tokens == 0: + if has_token_breakdown: + out_text_tokens = max( + 0, + usage.completion_tokens - out_reasoning_tokens - out_audio_tokens - out_image_tokens, + ) + else: + out_text_tokens = usage.completion_tokens + is_text_tokens_total = True + + out_text_cost = float(out_text_tokens) * completion_base_cost + out_audio_cost = 0.0 + out_reasoning_cost = 0.0 + + if not is_text_tokens_total and out_audio_tokens > 0: + _ocpat = _get_cost_per_unit(model_info, "output_cost_per_audio_token", None) + _ocpat = _ocpat if _ocpat is not None else completion_base_cost + out_audio_cost = float(out_audio_tokens) * _ocpat + + if not is_text_tokens_total and out_reasoning_tokens > 0: + _ocprt = _get_cost_per_unit(model_info, "output_cost_per_reasoning_token", None) + _ocprt = _ocprt if _ocprt is not None else completion_base_cost + out_reasoning_cost = float(out_reasoning_tokens) * _ocprt + + if not is_text_tokens_total and out_image_tokens > 0: + _ocpit = _get_cost_per_unit(model_info, "output_cost_per_image_token", None) + _ocpit = _ocpit if _ocpit is not None else completion_base_cost + out_text_cost += float(out_image_tokens) * _ocpit + + output_breakdown = OutputCostBreakdownResult( + total=out_text_cost + out_reasoning_cost + out_audio_cost, + text_cost=out_text_cost, + reasoning_cost=out_reasoning_cost, + audio_cost=out_audio_cost, + ) + + completion_tokens_details_parsed = CompletionTokensDetailsResult( + audio_tokens=out_audio_tokens, + text_tokens=out_text_tokens, + reasoning_tokens=out_reasoning_tokens, + image_tokens=out_image_tokens, + ) + + return CostPerTokenBreakdown( + prompt_cost=input_breakdown["total"], + completion_cost=output_breakdown["total"], + input_breakdown=input_breakdown, + output_breakdown=output_breakdown, + prompt_tokens_details=prompt_tokens_details, + completion_tokens_details_parsed=completion_tokens_details_parsed, + above_128k_tokens=above_128k, + above_200k_tokens=above_200k, + ) + + def calculate_image_response_cost_from_usage( model: str, image_response: ImageResponse, diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 3c818387744..a29929d9ed0 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2736,6 +2736,30 @@ class CostBreakdown(TypedDict, total=False): margin_fixed_amount: float # Fixed margin amount in USD (optional) margin_total_amount: float # Total margin added in USD (optional) + # Granular input cost breakdown + input_cost_text: float # Cost for non-cached text input tokens + input_cost_cache_read: float # Cost for cache read/hit tokens + input_cost_cache_creation: float # Cost for cache creation/write tokens + input_cost_audio: float # Cost for audio input tokens + + # Granular output cost breakdown + output_cost_text: float # Cost for text output tokens + output_cost_reasoning: float # Cost for reasoning output tokens + output_cost_audio: float # Cost for audio output tokens + + # Token counts for granular breakdown + input_tokens_text: int # Number of non-cached text input tokens + input_tokens_cache_read: int # Number of cache read/hit tokens + input_tokens_cache_creation: int # Number of cache creation/write tokens + input_tokens_audio: int # Number of audio input tokens + output_tokens_text: int # Number of text output tokens + output_tokens_reasoning: int # Number of reasoning output tokens + output_tokens_audio: int # Number of audio output tokens + + # Pricing tier indicator + above_128k_tokens: bool # Whether above-128K token pricing was used + above_200k_tokens: bool # Whether above-200K token pricing was used + class StandardLoggingPayloadStatusFields(TypedDict, total=False): """Status fields for easy filtering and analytics""" diff --git a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx index 087863e9478..66c5425b639 100644 --- a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx @@ -14,6 +14,30 @@ export interface CostBreakdown { margin_percent?: number; margin_fixed_amount?: number; margin_total_amount?: number; + + // Granular input cost breakdown + input_cost_text?: number; + input_cost_cache_read?: number; + input_cost_cache_creation?: number; + input_cost_audio?: number; + + // Granular output cost breakdown + output_cost_text?: number; + output_cost_reasoning?: number; + output_cost_audio?: number; + + // Token counts for granular breakdown + input_tokens_text?: number; + input_tokens_cache_read?: number; + input_tokens_cache_creation?: number; + input_tokens_audio?: number; + output_tokens_text?: number; + output_tokens_reasoning?: number; + output_tokens_audio?: number; + + // Pricing tier indicators + above_128k_tokens?: boolean; + above_200k_tokens?: boolean; } interface CostBreakdownViewerProps { @@ -76,6 +100,24 @@ export const CostBreakdownViewer: React.FC = ({ const originalCost = isCached ? 0 : costBreakdown?.original_cost; const totalCost = isCached ? 0 : (costBreakdown?.total_cost ?? totalSpend); + const hasGranularInput = + !isCached && + costBreakdown && + (costBreakdown.input_cost_cache_read !== undefined || + costBreakdown.input_cost_cache_creation !== undefined || + costBreakdown.input_cost_audio !== undefined); + + const hasGranularOutput = + !isCached && + costBreakdown && + (costBreakdown.output_cost_reasoning !== undefined || + costBreakdown.output_cost_audio !== undefined); + + const hasPricingTier = + !isCached && + costBreakdown && + (costBreakdown.above_128k_tokens || costBreakdown.above_200k_tokens); + return (
= ({ ), children: (
- {/* Step 1: Base Token Costs */} + {/* Pricing tier badge */} + {hasPricingTier && ( +
+ + {costBreakdown?.above_200k_tokens + ? "Above 200K Token Pricing" + : "Above 128K Token Pricing"} + +
+ )} + + {/* Input Cost Section */}
Input Cost: @@ -110,6 +163,76 @@ export const CostBreakdownViewer: React.FC = ({ )}
+ {/* Granular input breakdown */} + {hasGranularInput && ( +
+ {costBreakdown.input_cost_text !== undefined && costBreakdown.input_cost_text > 0 && ( +
+ Text Tokens: + + {formatCost(costBreakdown.input_cost_text)} + {costBreakdown.input_tokens_text !== undefined && ( + + ({costBreakdown.input_tokens_text.toLocaleString()} tokens) + + )} + +
+ )} + {costBreakdown.input_cost_cache_read !== undefined && costBreakdown.input_cost_cache_read > 0 && ( +
+ Cache Read: + + {formatCost(costBreakdown.input_cost_cache_read)} + {costBreakdown.input_tokens_cache_read !== undefined && ( + + ({costBreakdown.input_tokens_cache_read.toLocaleString()} tokens) + + )} + +
+ )} + {costBreakdown.input_tokens_cache_read !== undefined && costBreakdown.input_tokens_cache_read > 0 && (costBreakdown.input_cost_cache_read === undefined || costBreakdown.input_cost_cache_read === 0) && ( +
+ Cache Read: + + {formatCost(0)} + + ({costBreakdown.input_tokens_cache_read.toLocaleString()} tokens — free) + + +
+ )} + {costBreakdown.input_cost_cache_creation !== undefined && costBreakdown.input_cost_cache_creation > 0 && ( +
+ Cache Write: + + {formatCost(costBreakdown.input_cost_cache_creation)} + {costBreakdown.input_tokens_cache_creation !== undefined && ( + + ({costBreakdown.input_tokens_cache_creation.toLocaleString()} tokens) + + )} + +
+ )} + {costBreakdown.input_cost_audio !== undefined && costBreakdown.input_cost_audio > 0 && ( +
+ Audio: + + {formatCost(costBreakdown.input_cost_audio)} + {costBreakdown.input_tokens_audio !== undefined && ( + + ({costBreakdown.input_tokens_audio.toLocaleString()} tokens) + + )} + +
+ )} +
+ )} + + {/* Output Cost */}
Output Cost: @@ -121,9 +244,55 @@ export const CostBreakdownViewer: React.FC = ({ )}
+ {/* Granular output breakdown */} + {hasGranularOutput && ( +
+ {costBreakdown.output_cost_text !== undefined && costBreakdown.output_cost_text > 0 && ( +
+ Text Tokens: + + {formatCost(costBreakdown.output_cost_text)} + {costBreakdown.output_tokens_text !== undefined && ( + + ({costBreakdown.output_tokens_text.toLocaleString()} tokens) + + )} + +
+ )} + {costBreakdown.output_cost_reasoning !== undefined && costBreakdown.output_cost_reasoning > 0 && ( +
+ Reasoning: + + {formatCost(costBreakdown.output_cost_reasoning)} + {costBreakdown.output_tokens_reasoning !== undefined && ( + + ({costBreakdown.output_tokens_reasoning.toLocaleString()} tokens) + + )} + +
+ )} + {costBreakdown.output_cost_audio !== undefined && costBreakdown.output_cost_audio > 0 && ( +
+ Audio: + + {formatCost(costBreakdown.output_cost_audio)} + {costBreakdown.output_tokens_audio !== undefined && ( + + ({costBreakdown.output_tokens_audio.toLocaleString()} tokens) + + )} + +
+ )} +
+ )} + + {/* Web Search / Tool Usage Cost */} {costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
- Tool Usage Cost: + Web Search / Tool Cost: {formatCost(costBreakdown.tool_usage_cost)}
)}