From ae720d0dee9d9f4295668ba1639ad1e77a0c0346 Mon Sep 17 00:00:00 2001 From: Dor Amir Date: Thu, 5 Mar 2026 11:21:14 -0500 Subject: [PATCH] fix: HuggingFace cost calculation now respects custom input_cost_per_token/output_cost_per_token config Fixes #22863 When using HuggingFace via router.huggingface.co with custom pricing, cost tracking was always returning $0. The completion_cost function wasn't extracting custom pricing from litellm_logging_obj.litellm_params .metadata.model_info when custom_pricing=True. Added logic to extract custom_cost_per_token from model_info metadata when custom_pricing=True and no explicit custom_cost_per_token was passed. Also fixed a subtle bug: use 'x if x is not None else 0.0' instead of 'x or 0.0' to correctly handle explicit zero costs (free input tokens). Tests: 4 new test cases added, all 36 tests in file pass. --- litellm/cost_calculator.py | 24 ++++ tests/test_litellm/test_cost_calculator.py | 126 +++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index e0e1e35b94e..0f2f48aa1dd 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1108,6 +1108,30 @@ def completion_cost( # noqa: PLR0915 elif isinstance(cost_per_token_usage_object, dict): service_tier = cost_per_token_usage_object.get("service_tier") + # Extract custom_cost_per_token from litellm_logging_obj when custom_pricing=True + # This enables cost calculation for custom/HuggingFace providers with per-deployment pricing + if ( + custom_pricing is True + and custom_cost_per_token is None + and litellm_logging_obj is not None + ): + _litellm_params = getattr(litellm_logging_obj, "litellm_params", None) + if _litellm_params is not None: + _metadata = _litellm_params.get("metadata", {}) or {} + _model_info = _metadata.get("model_info", {}) or {} + _input_cost = _model_info.get("input_cost_per_token") + _output_cost = _model_info.get("output_cost_per_token") + if _input_cost is not None or _output_cost is not None: + custom_cost_per_token = { + "input_cost_per_token": _input_cost if _input_cost is not None else 0.0, + "output_cost_per_token": _output_cost if _output_cost is not None else 0.0, + } + # Also extract custom_cost_per_second if available + if custom_cost_per_second is None: + _cost_per_second = _model_info.get("input_cost_per_second") + if _cost_per_second is not None: + custom_cost_per_second = _cost_per_second + selected_model = _select_model_name_for_cost_calc( model=model, completion_response=completion_response, diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 8f5c3ece0ca..0a1f7056acd 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -1970,3 +1970,129 @@ def test_additional_costs_only_for_azure_ai(): completion_tokens=50, ) assert result is None, "Vertex AI should have no additional costs" + + +def test_custom_pricing_huggingface_extracts_from_model_info(): + """ + Test that custom pricing for HuggingFace (or any custom provider) correctly extracts + input_cost_per_token and output_cost_per_token from litellm_logging_obj.litellm_params.metadata.model_info. + + This fixes issue #22863: HuggingFace cost calculation always returns $0. + + When using router.huggingface.co with custom pricing config, the cost should be calculated + based on the input_cost_per_token/output_cost_per_token values in model_info. + """ + from unittest.mock import MagicMock + + # Create mock response with usage + response = ModelResponse( + id="test-id", + model="huggingface/my-custom-model", + choices=[], + usage=Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150), + ) + + # Create mock litellm_logging_obj with custom pricing in model_info + mock_logging_obj = MagicMock() + mock_logging_obj.litellm_params = { + "metadata": { + "model_info": { + "input_cost_per_token": 0.0001, # $0.0001 per input token + "output_cost_per_token": 0.0002, # $0.0002 per output token + } + } + } + + # Calculate cost with custom_pricing=True + cost = completion_cost( + completion_response=response, + model="huggingface/my-custom-model", + custom_llm_provider="huggingface", + custom_pricing=True, + litellm_logging_obj=mock_logging_obj, + ) + + # Expected: (100 * 0.0001) + (50 * 0.0002) = 0.01 + 0.01 = 0.02 + expected_cost = (100 * 0.0001) + (50 * 0.0002) + assert cost == expected_cost, f"Expected cost {expected_cost}, got {cost}" + assert cost > 0, "Cost should be greater than 0 for custom HuggingFace pricing" + + +def test_custom_pricing_unknown_provider_extracts_from_model_info(): + """ + Test that any unknown/custom provider with custom_pricing=True correctly uses + input_cost_per_token and output_cost_per_token from litellm_logging_obj. + + This is a more general test to ensure the fix works for any custom provider, + not just HuggingFace. + """ + from unittest.mock import MagicMock + + # Create mock response + response = ModelResponse( + id="test-id", + model="custom-provider/custom-model", + choices=[], + usage=Usage(prompt_tokens=200, completion_tokens=100, total_tokens=300), + ) + + # Create mock litellm_logging_obj + mock_logging_obj = MagicMock() + mock_logging_obj.litellm_params = { + "metadata": { + "model_info": { + "input_cost_per_token": 0.00005, + "output_cost_per_token": 0.00015, + } + } + } + + cost = completion_cost( + completion_response=response, + model="custom-provider/custom-model", + custom_llm_provider="custom_provider", + custom_pricing=True, + litellm_logging_obj=mock_logging_obj, + ) + + # Expected: (200 * 0.00005) + (100 * 0.00015) = 0.01 + 0.015 = 0.025 + expected_cost = (200 * 0.00005) + (100 * 0.00015) + assert cost == expected_cost, f"Expected cost {expected_cost}, got {cost}" + + +def test_custom_pricing_partial_costs_in_model_info(): + """ + Test that custom pricing works when only one of input/output cost is provided. + The missing cost should default to 0. + """ + from unittest.mock import MagicMock + + response = ModelResponse( + id="test-id", + model="custom/model", + choices=[], + usage=Usage(prompt_tokens=100, completion_tokens=50, total_tokens=150), + ) + + # Only input_cost_per_token provided + mock_logging_obj = MagicMock() + mock_logging_obj.litellm_params = { + "metadata": { + "model_info": { + "input_cost_per_token": 0.001, + # output_cost_per_token not provided + } + } + } + + cost = completion_cost( + completion_response=response, + model="custom/model", + custom_llm_provider="custom", + custom_pricing=True, + litellm_logging_obj=mock_logging_obj, + ) + + # Expected: (100 * 0.001) + (50 * 0.0) = 0.1 + expected_cost = 100 * 0.001 + assert cost == expected_cost, f"Expected cost {expected_cost}, got {cost}"