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
- Send finite numbers only: use 0 for 'no spend yet' and a large finite cap instead of infinity
- Add a client-side isfinite check on every numeric spend field before the request
- If the bad value originated upstream (cost pipeline), fix the producer so NaN/inf never reaches the proxy API
- 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
- Use 0 for 'no spend yet', never NaN; use a large finite number instead of infinity
- Run json.dumps(..., allow_nan=False) client-side to catch NaN before it leaves the process
- Fix upstream cost pipelines that emit inf on zero-cost division
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
- git-subdir 'path' must be a relative path of the form 'segme
- Invalid budget_duration '{budget_duration}'. Use a format li
- mcp_tools_config must be a list of dictionaries
- GitHub source must include 'repo' field (e.g., 'org/repo')
- URL source must include 'url' field (e.g., 'https://github.c
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/cc1d42f0681c78e8.
Report an issue: GitHub.