Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/budget_management_endpoints.py: 82%
108 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2BUDGET MANAGEMENT
4All /budget management endpoints
6/budget/new
7/budget/info
8/budget/update
9/budget/delete
10/budget/settings
11/budget/list
12"""
14#### BUDGET TABLE MANAGEMENT ####
15import math
16from collections.abc import Mapping
17from types import MappingProxyType
18from typing import Final
20from fastapi import APIRouter, Depends, HTTPException
22from litellm.proxy._types import *
23from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
24from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
25from litellm.proxy.management_endpoints.common_utils import (
26 _user_has_admin_view,
27 validate_budget_duration,
28)
29from litellm.proxy.utils import jsonify_object
30from litellm.repositories.budget_repository import BudgetRepository
32router: Final = APIRouter()
35@router.post(
36 "/budget/new",
37 tags=["budget management"],
38 dependencies=[Depends(user_api_key_auth)],
39)
40async def new_budget(
41 budget_obj: BudgetNewRequest,
42 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
43):
44 """
45 Create a new budget object. Can apply this to teams, orgs, end-users, keys.
47 Parameters:
48 - budget_duration: Optional[str] - Budget reset period ("30d", "1h", etc.)
49 - budget_id: Optional[str] - The id of the budget. If not provided, a new id will be generated.
50 - max_budget: Optional[float] - The max budget for the budget.
51 - soft_budget: Optional[float] - The soft budget for the budget.
52 - max_parallel_requests: Optional[int] - The max number of parallel requests for the budget.
53 - tpm_limit: Optional[int] - The tokens per minute limit for the budget.
54 - rpm_limit: Optional[int] - The requests per minute limit for the budget.
55 - tpd_limit: Optional[int] - The tokens per day limit for the budget. Charged by batch submissions instead of tpm_limit/rpm_limit.
56 - model_max_budget: Optional[dict] - Specify max budget for a given model. Example: {"openai/gpt-4o-mini": {"max_budget": 100.0, "budget_duration": "1d", "tpm_limit": 100000, "rpm_limit": 100000}}
57 - budget_reset_at: Optional[datetime] - Datetime when the initial budget is reset. Default is now.
58 """
59 from prisma.errors import UniqueViolationError
61 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
63 if prisma_client is None: 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 raise HTTPException(
65 status_code=500,
66 detail={"error": CommonProxyErrors.db_not_connected_error.value},
67 )
69 # Validate budget values are not negative
70 if budget_obj.max_budget is not None and (not math.isfinite(budget_obj.max_budget) or budget_obj.max_budget < 0):
71 raise HTTPException(
72 status_code=400,
73 detail={"error": f"max_budget must be a non-negative finite number. Received: {budget_obj.max_budget}"},
74 )
75 if budget_obj.soft_budget is not None and (not math.isfinite(budget_obj.soft_budget) or budget_obj.soft_budget < 0):
76 raise HTTPException(
77 status_code=400,
78 detail={"error": f"soft_budget must be a non-negative finite number. Received: {budget_obj.soft_budget}"},
79 )
81 validate_budget_duration(budget_obj.budget_duration)
83 # Validate model_max_budget if present
84 if budget_obj.model_max_budget is not None and len(budget_obj.model_max_budget) > 0:
85 from litellm.proxy.management_endpoints.key_management_endpoints import (
86 validate_model_max_budget,
87 )
89 try:
90 validate_model_max_budget(budget_obj.model_max_budget)
91 except ValueError as e:
92 raise HTTPException(status_code=400, detail={"error": str(e)})
94 # if no budget_reset_at date is set, but a budget_duration is given, then set budget_reset_at initially to the first completed duration interval in future
95 if budget_obj.budget_reset_at is None and budget_obj.budget_duration is not None: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 budget_obj.budget_reset_at = get_budget_reset_time(budget_duration=budget_obj.budget_duration)
98 budget_obj_json: Final = budget_obj.model_dump(exclude_none=True)
99 budget_obj_jsonified: Final[dict[str, object]] = jsonify_object(budget_obj_json) # mutable-ok: prisma create input
100 try:
101 response: Final = await BudgetRepository(prisma_client).table.create(
102 data={
103 **budget_obj_jsonified,
104 "created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
105 "updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
106 }
107 )
108 except Exception as e:
109 if not isinstance(e, UniqueViolationError): 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 raise
111 raise HTTPException(
112 status_code=400,
113 detail={"error": f"Budget with id '{budget_obj.budget_id}' already exists."},
114 )
116 return response
119@router.post(
120 "/budget/update",
121 tags=["budget management"],
122 dependencies=[Depends(user_api_key_auth)],
123)
124async def update_budget(
125 budget_obj: BudgetNewRequest,
126 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
127):
128 """
129 Update an existing budget object.
131 Parameters:
132 - budget_duration: Optional[str] - Budget reset period ("30d", "1h", etc.)
133 - budget_id: Optional[str] - The id of the budget. If not provided, a new id will be generated.
134 - max_budget: Optional[float] - The max budget for the budget.
135 - soft_budget: Optional[float] - The soft budget for the budget.
136 - max_parallel_requests: Optional[int] - The max number of parallel requests for the budget.
137 - tpm_limit: Optional[int] - The tokens per minute limit for the budget.
138 - rpm_limit: Optional[int] - The requests per minute limit for the budget.
139 - tpd_limit: Optional[int] - The tokens per day limit for the budget. Charged by batch submissions instead of tpm_limit/rpm_limit.
140 - model_max_budget: Optional[dict] - Specify max budget for a given model. Example: {"openai/gpt-4o-mini": {"max_budget": 100.0, "budget_duration": "1d", "tpm_limit": 100000, "rpm_limit": 100000}}
141 - budget_reset_at: Optional[datetime] - Update the Datetime when the budget was last reset.
142 """
143 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
145 if prisma_client is None: 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 raise HTTPException(
147 status_code=500,
148 detail={"error": CommonProxyErrors.db_not_connected_error.value},
149 )
150 if budget_obj.budget_id is None:
151 raise HTTPException(status_code=400, detail={"error": "budget_id is required"})
153 # Validate budget values are not negative
154 if budget_obj.max_budget is not None and (not math.isfinite(budget_obj.max_budget) or budget_obj.max_budget < 0):
155 raise HTTPException(
156 status_code=400,
157 detail={"error": f"max_budget must be a non-negative finite number. Received: {budget_obj.max_budget}"},
158 )
159 if budget_obj.soft_budget is not None and (not math.isfinite(budget_obj.soft_budget) or budget_obj.soft_budget < 0):
160 raise HTTPException(
161 status_code=400,
162 detail={"error": f"soft_budget must be a non-negative finite number. Received: {budget_obj.soft_budget}"},
163 )
165 validate_budget_duration(budget_obj.budget_duration)
167 # Validate model_max_budget if present in update
168 if budget_obj.model_max_budget is not None and len(budget_obj.model_max_budget) > 0: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 from litellm.proxy.management_endpoints.key_management_endpoints import (
170 validate_model_max_budget,
171 )
173 try:
174 validate_model_max_budget(budget_obj.model_max_budget)
175 except ValueError as e:
176 raise HTTPException(status_code=400, detail={"error": str(e)})
178 # recompute budget_reset_at when the duration changes, unless the caller pinned a reset time explicitly
179 recomputed_reset_at: Final = (
180 {"budget_reset_at": get_budget_reset_time(budget_duration=budget_obj.budget_duration)}
181 if budget_obj.budget_duration is not None and "budget_reset_at" not in budget_obj.model_fields_set
182 else MappingProxyType({"budget_reset_at": None})
183 if "budget_duration" in budget_obj.model_fields_set
184 and budget_obj.budget_duration is None
185 and "budget_reset_at" not in budget_obj.model_fields_set
186 else {}
187 )
189 budget_obj_jsonified: Final[Mapping[str, object]] = jsonify_object(
190 {
191 **budget_obj.model_dump(exclude_unset=True),
192 **recomputed_reset_at,
193 "updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
194 }
195 )
197 response: Final = await BudgetRepository(prisma_client).table.update(
198 where={"budget_id": budget_obj.budget_id},
199 data=budget_obj_jsonified,
200 )
202 return response
205@router.post(
206 "/budget/info",
207 tags=["budget management"],
208 dependencies=[Depends(user_api_key_auth)],
209)
210async def info_budget(data: BudgetRequest):
211 """
212 Get the budget id specific information
214 Parameters:
215 - budgets: List[str] - The list of budget ids to get information for
216 """
217 from litellm.proxy.proxy_server import prisma_client
219 if prisma_client is None: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 raise HTTPException(status_code=500, detail={"error": "No db connected"})
222 if len(data.budgets) == 0:
223 raise HTTPException(
224 status_code=400,
225 detail={"error": f"Specify list of budget id's to query. Passed in={data.budgets}"},
226 )
227 response: Final = await BudgetRepository(prisma_client).table.find_many(
228 where={"budget_id": {"in": data.budgets}},
229 )
231 return response
234@router.get(
235 "/budget/settings",
236 tags=["budget management"],
237 dependencies=[Depends(user_api_key_auth)],
238)
239async def budget_settings(
240 budget_id: str,
241 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
242):
243 """
244 Get list of configurable params + current value for a budget item + description of each field
246 Used on Admin UI.
248 Query Parameters:
249 - budget_id: str - The budget id to get information for
250 """
251 from litellm.proxy.proxy_server import prisma_client
253 if prisma_client is None: 253 ↛ 254line 253 didn't jump to line 254 because the condition on line 253 was never true
254 raise HTTPException(
255 status_code=400,
256 detail={"error": CommonProxyErrors.db_not_connected_error.value},
257 )
259 if not _user_has_admin_view(user_api_key_dict): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 raise HTTPException(
261 status_code=400,
262 detail={"error": f"{CommonProxyErrors.not_allowed_access.value}, your role={user_api_key_dict.user_role}"},
263 )
265 ## get budget item from db
266 db_budget_row: Final = await BudgetRepository(prisma_client).table.find_first(where={"budget_id": budget_id})
268 if db_budget_row is not None:
269 db_budget_row_dict = db_budget_row.model_dump(exclude_none=True)
270 else:
271 db_budget_row_dict = {}
273 allowed_args: Final = {
274 "max_parallel_requests": {"type": "Integer"},
275 "tpm_limit": {"type": "Integer"},
276 "rpm_limit": {"type": "Integer"},
277 "tpd_limit": {"type": "Integer"},
278 "budget_duration": {"type": "String"},
279 "max_budget": {"type": "Float"},
280 "soft_budget": {"type": "Float"},
281 "model_max_budget": {"type": "Object"},
282 }
284 return_val: Final = []
286 for field_name, field_info in BudgetNewRequest.model_fields.items():
287 if field_name in allowed_args:
288 _stored_in_db = True
290 _response_obj = ConfigList(
291 field_name=field_name,
292 field_type=allowed_args[field_name]["type"],
293 field_description=field_info.description or "",
294 field_value=db_budget_row_dict.get(field_name, None),
295 stored_in_db=_stored_in_db,
296 field_default_value=field_info.default,
297 )
298 return_val.append(_response_obj)
300 return return_val
303@router.get(
304 "/budget/list",
305 tags=["budget management"],
306 dependencies=[Depends(user_api_key_auth)],
307)
308async def list_budget(
309 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
310):
311 """List all the created budgets in proxy db. Used on Admin UI."""
312 from litellm.proxy.proxy_server import prisma_client
314 if prisma_client is None: 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true
315 raise HTTPException(
316 status_code=400,
317 detail={"error": CommonProxyErrors.db_not_connected_error.value},
318 )
320 if not _user_has_admin_view(user_api_key_dict): 320 ↛ 321line 320 didn't jump to line 321 because the condition on line 320 was never true
321 raise HTTPException(
322 status_code=400,
323 detail={"error": f"{CommonProxyErrors.not_allowed_access.value}, your role={user_api_key_dict.user_role}"},
324 )
326 response: Final = await BudgetRepository(prisma_client).table.find_many()
328 return response
331@router.post(
332 "/budget/delete",
333 tags=["budget management"],
334 dependencies=[Depends(user_api_key_auth)],
335)
336async def delete_budget(
337 data: BudgetDeleteRequest,
338 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
339):
340 """
341 Delete budget
343 Parameters:
344 - id: str - The budget id to delete
345 """
346 from litellm.proxy.proxy_server import prisma_client
348 if prisma_client is None: 348 ↛ 349line 348 didn't jump to line 349 because the condition on line 348 was never true
349 raise HTTPException(
350 status_code=500,
351 detail={"error": CommonProxyErrors.db_not_connected_error.value},
352 )
354 if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 354 ↛ 355line 354 didn't jump to line 355 because the condition on line 354 was never true
355 raise HTTPException(
356 status_code=400,
357 detail={"error": f"{CommonProxyErrors.not_allowed_access.value}, your role={user_api_key_dict.user_role}"},
358 )
360 response: Final = await BudgetRepository(prisma_client).table.delete(where={"budget_id": data.id})
362 return response