BerriAI/litellm · error · HTTPException

spend must be a finite number. Received: {spend}

Error message

spend must be a finite number. Received: {spend}

What it means

validate_finite_spend is called on management paths that accept a spend value (key/user/team/budget create-update) and rejects NaN and ±infinity with HTTP 400 before the value reaches the DB or the in-memory spend counter. This is a security fix as well as a data-integrity one: NaN compares False against any max_budget, so a non-finite spend would let an entity spend past its budget forever. Notably FastAPI/Pydantic accept the JSON strings "NaN" and "Infinity" for float fields, so such values pass schema validation and are stopped only here.

Source

Thrown at litellm/proxy/management_endpoints/common_utils.py:19

import math
from typing import TYPE_CHECKING, Any, Final, 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: float | None) -> 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}"},
        )


def validate_budget_duration(budget_duration: str | None) -> None:
    """Reject budget durations that can't be parsed, are non-positive, or
    overflow date math, so a bad value can't be persisted and later crash the
    budget reset job.

    A non-positive duration also resolves to a reset time of "now", which leaves
    the row permanently due: the reset job re-reads it every tick and, once
    enough of them exist, they fill each batch and starve every other tenant's
    reset.
    """
    if budget_duration is None:
        return

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Send finite numbers only: use 0 for 'no spend yet' and a large finite cap instead of infinity
  2. Add a client-side isfinite check on every numeric spend field before the request
  3. If the bad value originated upstream (cost pipeline), fix the producer so NaN/inf never reaches the proxy API
  4. If you intended 'unlimited', omit max_budget/spend constraints rather than encoding infinity

Example fix

# before
curl -X POST http://localhost:4000/key/generate -d '{"max_budget": 100, "spend": NaN}'
# 400 spend must be a finite number. Received: nan

# after
curl -X POST http://localhost:4000/key/generate -d '{"max_budget": 100, "spend": 0}'
Defensive patterns

Strategy: validation

Validate before calling

import math, json

def sanitize_payload(payload: dict) -> dict:
    for k, v in payload.items():
        if isinstance(v, float) and not math.isfinite(v):
            raise ValueError(f"non-finite value for {k!r}: {v!r}; use 0 or a finite cap")
    return payload

payload = sanitize_payload({"max_budget": 100, "spend": parse_spend(input_data)})

Type guard

import math
from typing import TypeGuard

def is_finite_spend(value: object) -> TypeGuard[float]:
    """Narrows to a float/int that the proxy will accept as spend."""
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)

Try / catch

import httpx

try:
    r = httpx.post(f"{PROXY_URL}/key/generate", json=payload, headers=hdrs)
    r.raise_for_status()
except httpx.HTTPStatusError as e:
    if e.response.status_code == 400 and "finite number" in e.response.text:
        payload["spend"] = 0  # replace NaN/inf with a finite default and retry once
        r = httpx.post(f"{PROXY_URL}/key/generate", json=payload, headers=hdrs)
        r.raise_for_status()
    else:
        raise

Prevention

When it happens

Trigger: POST/PATCH on /key, /user, /team, or budget endpoints with "spend": "NaN" or "Infinity" (or Python float('nan') client-side serialized that way); payloads deserialized from untrusted telemetry that injected non-finite floats; test fixtures using float('inf') as 'unlimited' spend.

Common situations: Clients encoding 'no spend yet' as NaN instead of 0; upstream cost trackers emitting inf when a model returns zero-cost divisions; JSON libraries that emit NaN/Infinity tokens (non-strict JSON) leaking into proxy payloads.

Related errors


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