{"record":{"id":"cc1d42f0681c78e8","repo":"BerriAI/litellm","slug":"spend-must-be-a-finite-number-received-spend","errorCode":null,"errorMessage":"spend must be a finite number. Received: {spend}","messagePattern":"spend must be a finite number\\. Received: (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/management_endpoints/common_utils.py","lineNumber":19,"sourceCode":"import math\nfrom typing import TYPE_CHECKING, Any, Final, Optional, Union\n\nfrom fastapi import HTTPException, status\nfrom pydantic import BaseModel\n\n\n# Defined above the `litellm.proxy.*` imports so the name is bound even when\n# this module is imported first through the proxy import cycle (CodeQL:\n# module-level cyclic import). Depends only on `math` + `HTTPException`.\ndef validate_finite_spend(spend: float | None) -> None:\n    \"\"\"Reject NaN/±inf spend before it reaches the DB / spend counter.\n\n    A non-finite spend would otherwise slip past `spend >= max_budget`\n    enforcement, since any comparison with NaN (and `-inf >= max_budget`)\n    is False, letting the entity keep spending past its configured budget.\n    \"\"\"\n    if spend is not None and not math.isfinite(spend):\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": f\"spend must be a finite number. Received: {spend}\"},\n        )\n\n\ndef validate_budget_duration(budget_duration: str | None) -> None:\n    \"\"\"Reject budget durations that can't be parsed, are non-positive, or\n    overflow date math, so a bad value can't be persisted and later crash the\n    budget reset job.\n\n    A non-positive duration also resolves to a reset time of \"now\", which leaves\n    the row permanently due: the reset job re-reads it every tick and, once\n    enough of them exist, they fill each batch and starve every other tenant's\n    reset.\n    \"\"\"\n    if budget_duration is None:\n        return\n","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/common_utils.py#L1-L37","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\ncurl -X POST http://localhost:4000/key/generate -d '{\"max_budget\": 100, \"spend\": NaN}'\n# 400 spend must be a finite number. Received: nan\n\n# after\ncurl -X POST http://localhost:4000/key/generate -d '{\"max_budget\": 100, \"spend\": 0}'","handlingStrategy":"validation","validationCode":"import math, json\n\ndef sanitize_payload(payload: dict) -> dict:\n    for k, v in payload.items():\n        if isinstance(v, float) and not math.isfinite(v):\n            raise ValueError(f\"non-finite value for {k!r}: {v!r}; use 0 or a finite cap\")\n    return payload\n\npayload = sanitize_payload({\"max_budget\": 100, \"spend\": parse_spend(input_data)})","typeGuard":"import math\nfrom typing import TypeGuard\n\ndef is_finite_spend(value: object) -> TypeGuard[float]:\n    \"\"\"Narrows to a float/int that the proxy will accept as spend.\"\"\"\n    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)","tryCatchPattern":"import httpx\n\ntry:\n    r = httpx.post(f\"{PROXY_URL}/key/generate\", json=payload, headers=hdrs)\n    r.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"finite number\" in e.response.text:\n        payload[\"spend\"] = 0  # replace NaN/inf with a finite default and retry once\n        r = httpx.post(f\"{PROXY_URL}/key/generate\", json=payload, headers=hdrs)\n        r.raise_for_status()\n    else:\n        raise","preventionTips":["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"],"tags":["litellm-proxy","validation","nan","numbers","security","budget-management"],"backgroundTag":"nan-value-rejected","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}