From 7e5b3b49d46ae1773e3f0450adbda334c198f108 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 17 Sep 2026 22:33:54 +0000 Subject: [PATCH] fix(proxy): treat non-positive project max_budget as unbudgeted Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/auth/auth_checks.py | 2 +- .../proxy/auth/test_auth_checks.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index e80a4a677ce..e6313790380 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -5612,7 +5612,7 @@ async def _project_max_budget_check( if project_object.litellm_budget_table is not None: max_budget = project_object.litellm_budget_table.max_budget - if max_budget is None or not math.isfinite(max_budget): + if max_budget is None or max_budget <= 0 or not math.isfinite(max_budget): return from litellm.proxy.proxy_server import get_current_spend diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index 56ee0aac249..08ed1575fdd 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -7378,6 +7378,29 @@ async def test_project_max_budget_check_reads_live_spend_counter(counter_spend, assert proxy_logging_obj.budget_alerts.await_args.kwargs["type"] == "project_budget" +@pytest.mark.asyncio +@pytest.mark.parametrize("max_budget", [0.0, -1.0]) +async def test_project_max_budget_check_treats_non_positive_budget_as_unbudgeted(max_budget): + from litellm.caching.dual_cache import DualCache + from litellm.proxy.auth.auth_checks import _project_max_budget_check + + real_spend_counter_cache = DualCache() + real_spend_counter_cache.in_memory_cache.set_cache(key="spend:project:p-budget", value=12.5) + proxy_logging_obj = MagicMock() + proxy_logging_obj.budget_alerts = AsyncMock() + + with patch( # test-quality-ok: injects a real DualCache for the module global, not a behavior mock + "litellm.proxy.proxy_server.spend_counter_cache", real_spend_counter_cache + ): + await _project_max_budget_check( + project_object=_project_with_budget(spend=12.5, max_budget=max_budget), + valid_token=UserAPIKeyAuth(api_key="hashed-key", project_id="p-budget"), + proxy_logging_obj=proxy_logging_obj, + ) + + proxy_logging_obj.budget_alerts.assert_not_awaited() + + def test_is_user_proxy_admin_rejects_view_only_admin(): """This predicate skips `non_proxy_admin_allowed_routes_check` entirely, so an Admin Viewer answering True here would gain every write route. Read parity for