diff --git a/litellm/router_strategy/budget_limiter.py b/litellm/router_strategy/budget_limiter.py index 64252cbbfb3..cde39dbd48d 100644 --- a/litellm/router_strategy/budget_limiter.py +++ b/litellm/router_strategy/budget_limiter.py @@ -246,7 +246,7 @@ class RouterBudgetLimiting(CustomLogger): budget_limit=config.max_budget, ) - if config.max_budget and current_spend >= config.max_budget: + if config.max_budget is not None and current_spend >= config.max_budget: debug_msg = f"Exceeded budget for provider {provider}: {current_spend} >= {config.max_budget}" deployment_above_budget_info += f"{debug_msg}\n" is_within_budget = False @@ -261,7 +261,7 @@ class RouterBudgetLimiting(CustomLogger): if model_id in deployment_configs: config = deployment_configs[model_id] current_spend = spend_map.get(f"deployment_spend:{model_id}:{config.budget_duration}", 0.0) - if config.max_budget and current_spend >= config.max_budget: + if config.max_budget is not None and current_spend >= config.max_budget: debug_msg = f"Exceeded budget for deployment model_name: {_model_name}, litellm_params.model: {_litellm_model_name}, model_id: {model_id}: {current_spend} >= {config.budget_duration}" verbose_router_logger.debug(debug_msg) deployment_above_budget_info += f"{debug_msg}\n" @@ -276,7 +276,7 @@ class RouterBudgetLimiting(CustomLogger): f"tag_spend:{_tag}:{_tag_budget_config.budget_duration}", 0.0, ) - if _tag_budget_config.max_budget and _tag_spend >= _tag_budget_config.max_budget: + if _tag_budget_config.max_budget is not None and _tag_spend >= _tag_budget_config.max_budget: debug_msg = f"Exceeded budget for tag='{_tag}', tag_spend={_tag_spend}, tag_budget_limit={_tag_budget_config.max_budget}" verbose_router_logger.debug(debug_msg) deployment_above_budget_info += f"{debug_msg}\n" diff --git a/tests/unit/router_strategy/test_budget_limiter.py b/tests/unit/router_strategy/test_budget_limiter.py index 62de1586fdd..44b6c585806 100644 --- a/tests/unit/router_strategy/test_budget_limiter.py +++ b/tests/unit/router_strategy/test_budget_limiter.py @@ -12,6 +12,7 @@ import pytest from litellm.caching.caching import DualCache from litellm.router_strategy.budget_limiter import RouterBudgetLimiting +from litellm.types.utils import BudgetConfig @pytest.fixture @@ -135,3 +136,90 @@ async def test_deployment_budget_tracked_when_provider_is_unresolvable(disable_b ) assert await limiter.dual_cache.async_get_cache("deployment_spend:deployment-1:1d") == 0.25 + + +@pytest.mark.asyncio +async def test_zero_max_budget_blocks_spend(disable_budget_sync): + """A configured budget of 0 blocks all spend; only None means "no limit". + + The three budget checks used a truthy test on ``max_budget``, so a legitimate + ``0`` (an admin fully blocking spend) short-circuited to "no limit" and left + the deployment routable. + """ + healthy_deployments: Final = [ + { + "model_name": "gpt-4o", + "litellm_params": {"model": "openai/gpt-4o", "max_budget": 0.0, "budget_duration": "1d"}, + "model_info": {"id": "deployment-1"}, + } + ] + + provider_limiter = RouterBudgetLimiting( + dual_cache=DualCache(), + provider_budget_config={"openai": {"budget_limit": 0.0, "time_period": "1d"}}, + ) + provider_configs: Final = {"openai": BudgetConfig(max_budget=0.0, budget_duration="1d")} + provider_kept, _ = provider_limiter._filter_out_deployments_above_budget( + potential_deployments=[], + healthy_deployments=healthy_deployments, + provider_configs=provider_configs, + deployment_configs={}, + deployment_providers=["openai"], + spend_map={"provider_spend:openai:1d": 5.0}, + request_tags=[], + ) + assert provider_kept == [] + + deployment_limiter = RouterBudgetLimiting( + dual_cache=DualCache(), + provider_budget_config=None, + model_list=[ + { + "model_name": "gpt-4o", + "litellm_params": {"model": "openai/gpt-4o", "max_budget": 0.0, "budget_duration": "1d"}, + "model_info": {"id": "deployment-1"}, + } + ], + ) + deployment_configs: Final = {"deployment-1": BudgetConfig(max_budget=0.0, budget_duration="1d")} + deployment_kept, _ = deployment_limiter._filter_out_deployments_above_budget( + potential_deployments=[], + healthy_deployments=healthy_deployments, + provider_configs={}, + deployment_configs=deployment_configs, + deployment_providers=["openai"], + spend_map={"deployment_spend:deployment-1:1d": 0.0}, + request_tags=[], + ) + assert deployment_kept == [] + + tag_limiter = RouterBudgetLimiting( + dual_cache=DualCache(), + provider_budget_config=None, + ) + tag_limiter.tag_budget_config = {"blocked-tag": BudgetConfig(time_period="1d", budget_limit=0.0)} + tag_kept, _ = tag_limiter._filter_out_deployments_above_budget( + potential_deployments=[], + healthy_deployments=healthy_deployments, + provider_configs={}, + deployment_configs={}, + deployment_providers=["openai"], + spend_map={"tag_spend:blocked-tag:1d": 0.0}, + request_tags=["blocked-tag"], + ) + assert tag_kept == [] + + unset_limiter = RouterBudgetLimiting( + dual_cache=DualCache(), + provider_budget_config=None, + ) + unset_kept, _ = unset_limiter._filter_out_deployments_above_budget( + potential_deployments=[], + healthy_deployments=healthy_deployments, + provider_configs={}, + deployment_configs={}, + deployment_providers=["openai"], + spend_map={}, + request_tags=[], + ) + assert len(unset_kept) == 1