BerriAI/litellm · error · HTTPException
soft_budget must be a non-negative finite number. Received:
Error message
soft_budget must be a non-negative finite number. Received: {data.soft_budget} What it means
LiteLLM Proxy rejects a POST /key/generate (or /key/generate for service accounts sharing this validation path) whose soft_budget is negative, NaN, or infinite. The check exists because float('nan') silently passes a naive `< 0` comparison (security advisory GHSA-2rv4-xv66-fpjg), so both finiteness and sign are now enforced. It returns HTTP 400 with the offending value echoed back.
Source
Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:1695
if prisma_client is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
verbose_proxy_logger.debug("entered /key/generate")
await check_org_admin_can_generate_keys(user_api_key_dict=user_api_key_dict)
# Validate budget values are not negative and are finite numbers
# (GHSA-2rv4-xv66-fpjg): float('nan') passes `< 0` because nan < 0 is False.
if data.max_budget is not None and (not math.isfinite(data.max_budget) or data.max_budget < 0):
raise HTTPException(
status_code=400,
detail={"error": f"max_budget must be a non-negative finite number. Received: {data.max_budget}"},
)
if data.soft_budget is not None and (not math.isfinite(data.soft_budget) or data.soft_budget < 0):
raise HTTPException(
status_code=400,
detail={"error": f"soft_budget must be a non-negative finite number. Received: {data.soft_budget}"},
)
custom_key_generate_hook: Final[Callable[..., Awaitable[Mapping[str, object]]] | None] = (
user_custom_key_generate
)
if custom_key_generate_hook is not None:
if inspect.iscoroutinefunction(custom_key_generate_hook):
result: Final = await custom_key_generate_hook(data)
else:
raise ValueError("user_custom_key_generate must be a coroutine")
decision: Final = result.get("decision", True)
message: Final = result.get("message", "Authentication Failed - Custom Auth Rule")
if not decision:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message)
_check_allowed_routes_caller_permission(View on GitHub (pinned to 77b7c6c40c)
Solutions
- Send a finite soft_budget >= 0 (e.g. 100) or omit the field entirely to leave it unset
- If the value is computed, guard the arithmetic: default to 0 or None when the input is NaN/inf before calling the API
- Sanitize deserialized JSON: reject or clamp float('nan')/float('inf') client-side since Python json.loads accepts NaN/Infinity by default
- Upgrade to a patched LiteLLM version if you relied on the old behavior that accepted NaN
Example fix
# before
soft_budget = remaining / count # count == 0 -> ZeroDivisionError or nan from bad data
await client.post("/key/generate", json={"soft_budget": soft_budget, ...})
# after
import math
soft_budget = remaining / count if count else None
if soft_budget is not None and not math.isfinite(soft_budget):
soft_budget = None # or raise your own validation error
await client.post("/key/generate", json={"soft_budget": soft_budget, ...}) Defensive patterns
Strategy: validation
Validate before calling
import math
def valid_budget(v) -> bool:
return v is None or (isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and v >= 0)
payload = {"soft_budget": sb for sb in [computed] if valid_budget(sb)}
# omit the key entirely when invalid: json.loads accepts NaN/Infinity, so guard deserialized values too Type guard
def is_finite_non_negative(v: object) -> bool:
return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and v >= 0 Try / catch
try:
r = await client.post("/key/generate", json=payload)
except httpx.HTTPStatusError as e:
if e.response.status_code == 400 and "soft_budget" in e.response.text:
fix_budget_and_retry(e)
raise Prevention
- Never send computed budget values without a math.isfinite check; Python json.loads accepts NaN/Infinity tokens
- Omit soft_budget rather than sending sentinel values like -1 or inf for 'none'
- Centralize budget sanitization (clamp at 0, null on NaN) in one helper used by every script that talks to /key/generate
When it happens
Trigger: POST /key/generate with body {"soft_budget": -1}; passing JSON float NaN or Infinity (e.g. computed from 0/0 arithmetic, or deserialized from "NaN"/"Infinity" which Python's json module accepts); supplying a soft_budget string like "inf" that a pre-processing step coerces to a float; any budget value derived from user input that was never sanitized.
Common situations: Dashboards that compute soft_budget = remaining_quota / num_users and divide by zero; configs migrated from older LiteLLM versions where NaN slipped through and got stored; tests posting float('nan') directly; YAML/JSON configs using '.inf' or 'Infinity' literals which YAML parses to float inf.
Related errors
- max_budget must be a non-negative finite number. Received: {
- max_budget cannot be negative. Received: {data.max_budget}
- soft_budget cannot be negative. Received: {data.soft_budget}
- soft_budget ({data.soft_budget}) must be strictly lower than
- Project max_budget ({data.max_budget}) exceeds team's max_bu
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/e5e1f0ccc5ffd202.
Report an issue: GitHub.