fix(ui): type the patch response's JSON columns as the shapes the details panel reads

The generated UserItem calls metadata and model_max_budget free-form objects, which is true
of the columns and not assignable to what the details panel reads, so the response type now
restates them as the shapes /v2/user/info already claims.
This commit is contained in:
ryan-crabbe-berri 2026-08-28 12:46:58 -07:00
parent 7080d51c39
commit ae263fdcfc
3 changed files with 22 additions and 69 deletions

View file

@ -2670,7 +2670,7 @@ async def test_user_info_v2_proxy_admin_can_query_any_user(mocker, user_rows):
@pytest.mark.asyncio
async def test_user_info_v2_returns_rate_limits(mocker):
async def test_user_info_v2_returns_rate_limits(mocker, user_rows):
"""
The Admin UI seeds its edit form from this response, so a limit missing here reads
to the operator as "not set" and a save silently wipes it.
@ -2679,21 +2679,17 @@ async def test_user_info_v2_returns_rate_limits(mocker):
from litellm.proxy.management_endpoints.internal_user_endpoints import user_info_v2
mock_prisma_client = mocker.MagicMock()
mock_user_row = mocker.MagicMock()
mock_user_row.model_dump.return_value = {
"user_id": "limited-user",
"tpm_limit": 12000,
"rpm_limit": 60,
"max_parallel_requests": 3,
"teams": [],
}
mock_prisma_client.db.litellm_usertable.find_unique = mocker.AsyncMock(
return_value=mock_user_row
user_rows(
**{
"limited-user": {
"user_id": "limited-user",
"tpm_limit": 12000,
"rpm_limit": 60,
"max_parallel_requests": 3,
"teams": [],
}
}
)
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
response = await user_info_v2(
request=mocker.MagicMock(spec=Request),

View file

@ -1082,8 +1082,17 @@ export interface UserInfoV2Response {
*/
export type UserPatchRequest = components["schemas"]["UserPatchRequest"];
/** The user row as the patch wrote it. Narrower than UserInfoV2Response: no keys, teams or usage. */
export type UserPatchResponse = components["schemas"]["UserItem"];
/**
* The user row as the patch wrote it. Narrower than UserInfoV2Response: no keys, teams or usage.
*
* The two JSON columns are re-stated as the shapes the rest of the dashboard reads them as. The
* generated spec calls them free-form objects, which is true of the column and useless to a caller,
* and it is the same claim /v2/user/info already makes about the same two columns.
*/
export type UserPatchResponse = Omit<components["schemas"]["UserItem"], "metadata" | "model_max_budget"> & {
metadata?: Record<string, any>;
model_max_budget?: ModelMaxBudget;
};
/**
* Partially update one internal user. Unlike userUpdateUserCall, a null here actually clears.

View file

@ -37809,58 +37809,6 @@ export interface components {
/** User Role */
user_role?: string | null;
};
/**
* UserItem
* @description One internal user as the control plane returns it, read back off the row the write produced.
*
* Re-reading rather than echoing the request is the point of the endpoint: a caller can tell a
* clear that landed from one that was dropped by looking at the response.
*/
UserItem: {
/** Budget Duration */
budget_duration?: string | null;
/** Budget Reset At */
budget_reset_at?: string | null;
/** Created At */
created_at?: string | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
max_parallel_requests?: number | null;
/** Metadata */
metadata?: {
[key: string]: components["schemas"]["JsonValue"];
};
/** Model Max Budget */
model_max_budget?: {
[key: string]: components["schemas"]["JsonValue"];
};
/** Models */
models?: string[];
/** Object Permission Id */
object_permission_id?: string | null;
/** Rpm Limit */
rpm_limit?: number | null;
/**
* Spend
* @default 0
*/
spend: number;
/** Teams */
teams?: string[];
/** Tpm Limit */
tpm_limit?: number | null;
/** Updated At */
updated_at?: string | null;
/** User Alias */
user_alias?: string | null;
/** User Email */
user_email?: string | null;
/** User Id */
user_id: string;
/** User Role */
user_role?: string | null;
};
/**
* UserListResponse
* @description Response model for the user list endpoint