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.
This commit is contained in:
Dor Amir 2026-03-05 11:21:14 -05:00
parent cec3e9e7d4
commit ae720d0dee
2 changed files with 150 additions and 0 deletions

View file

@ -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,

View file

@ -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}"