From 5262896d627fb9752242952bc217999ea28d49bf Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Sat, 13 Dec 2025 23:54:44 -0300 Subject: [PATCH] fix(perplexity): use API-provided cost instead of manual calculation (#17887) Fixes #15337 Perplexity API returns pre-calculated costs in `usage.cost.total_cost` that include the `request_cost` (fixed per-request fee). LiteLLM was ignoring this and calculating costs manually, resulting in ~27x underreporting (e.g., $0.0002 vs actual $0.006). Changes: - Use `usage.cost.total_cost` from Perplexity response when available - Fall back to manual calculation if cost object not present - Add tests for both behaviors --- litellm/llms/perplexity/cost_calculator.py | 11 ++++ .../test_perplexity_cost_calculator.py | 60 ++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/litellm/llms/perplexity/cost_calculator.py b/litellm/llms/perplexity/cost_calculator.py index c8fd2a682a8..463d897901b 100644 --- a/litellm/llms/perplexity/cost_calculator.py +++ b/litellm/llms/perplexity/cost_calculator.py @@ -20,6 +20,17 @@ def cost_per_token(model: str, usage: Usage) -> Tuple[float, float]: Returns: Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd """ + ## USE PRE-CALCULATED COST FROM PERPLEXITY IF AVAILABLE + ## Perplexity returns accurate cost in usage.cost.total_cost including request fees + cost_info = getattr(usage, "cost", None) + if cost_info is not None and isinstance(cost_info, dict): + total_cost = cost_info.get("total_cost") + if total_cost is not None: + # Return total cost as completion_cost (prompt_cost=0) since Perplexity + # doesn't break down by input/output in their cost object + return (0.0, float(total_cost)) + + ## FALLBACK: Calculate cost manually if Perplexity doesn't provide it ## GET MODEL INFO model_info = get_model_info(model=model, custom_llm_provider="perplexity") diff --git a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py index f9a52100070..7fda731038a 100644 --- a/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py +++ b/tests/test_litellm/llms/perplexity/test_perplexity_cost_calculator.py @@ -370,4 +370,62 @@ class TestPerplexityCostCalculator: # Ensure costs are non-negative assert prompt_cost >= 0 - assert completion_cost >= 0 \ No newline at end of file + assert completion_cost >= 0 + + def test_uses_perplexity_provided_cost_when_available(self): + """ + Test that when Perplexity provides pre-calculated cost in usage.cost.total_cost, + it is used directly instead of manual calculation. + + This is the fix for issue #15337 - Perplexity returns accurate costs including + request_cost (fixed per-request fee) that LiteLLM cannot calculate. + """ + # Create usage with Perplexity's cost object (as returned by the API) + usage = Usage( + prompt_tokens=100, + completion_tokens=50, + total_tokens=150 + ) + + # Add the cost object that Perplexity returns + usage.cost = { + "input_tokens_cost": 0.0, + "output_tokens_cost": 0.002, + "request_cost": 0.006, + "total_cost": 0.008 + } + + prompt_cost, completion_cost = perplexity_cost_per_token( + model="sonar-pro", + usage=usage + ) + + # When Perplexity provides total_cost, we use it directly + # prompt_cost should be 0, completion_cost should be total_cost + assert prompt_cost == 0.0 + assert completion_cost == 0.008 + assert prompt_cost + completion_cost == 0.008 + + def test_falls_back_to_manual_calculation_when_no_cost_provided(self): + """ + Test that manual cost calculation is used when Perplexity doesn't + provide the cost object (fallback behavior). + """ + usage = Usage( + prompt_tokens=100, + completion_tokens=50, + total_tokens=150 + ) + # No cost object - should use manual calculation + + prompt_cost, completion_cost = perplexity_cost_per_token( + model="sonar-deep-research", + usage=usage + ) + + # Should calculate manually: 100 * 2e-6 + 50 * 8e-6 + expected_prompt = 100 * 2e-6 + expected_completion = 50 * 8e-6 + + assert math.isclose(prompt_cost, expected_prompt, rel_tol=1e-6) + assert math.isclose(completion_cost, expected_completion, rel_tol=1e-6) \ No newline at end of file