{"record":{"id":"e5e1f0ccc5ffd202","repo":"BerriAI/litellm","slug":"soft-budget-must-be-a-non-negative-finite-number-e5e1f0","errorCode":null,"errorMessage":"soft_budget must be a non-negative finite number. Received: {data.soft_budget}","messagePattern":"soft_budget must be a non-negative finite number\\. Received: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/management_endpoints/key_management_endpoints.py","lineNumber":1695,"sourceCode":"        if prisma_client is None:\n            raise HTTPException(\n                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,\n                detail={\"error\": CommonProxyErrors.db_not_connected_error.value},\n            )\n\n        verbose_proxy_logger.debug(\"entered /key/generate\")\n\n        await check_org_admin_can_generate_keys(user_api_key_dict=user_api_key_dict)\n\n        # Validate budget values are not negative and are finite numbers\n        # (GHSA-2rv4-xv66-fpjg): float('nan') passes `< 0` because nan < 0 is False.\n        if data.max_budget is not None and (not math.isfinite(data.max_budget) or data.max_budget < 0):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": f\"max_budget must be a non-negative finite number. Received: {data.max_budget}\"},\n            )\n        if data.soft_budget is not None and (not math.isfinite(data.soft_budget) or data.soft_budget < 0):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": f\"soft_budget must be a non-negative finite number. Received: {data.soft_budget}\"},\n            )\n\n        custom_key_generate_hook: Final[Callable[..., Awaitable[Mapping[str, object]]] | None] = (\n            user_custom_key_generate\n        )\n        if custom_key_generate_hook is not None:\n            if inspect.iscoroutinefunction(custom_key_generate_hook):\n                result: Final = await custom_key_generate_hook(data)\n            else:\n                raise ValueError(\"user_custom_key_generate must be a coroutine\")\n            decision: Final = result.get(\"decision\", True)\n            message: Final = result.get(\"message\", \"Authentication Failed - Custom Auth Rule\")\n            if not decision:\n                raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message)\n\n        _check_allowed_routes_caller_permission(","sourceCodeStart":1677,"sourceCodeEnd":1713,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/key_management_endpoints.py#L1677-L1713","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nsoft_budget = remaining / count  # count == 0 -> ZeroDivisionError or nan from bad data\nawait client.post(\"/key/generate\", json={\"soft_budget\": soft_budget, ...})\n\n# after\nimport math\nsoft_budget = remaining / count if count else None\nif soft_budget is not None and not math.isfinite(soft_budget):\n    soft_budget = None  # or raise your own validation error\nawait client.post(\"/key/generate\", json={\"soft_budget\": soft_budget, ...})","handlingStrategy":"validation","validationCode":"import math\n\ndef valid_budget(v) -> bool:\n    return v is None or (isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and v >= 0)\n\npayload = {\"soft_budget\": sb for sb in [computed] if valid_budget(sb)}\n# omit the key entirely when invalid: json.loads accepts NaN/Infinity, so guard deserialized values too","typeGuard":"def is_finite_non_negative(v: object) -> bool:\n    return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) and v >= 0","tryCatchPattern":"try:\n    r = await client.post(\"/key/generate\", json=payload)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"soft_budget\" in e.response.text:\n        fix_budget_and_retry(e)\n    raise","preventionTips":["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"],"tags":["litellm","proxy","virtual-key","validation","budget"],"backgroundTag":"request-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}