mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(budget): serialize model_max_budget before the /budget/update write
/budget/update handed prisma the raw update dict, so a model_max_budget payload reached the Json? column as a nested python dict. prisma-client-py renders that into the GraphQL mutation as bare object keys rather than a JSON string, and the query engine rejects it, so every per-model budget update returned a 500 and the cap was never stored. Model ids carrying punctuation (glm-5.2) also produced an invalid GraphQL name. /budget/new already ran its payload through jsonify_object for exactly this reason. Do the same on the update path. Team member and organization member budget updates route through this handler too, so they were failing the same way. The existing unit tests mocked the prisma table with an AsyncMock that accepts any dict, which is why this never showed up outside a live proxy. The new test asserts on what the endpoint hands prisma.
This commit is contained in:
parent
f677292901
commit
4d6786d420
2 changed files with 41 additions and 4 deletions
|
|
@ -13,6 +13,7 @@ All /budget management endpoints
|
|||
|
||||
#### BUDGET TABLE MANAGEMENT ####
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
|
@ -178,13 +179,17 @@ async def update_budget(
|
|||
else {}
|
||||
)
|
||||
|
||||
response: Final = await BudgetRepository(prisma_client).table.update(
|
||||
where={"budget_id": budget_obj.budget_id},
|
||||
data={
|
||||
budget_obj_jsonified: Final[Mapping[str, object]] = jsonify_object(
|
||||
{
|
||||
**budget_obj.model_dump(exclude_unset=True),
|
||||
**recomputed_reset_at,
|
||||
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
response: Final = await BudgetRepository(prisma_client).table.update(
|
||||
where={"budget_id": budget_obj.budget_id},
|
||||
data=budget_obj_jsonified,
|
||||
)
|
||||
|
||||
return response
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# tests/test_budget_endpoints.py
|
||||
|
||||
import json
|
||||
import types
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
|
|
@ -388,3 +389,34 @@ async def test_update_budget_duration_none_does_not_recompute(client_and_mocks):
|
|||
|
||||
assert "budget_duration" in captured and captured["budget_duration"] is None
|
||||
assert "budget_reset_at" not in captured
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_budget_serializes_model_max_budget_for_prisma(
|
||||
client_and_mocks, monkeypatch
|
||||
):
|
||||
monkeypatch.setattr(ps, "premium_user", True)
|
||||
|
||||
client, _, mock_table = client_and_mocks
|
||||
captured = _capture_update_data(mock_table)
|
||||
|
||||
resp = client.post(
|
||||
"/budget/update",
|
||||
json={
|
||||
"budget_id": "budget_per_model",
|
||||
"model_max_budget": {
|
||||
"gpt4o": {"budget_limit": 5.0, "time_period": "1d"},
|
||||
"glm-5.2": {"budget_limit": 7.5, "time_period": "30d"},
|
||||
},
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
stored = captured["model_max_budget"]
|
||||
assert isinstance(stored, str), (
|
||||
f"model_max_budget must reach prisma as a JSON string, got {type(stored).__name__}"
|
||||
)
|
||||
assert json.loads(stored) == {
|
||||
"gpt4o": {"max_budget": 5.0, "budget_duration": "1d"},
|
||||
"glm-5.2": {"max_budget": 7.5, "budget_duration": "30d"},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue