diff --git a/litellm/proxy/management_endpoints/common_utils.py b/litellm/proxy/management_endpoints/common_utils.py index f28bcc2bcd4..b15a934064b 100644 --- a/litellm/proxy/management_endpoints/common_utils.py +++ b/litellm/proxy/management_endpoints/common_utils.py @@ -1,8 +1,27 @@ +import math from typing import TYPE_CHECKING, Any, Dict, Optional, Union from fastapi import HTTPException, status from pydantic import BaseModel + +# Defined above the `litellm.proxy.*` imports so the name is bound even when +# this module is imported first through the proxy import cycle (CodeQL: +# module-level cyclic import). Depends only on `math` + `HTTPException`. +def validate_finite_spend(spend: Optional[float]) -> None: + """Reject NaN/±inf spend before it reaches the DB / spend counter. + + A non-finite spend would otherwise slip past `spend >= max_budget` + enforcement, since any comparison with NaN (and `-inf >= max_budget`) + is False, letting the entity keep spending past its configured budget. + """ + if spend is not None and not math.isfinite(spend): + raise HTTPException( + status_code=400, + detail={"error": f"spend must be a finite number. Received: {spend}"}, + ) + + from litellm._logging import verbose_proxy_logger from litellm.caching import DualCache from litellm.proxy._types import ( diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_utils.py b/tests/test_litellm/proxy/management_endpoints/test_common_utils.py index 7a8d04507dc..bf7d89fdc26 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_utils.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_utils.py @@ -570,3 +570,35 @@ class TestRequireCallerUserIdForNonAdmin: assert exc_info.value.status_code == 403 assert "Service-account keys" in str(exc_info.value.detail) + + +class TestValidateFiniteSpend: + """`validate_finite_spend` rejects NaN/±inf so a non-finite spend cannot + bypass `spend >= max_budget` enforcement (NaN/-inf compare false).""" + + def test_none_is_allowed(self): + from litellm.proxy.management_endpoints.common_utils import ( + validate_finite_spend, + ) + + assert validate_finite_spend(None) is None + + def test_finite_value_is_allowed(self): + from litellm.proxy.management_endpoints.common_utils import ( + validate_finite_spend, + ) + + assert validate_finite_spend(0.0) is None + assert validate_finite_spend(12.5) is None + + @pytest.mark.parametrize("bad", [float("nan"), float("inf"), float("-inf")]) + def test_non_finite_is_rejected(self, bad): + from fastapi import HTTPException + + from litellm.proxy.management_endpoints.common_utils import ( + validate_finite_spend, + ) + + with pytest.raises(HTTPException) as exc_info: + validate_finite_spend(bad) + assert exc_info.value.status_code == 400