Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions litellm/cost_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
_parse_prompt_tokens_details,
calculate_cost_component,
generic_cost_per_token,
get_token_type_cost_breakdown,
get_billable_input_tokens,
select_cost_metric_for_model,
)
Expand Down Expand Up @@ -1050,6 +1051,7 @@ def _store_cost_breakdown_in_logging_obj(
margin_total_amount: Optional[float] = None,
cache_read_cost: Optional[float] = None,
cache_creation_cost: Optional[float] = None,
reasoning_cost: Optional[float] = None,
) -> None:
"""
Helper function to store cost breakdown in the logging object.
Expand Down Expand Up @@ -1087,6 +1089,7 @@ def _store_cost_breakdown_in_logging_obj(
margin_total_amount=margin_total_amount,
cache_read_cost=cache_read_cost,
cache_creation_cost=cache_creation_cost,
reasoning_cost=reasoning_cost,
)

except Exception as breakdown_error:
Expand Down Expand Up @@ -1628,28 +1631,23 @@ def completion_cost(

# Store cost breakdown in logging object if available
if litellm_logging_obj is not None:
_reasoning_cost: Optional[float] = None
_cache_read_cost: Optional[float] = None
_cache_creation_cost: Optional[float] = None
if cost_per_token_usage_object is not None:
_cr = getattr(cost_per_token_usage_object, "cache_read_input_tokens", None) or (
cost_per_token_usage_object.model_extra or {}
).get("cache_read_input_tokens")
_cc = getattr(
cost_per_token_usage_object,
"cache_creation_input_tokens",
None,
) or (cost_per_token_usage_object.model_extra or {}).get("cache_creation_input_tokens")
if (_cr or _cc) and model:
try:
_mi = litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider)
_cr_rate = _mi.get("cache_read_input_token_cost")
if _cr and _cr_rate is not None:
_cache_read_cost = float(_cr) * float(_cr_rate)
_cc_rate = _mi.get("cache_creation_input_token_cost")
if _cc and _cc_rate is not None:
_cache_creation_cost = float(_cc) * float(_cc_rate)
except Exception:
pass
if cost_per_token_usage_object is not None and model:
_breakdown_provider: Optional[str] = (
custom_llm_provider if isinstance(custom_llm_provider, str) else None
)
_token_type_breakdown = get_token_type_cost_breakdown(
model=model,
custom_llm_provider=_breakdown_provider,
usage=cost_per_token_usage_object,
service_tier=service_tier,
data_residency=data_residency,
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
_reasoning_cost = _token_type_breakdown.reasoning_cost
_cache_read_cost = _token_type_breakdown.cache_read_cost
_cache_creation_cost = _token_type_breakdown.cache_creation_cost
_store_cost_breakdown_in_logging_obj(
litellm_logging_obj=litellm_logging_obj,
prompt_tokens_cost_usd_dollar=prompt_tokens_cost_usd_dollar,
Expand All @@ -1665,6 +1663,7 @@ def completion_cost(
margin_total_amount=margin_total_amount,
cache_read_cost=_cache_read_cost,
cache_creation_cost=_cache_creation_cost,
reasoning_cost=_reasoning_cost,
)

return _final_cost
Expand Down
3 changes: 3 additions & 0 deletions litellm/litellm_core_utils/litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,7 @@ def set_cost_breakdown(
margin_total_amount: Optional[float] = None,
cache_read_cost: Optional[float] = None,
cache_creation_cost: Optional[float] = None,
reasoning_cost: Optional[float] = None,
) -> None:
"""
Helper method to store cost breakdown in the logging object.
Expand Down Expand Up @@ -1325,6 +1326,8 @@ def set_cost_breakdown(
self.cost_breakdown["cache_read_cost"] = cache_read_cost
if cache_creation_cost is not None and cache_creation_cost > 0:
self.cost_breakdown["cache_creation_cost"] = cache_creation_cost
if reasoning_cost is not None and reasoning_cost > 0:
self.cost_breakdown["reasoning_cost"] = reasoning_cost

# Store additional costs if provided (free-form dict for extensibility)
if additional_costs and isinstance(additional_costs, dict) and len(additional_costs) > 0:
Expand Down
102 changes: 102 additions & 0 deletions litellm/litellm_core_utils/llm_cost_calc/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# What is this?
## Helper utilities for cost_per_token()

from dataclasses import dataclass
from typing import Any, Literal, Optional, Tuple, TypedDict, cast

import litellm
Expand Down Expand Up @@ -813,6 +814,107 @@ def generic_cost_per_token(
return prompt_cost, completion_cost


def _coerce_token_count(value: object) -> int:
return value if isinstance(value, int) and value > 0 else 0


@dataclass(frozen=True, slots=True)
class TokenTypeCostBreakdown:
reasoning_cost: float
cache_read_cost: float
cache_creation_cost: float


def get_token_type_cost_breakdown(
model: str,
custom_llm_provider: Optional[str],
usage: Usage,
service_tier: Optional[str] = None,
data_residency: Optional[str] = None,
) -> TokenTypeCostBreakdown:
"""
Provider-agnostic cost of reasoning and cache tokens, derived from the usage
object and model pricing alone.

This works for every provider, including Perplexity/Cerebras/Dashscope whose
cost calculators bypass ``generic_cost_per_token``, because cache tokens always
land on ``prompt_tokens_details`` (via the Usage constructor and provider
transformations) and reasoning tokens on ``completion_tokens_details``. It reuses
the same rate-resolution primitives as the total-cost path so the breakdown can
never drift from the totals. Returns zeros (never raises) when the model or its
pricing cannot be resolved.
"""
try:
model_info = get_model_info(model=model, custom_llm_provider=custom_llm_provider)
except Exception:
return TokenTypeCostBreakdown(0.0, 0.0, 0.0)

(
_prompt_base_cost,
completion_base_cost,
cache_creation_cost_rate,
cache_creation_cost_above_1hr_rate,
cache_read_cost_rate,
) = _get_token_base_cost(model_info=model_info, usage=usage, service_tier=service_tier)

reasoning_tokens = (
_parse_completion_tokens_details(usage)["reasoning_tokens"]
if usage.completion_tokens_details is not None
else 0
)
if not reasoning_tokens:
reasoning_tokens = _coerce_token_count(getattr(usage, "reasoning_tokens", 0))

# Reasoning is billed at the explicit per-reasoning-token rate when the model
# defines one, otherwise at the standard output-token rate - this mirrors how the
# total completion cost is computed, so the breakdown can never diverge from it.
reasoning_rate = _get_cost_per_unit(model_info, "output_cost_per_reasoning_token", None)
if reasoning_rate is None:
reasoning_rate = completion_base_cost
reasoning_cost = float(reasoning_tokens) * reasoning_rate

cache_read_tokens = 0
cache_creation_tokens = 0
cache_creation_token_details: Optional[CacheCreationTokenDetails] = None
if usage.prompt_tokens_details is not None:
prompt_tokens_details = _parse_prompt_tokens_details(usage)
cache_read_tokens = prompt_tokens_details["cache_hit_tokens"]
cache_creation_tokens = prompt_tokens_details["cache_creation_tokens"]
cache_creation_token_details = prompt_tokens_details["cache_creation_token_details"]
# Some OpenAI-compatible providers (e.g. kimi-k2) report cache-write tokens
# under `cache_write_tokens`; mirror the total-cost normalization path.
if not cache_creation_tokens:
cache_creation_tokens = _coerce_token_count(getattr(usage.prompt_tokens_details, "cache_write_tokens", 0))
# Fall back to the private top-level counters the Usage constructor mirrors cache
# tokens onto, so providers/callers that bypass prompt_tokens_details are covered.
if not cache_read_tokens:
cache_read_tokens = _coerce_token_count(getattr(usage, "_cache_read_input_tokens", 0))
if not cache_creation_tokens:
cache_creation_tokens = _coerce_token_count(getattr(usage, "_cache_creation_input_tokens", 0))

cache_read_cost = float(cache_read_tokens) * cache_read_cost_rate
cache_creation_cost = calculate_cache_writing_cost(
cache_creation_tokens=cache_creation_tokens,
cache_creation_token_details=cache_creation_token_details,
cache_creation_cost_above_1hr=cache_creation_cost_above_1hr_rate,
cache_creation_cost=cache_creation_cost_rate,
)

# Apply the same flat regional-processing uplift the totals get, so per-type
# costs stay reconciled with input_cost/output_cost for regionalized OpenAI hosts.
uplift = _get_regional_uplift_multiplier(model_info, data_residency)
if uplift != 1.0:
reasoning_cost *= uplift
cache_read_cost *= uplift
cache_creation_cost *= uplift

return TokenTypeCostBreakdown(
reasoning_cost=reasoning_cost,
cache_read_cost=cache_read_cost,
cache_creation_cost=cache_creation_cost,
)


def calculate_image_response_cost_from_usage(
model: str,
image_response: ImageResponse,
Expand Down
1 change: 1 addition & 0 deletions litellm/types/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2812,6 +2812,7 @@ class CostBreakdown(TypedDict, total=False):
cache_read_cost: float # Cost of cache-read tokens (discounted rate)
cache_creation_cost: float # Cost of cache-write tokens (premium rate)
output_cost: float # Cost of output/completion tokens (includes reasoning if applicable)
reasoning_cost: float # Cost of reasoning tokens (subset of output_cost)
total_cost: float # Total cost (input + output + tool usage)
tool_usage_cost: float # Cost of usage of built-in tools
additional_costs: Dict[str, float] # Free-form additional costs (e.g., {"azure_model_router_flat_cost": 0.00014})
Expand Down
Loading
Loading