diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 3b73b853eca..3e0e25e81a7 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -283,6 +283,7 @@ def cost_per_token( # noqa: PLR0915 audio_transcription_file_duration: float = 0.0, # for audio transcription calls - the file time in seconds ### SERVICE TIER ### service_tier: Optional[str] = None, # for OpenAI service tier pricing + custom_pricing: Optional[bool] = None, # whether model uses custom pricing from router/config response: Optional[Any] = None, ### REQUEST MODEL ### request_model: Optional[str] = None, # original request model for router detection @@ -475,6 +476,13 @@ def cost_per_token( # noqa: PLR0915 else None ), ) + elif custom_pricing is True and model in model_cost_ref: + return generic_cost_per_token( + model=model, + usage=usage_block, + custom_llm_provider=custom_llm_provider or "", + service_tier=service_tier, + ) elif custom_llm_provider == "vertex_ai": cost_router = google_cost_router( model=model_without_prefix, @@ -545,8 +553,8 @@ def cost_per_token( # noqa: PLR0915 ) if ( - (model_info.get("input_cost_per_token") or 0.0) > 0 - or (model_info.get("output_cost_per_token") or 0.0) > 0 + model_info.get("input_cost_per_token") is not None + or model_info.get("output_cost_per_token") is not None ): return generic_cost_per_token( model=model, @@ -1509,6 +1517,7 @@ def completion_cost( # noqa: PLR0915 audio_transcription_file_duration=audio_transcription_file_duration, rerank_billed_units=rerank_billed_units, service_tier=service_tier, + custom_pricing=custom_pricing, response=completion_response, request_model=request_model_for_cost, ) diff --git a/tests/test_litellm/test_cost_calculator.py b/tests/test_litellm/test_cost_calculator.py index 8f5c3ece0ca..52847650929 100644 --- a/tests/test_litellm/test_cost_calculator.py +++ b/tests/test_litellm/test_cost_calculator.py @@ -447,6 +447,77 @@ def test_per_request_custom_pricing_with_router(): assert "gpt-3.5-turbo" in selected +def test_zero_cost_custom_pricing_with_anthropic_provider(): + """ + When a user sets input_cost_per_token=0.0 and output_cost_per_token=0.0 + with custom_llm_provider="anthropic" via the router, the zero-cost + override must be respected. The provider-specific dispatch should + be bypassed so that real Anthropic pricing is NOT used. + + Regression test for https://github.com/BerriAI/litellm/issues/25204 + """ + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "free-claude", + "litellm_params": { + "model": "anthropic/claude-sonnet-4-20250514", + "api_key": "test_api_key", + }, + "model_info": { + "id": "zero-cost-model-id", + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + }, + }, + ] + ) + + result = router.completion( + model="free-claude", + messages=[{"role": "user", "content": "Hello!"}], + mock_response="Hi there!", + ) + + assert result._hidden_params["response_cost"] == 0.0 + + +def test_cost_per_token_custom_pricing_bypasses_provider_dispatch(): + """ + cost_per_token with custom_pricing=True should use generic_cost_per_token + instead of the provider-specific dispatch, ensuring custom pricing entries + (including zero costs) are respected. + + Regression test for https://github.com/BerriAI/litellm/issues/25204 + """ + from litellm.cost_calculator import cost_per_token + + custom_model_id = "custom-zero-cost-anthropic" + litellm.register_model( + model_cost={ + custom_model_id: { + "input_cost_per_token": 0.0, + "output_cost_per_token": 0.0, + "litellm_provider": "anthropic", + "max_tokens": 8192, + } + } + ) + + prompt_cost, completion_cost_val = cost_per_token( + model=custom_model_id, + prompt_tokens=1000, + completion_tokens=500, + custom_llm_provider="anthropic", + custom_pricing=True, + ) + + assert prompt_cost == 0.0 + assert completion_cost_val == 0.0 + + def test_azure_realtime_cost_calculator(): os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True" litellm.model_cost = litellm.get_model_cost_map(url="")