BerriAI/litellm · error · HTTPException

max_budget must be a non-negative finite number. Received: {

Error message

max_budget must be a non-negative finite number. Received: {max_budget}

What it means

_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.

Source

Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:2135

    if data.team_id is None:
        return False
    if existing_key_row.team_id is None:
        return True
    return data.team_id != existing_key_row.team_id


def _validate_max_budget(max_budget: float | None) -> None:
    """
    Validate that max_budget is not negative.

    Args:
        max_budget: The max_budget value to validate

    Raises:
        HTTPException: If max_budget is negative
    """
    if max_budget is not None and (not math.isfinite(max_budget) or max_budget < 0):
        raise HTTPException(
            status_code=400,
            detail={"error": f"max_budget must be a non-negative finite number. Received: {max_budget}"},
        )


async def _get_and_validate_existing_key(
    token: str | None, prisma_client: PrismaClient | None, key_alias: str | None = None
) -> LiteLLM_VerificationToken:
    """
    Get existing key from database and validate it exists.

    Args:
        token: The key token to look up
        prisma_client: Prisma client instance
        key_alias: Alias to look the key up by when token is not provided

    Returns:
        LiteLLM_VerificationToken: The existing key row

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Send a finite max_budget >= 0, or omit/null it when no cap is intended
  2. Clamp computed budgets: max(0, limit - spent) before calling the API
  3. If re-submitting stored values, skip or sanitize non-finite ones first (math.isfinite check)
  4. For 'unlimited', omit max_budget rather than using a huge/inf sentinel

Example fix

# before
new_budget = user_limit - total_spent  # can be -12.5
await client.post("/key/update", json={"key": k, "max_budget": new_budget})

# after
import math
new_budget = max(0.0, user_limit - total_spent)
if not math.isfinite(new_budget):
    new_budget = None
await client.post("/key/update", json={"key": k, "max_budget": new_budget})
Defensive patterns

Strategy: validation

Validate before calling

import math

def sanitize_budget(v):
    if v is None:
        return None
    v = float(v)
    return v if math.isfinite(v) and v >= 0 else None  # None = no limit

payload["max_budget"] = sanitize_budget(computed_limit)

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/update", json=payload)
except httpx.HTTPStatusError as e:
    if e.response.status_code == 400 and "max_budget" in e.response.text:
        payload["max_budget"] = max(0, math.floor(payload["max_budget"]))
        r = await client.post("/key/update", json=payload)
    else:
        raise

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/227f15aa523657e1. Report an issue: GitHub.