{"record":{"id":"227f15aa523657e1","repo":"BerriAI/litellm","slug":"max-budget-must-be-a-non-negative-finite-number-r-227f15","errorCode":null,"errorMessage":"max_budget must be a non-negative finite number. Received: {max_budget}","messagePattern":"max_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":2135,"sourceCode":"    if data.team_id is None:\n        return False\n    if existing_key_row.team_id is None:\n        return True\n    return data.team_id != existing_key_row.team_id\n\n\ndef _validate_max_budget(max_budget: float | None) -> None:\n    \"\"\"\n    Validate that max_budget is not negative.\n\n    Args:\n        max_budget: The max_budget value to validate\n\n    Raises:\n        HTTPException: If max_budget is negative\n    \"\"\"\n    if max_budget is not None and (not math.isfinite(max_budget) or max_budget < 0):\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": f\"max_budget must be a non-negative finite number. Received: {max_budget}\"},\n        )\n\n\nasync def _get_and_validate_existing_key(\n    token: str | None, prisma_client: PrismaClient | None, key_alias: str | None = None\n) -> LiteLLM_VerificationToken:\n    \"\"\"\n    Get existing key from database and validate it exists.\n\n    Args:\n        token: The key token to look up\n        prisma_client: Prisma client instance\n        key_alias: Alias to look the key up by when token is not provided\n\n    Returns:\n        LiteLLM_VerificationToken: The existing key row","sourceCodeStart":2117,"sourceCodeEnd":2153,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/key_management_endpoints.py#L2117-L2153","documentation":"_validate_max_budget is the shared guard used on key update paths to reject max_budget values that are negative, NaN, or infinite (the finiteness check closes the nan < 0 == False loophole from GHSA-2rv4-xv66-fpjg). It raises HTTP 400 echoing the bad value. It fires on /key/update-style flows wherever budgets are re-validated before persisting.","triggerScenarios":"POST /key/update with max_budget: -50; sending NaN/Infinity in JSON (Python json accepts NaN, Infinity tokens); computing max_budget as a subtraction that underflows to a negative; unsetting via 0-like sentinel values such as float('-inf') intended to mean 'no limit' (use null/omit instead).","commonSituations":"Billing scripts deriving max_budget = spend_limit - spent which goes negative once over budget; configs using .inf YAML literals for 'unlimited'; migrating old keys whose stored budget is already NaN and re-submitting it verbatim on update.","solutions":["Send a finite max_budget >= 0, or omit/null it when no cap is intended","Clamp computed budgets: max(0, limit - spent) before calling the API","If re-submitting stored values, skip or sanitize non-finite ones first (math.isfinite check)","For 'unlimited', omit max_budget rather than using a huge/inf sentinel"],"exampleFix":"# before\nnew_budget = user_limit - total_spent  # can be -12.5\nawait client.post(\"/key/update\", json={\"key\": k, \"max_budget\": new_budget})\n\n# after\nimport math\nnew_budget = max(0.0, user_limit - total_spent)\nif not math.isfinite(new_budget):\n    new_budget = None\nawait client.post(\"/key/update\", json={\"key\": k, \"max_budget\": new_budget})","handlingStrategy":"validation","validationCode":"import math\n\ndef sanitize_budget(v):\n    if v is None:\n        return None\n    v = float(v)\n    return v if math.isfinite(v) and v >= 0 else None  # None = no limit\n\npayload[\"max_budget\"] = sanitize_budget(computed_limit)","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/update\", json=payload)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"max_budget\" in e.response.text:\n        payload[\"max_budget\"] = max(0, math.floor(payload[\"max_budget\"]))\n        r = await client.post(\"/key/update\", json=payload)\n    else:\n        raise","preventionTips":["Clamp arithmetic budgets: max(0, limit - spent) before any API call","Represent 'unlimited' as an omitted/null max_budget, never inf or a huge sentinel","Guard against NaN in deserialized JSON payloads (json.loads accepts NaN/Infinity by default)"],"tags":["litellm","proxy","validation","budget","key-update"],"backgroundTag":"request-validation-failed","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}