diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 699afba412f..fbabad27d50 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -966,6 +966,8 @@ def _store_cost_breakdown_in_logging_obj( margin_percent: Optional[float] = None, margin_fixed_amount: Optional[float] = None, margin_total_amount: Optional[float] = None, + cache_read_cost: Optional[float] = None, + cache_creation_cost: Optional[float] = None, ) -> None: """ Helper function to store cost breakdown in the logging object. @@ -1001,6 +1003,8 @@ def _store_cost_breakdown_in_logging_obj( margin_percent=margin_percent, margin_fixed_amount=margin_fixed_amount, margin_total_amount=margin_total_amount, + cache_read_cost=cache_read_cost, + cache_creation_cost=cache_creation_cost, ) except Exception as breakdown_error: @@ -1599,6 +1603,22 @@ def completion_cost( # noqa: PLR0915 # Store cost breakdown in logging object if available if litellm_logging_obj is not 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 _store_cost_breakdown_in_logging_obj( litellm_logging_obj=litellm_logging_obj, prompt_tokens_cost_usd_dollar=prompt_tokens_cost_usd_dollar, @@ -1612,6 +1632,8 @@ def completion_cost( # noqa: PLR0915 margin_percent=margin_percent, margin_fixed_amount=margin_fixed_amount, margin_total_amount=margin_total_amount, + cache_read_cost=_cache_read_cost, + cache_creation_cost=_cache_creation_cost, ) return _final_cost diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index e84c1e13a8b..e335ebabd8b 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -1387,6 +1387,8 @@ class Logging(LiteLLMLoggingBaseClass): margin_percent: Optional[float] = None, margin_fixed_amount: Optional[float] = None, margin_total_amount: Optional[float] = None, + cache_read_cost: Optional[float] = None, + cache_creation_cost: Optional[float] = None, ) -> None: """ Helper method to store cost breakdown in the logging object. @@ -1411,6 +1413,10 @@ class Logging(LiteLLMLoggingBaseClass): total_cost=total_cost, tool_usage_cost=cost_for_built_in_tools_cost_usd_dollar, ) + if cache_read_cost is not None and cache_read_cost > 0: + 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 # Store additional costs if provided (free-form dict for extensibility) if ( diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index d7ce6a5f8de..9f2868dd50c 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1702,10 +1702,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ), ) + raw_input_tokens = usage_object.get("input_tokens", 0) or 0 prompt_tokens_details = PromptTokensDetailsWrapper( cached_tokens=cache_read_input_tokens, cache_creation_tokens=cache_creation_input_tokens, cache_creation_token_details=cache_creation_token_details, + text_tokens=raw_input_tokens, ) # Always populate completion_token_details, not just when there's reasoning_content reasoning_tokens = ( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 5cfb00d69b6..4e71c9584ab 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -1651,6 +1651,7 @@ class AmazonConverseConfig(BaseConfig): cache_creation_input_tokens: int = 0 cache_read_input_tokens: int = 0 + raw_input_tokens = input_tokens # capture before inflation if "cacheReadInputTokens" in usage: cache_read_input_tokens = usage["cacheReadInputTokens"] input_tokens += cache_read_input_tokens @@ -1659,7 +1660,9 @@ class AmazonConverseConfig(BaseConfig): input_tokens += cache_creation_input_tokens prompt_tokens_details = PromptTokensDetailsWrapper( - cached_tokens=cache_read_input_tokens + cached_tokens=cache_read_input_tokens, + cache_creation_tokens=cache_creation_input_tokens, + text_tokens=raw_input_tokens, ) reasoning_tokens = ( token_counter(text=reasoning_content, count_response_tokens=True) diff --git a/litellm/types/utils.py b/litellm/types/utils.py index c6fa61f8a97..0e3bc8f3ed3 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -2798,7 +2798,9 @@ class CostBreakdown(TypedDict, total=False): Detailed cost breakdown for a request """ - input_cost: float # Cost of input/prompt tokens + input_cost: float # Cost of raw (non-cached) input tokens only + 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) ) diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx index 1495c7d3e5b..69b29564d83 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx @@ -641,7 +641,12 @@ const UsagePage: React.FC = ({ teams, organizations }) => { Input Tokens - {userSpendData.metadata?.total_prompt_tokens?.toLocaleString() || 0} + {Math.max( + 0, + (userSpendData.metadata?.total_prompt_tokens || 0) - + (userSpendData.metadata?.total_cache_read_input_tokens || 0) - + (userSpendData.metadata?.total_cache_creation_input_tokens || 0) + ).toLocaleString()} diff --git a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx index 61699cb2ef5..a9ce90a73f0 100644 --- a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx @@ -4,6 +4,8 @@ import { formatNumberWithCommas } from "@/utils/dataUtils"; export interface CostBreakdown { input_cost?: number; + cache_read_cost?: number; + cache_creation_cost?: number; output_cost?: number; total_cost?: number; tool_usage_cost?: number; @@ -22,6 +24,9 @@ interface CostBreakdownViewerProps { promptTokens?: number; completionTokens?: number; cacheHit?: string; + rawInputTokens?: number; + cacheReadTokens?: number; + cacheCreationTokens?: number; } const formatCost = (cost: number | undefined): string => { @@ -40,6 +45,9 @@ export const CostBreakdownViewer: React.FC = ({ promptTokens, completionTokens, cacheHit, + rawInputTokens, + cacheReadTokens, + cacheCreationTokens, }) => { const isCached = cacheHit?.toLowerCase() === "true"; const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined; @@ -105,17 +113,63 @@ export const CostBreakdownViewer: React.FC = ({
{/* Step 1: Base Token Costs */}
-
- Input Cost: - - {formatCost(inputCost)} - {promptTokens !== undefined && ( - - ({promptTokens.toLocaleString()} prompt tokens) + {(() => { + const hasCacheBreakdown = + costBreakdown?.cache_read_cost !== undefined || + costBreakdown?.cache_creation_cost !== undefined; + if (hasCacheBreakdown) { + // Separate line items: Input / Cache Read / Cache Write + const rawCost = isCached ? 0 : (inputCost ?? 0) - (costBreakdown?.cache_read_cost ?? 0) - (costBreakdown?.cache_creation_cost ?? 0); + return ( + <> +
+ Input Cost: + + {formatCost(rawCost)} + {rawInputTokens !== undefined && rawInputTokens !== null && ( + ({rawInputTokens.toLocaleString()} tokens) + )} + +
+ {(costBreakdown?.cache_read_cost ?? 0) > 0 && ( +
+ Cache Read Cost: + + {formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)} + {(cacheReadTokens ?? 0) > 0 && ( + ({(cacheReadTokens ?? 0).toLocaleString()} tokens) + )} + +
+ )} + {(costBreakdown?.cache_creation_cost ?? 0) > 0 && ( +
+ Cache Write Cost: + + {formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)} + {(cacheCreationTokens ?? 0) > 0 && ( + ({(cacheCreationTokens ?? 0).toLocaleString()} tokens) + )} + +
+ )} + + ); + } + return ( +
+ Input Cost: + + {formatCost(inputCost)} + {promptTokens !== undefined && ( + + ({promptTokens.toLocaleString()} prompt tokens) + + )} - )} - -
+
+ ); + })()}
Output Cost: diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx index 175088ca0b9..992063fb69b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.test.tsx @@ -113,6 +113,35 @@ describe("LogDetailContent", () => { expect(screen.getAllByText("$0.00200000").length).toBeGreaterThanOrEqual(1); }); + it("should show Input Tokens and Output Tokens for anthropic_messages when uncached text_tokens exist", () => { + render( + , + ); + + expect(screen.getByText("Input Tokens")).toBeInTheDocument(); + expect(screen.getByText("Output Tokens")).toBeInTheDocument(); + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.getByText("28")).toBeInTheDocument(); + // Combined TokenFlow line should not appear (would include "prompt tokens") + expect(screen.queryByText(/prompt tokens \+ .* completion tokens/)).not.toBeInTheDocument(); + }); + it("should display ConfigInfoMessage when no messages, response, or error and not loading", () => { render( {/* Tools */} @@ -257,6 +260,19 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu ); } +/** + * Uncached input token count (billable non-cache prompt text), aligned with Cost Breakdown "Input". + * Same sources as CostBreakdownViewer rawInputTokens. + */ +function getUncachedInputTextTokens(metadata: Record): number | undefined { + const raw = + metadata?.additional_usage_values?.prompt_tokens_details?.text_tokens ?? + metadata?.usage_object?.prompt_tokens_details?.text_tokens; + if (raw === undefined || raw === null) return undefined; + const n = Number(raw); + return Number.isFinite(n) ? n : undefined; +} + function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record }) { const completionStartTime = logEntry.completionStartTime; const ttftMs = @@ -277,17 +293,32 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: ? "red" : "default"; + const uncachedInputTokens = getUncachedInputTextTokens(metadata); + const showAnthropicMessagesInputOutput = + logEntry.call_type === "anthropic_messages" && uncachedInputTokens !== undefined; + return (
- - - + {showAnthropicMessagesInputOutput ? ( + <> + + {formatNumberWithCommas(uncachedInputTokens)} + + + {formatNumberWithCommas(logEntry.completion_tokens)} + + + ) : ( + + + + )} ${formatNumberWithCommas(logEntry.spend || 0, 8)} {logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s {ttftMs != null && ttftMs > 0 && ( diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index d24bdd340f7..5b0352feb98 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ {