mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
feat(team): bulk update team members
This commit is contained in:
parent
173193d717
commit
d252e91823
9 changed files with 580 additions and 5685 deletions
|
|
@ -600,6 +600,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/team/permissions_list",
|
||||
"/team/permissions_update",
|
||||
"/team/permissions_bulk_update",
|
||||
"/team/member/bulk_update",
|
||||
"/team/daily/activity",
|
||||
# model
|
||||
"/model/new",
|
||||
|
|
@ -3704,6 +3705,48 @@ class TeamMemberUpdateResponse(MemberUpdateResponse):
|
|||
allowed_models: Optional[List[str]] = None
|
||||
|
||||
|
||||
class TeamMemberBulkUpdateFields(LiteLLMPydanticObjectBase):
|
||||
max_budget_in_team: Optional[float] = None
|
||||
role: Optional[Literal["admin", "user"]] = None
|
||||
tpm_limit: Optional[int] = None
|
||||
rpm_limit: Optional[int] = None
|
||||
budget_duration: Optional[str] = None
|
||||
allowed_models: Optional[List[str]] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def require_at_least_one_field(self):
|
||||
if not self.model_fields_set:
|
||||
raise ValueError("update_fields must specify at least one field to update")
|
||||
return self
|
||||
|
||||
|
||||
class BulkTeamMemberUpdateRequest(LiteLLMPydanticObjectBase):
|
||||
team_id: str
|
||||
user_ids: Optional[List[str]] = None
|
||||
all_members_in_team: bool = False
|
||||
update_fields: TeamMemberBulkUpdateFields
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_selection(self):
|
||||
has_user_ids = self.user_ids is not None and len(self.user_ids) > 0
|
||||
if has_user_ids and self.all_members_in_team:
|
||||
raise ValueError("Provide either user_ids or all_members_in_team=True, not both")
|
||||
if not has_user_ids and not self.all_members_in_team:
|
||||
raise ValueError("Must provide either user_ids (non-empty) or all_members_in_team=True")
|
||||
return self
|
||||
|
||||
|
||||
class FailedTeamMemberUpdate(MemberUpdateResponse):
|
||||
failed_reason: str
|
||||
|
||||
|
||||
class BulkTeamMemberUpdateResponse(LiteLLMPydanticObjectBase):
|
||||
team_id: str
|
||||
total_requested: int
|
||||
successful_updates: List[TeamMemberUpdateResponse]
|
||||
failed_updates: List[FailedTeamMemberUpdate]
|
||||
|
||||
|
||||
class TeamModelAddRequest(BaseModel):
|
||||
"""Request to add models to a team"""
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ _PROXY_ADMIN_VIEW_ONLY_BLOCKED_ROUTES = frozenset(
|
|||
"/team/unblock",
|
||||
"/team/permissions_update",
|
||||
"/team/permissions_bulk_update",
|
||||
"/team/member/bulk_update",
|
||||
# model
|
||||
"/model/new",
|
||||
"/model/update",
|
||||
|
|
@ -734,6 +735,7 @@ class RouteChecks:
|
|||
"/team/new",
|
||||
"/team/update",
|
||||
"/team/delete",
|
||||
"/team/member/bulk_update",
|
||||
"/model/new",
|
||||
"/model/update",
|
||||
"/model/delete",
|
||||
|
|
|
|||
|
|
@ -28,8 +28,11 @@ from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
|||
from litellm.proxy._types import (
|
||||
UI_TEAM_ID,
|
||||
BlockTeamRequest,
|
||||
BulkTeamMemberUpdateRequest,
|
||||
BulkTeamMemberUpdateResponse,
|
||||
CommonProxyErrors,
|
||||
DeleteTeamRequest,
|
||||
FailedTeamMemberUpdate,
|
||||
LiteLLM_AuditLogs,
|
||||
LiteLLM_DeletedTeamTable,
|
||||
LiteLLM_ManagementEndpoint_MetadataFields,
|
||||
|
|
@ -3002,6 +3005,82 @@ async def team_member_update(
|
|||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/team/member/bulk_update",
|
||||
tags=["team management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=BulkTeamMemberUpdateResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def bulk_update_team_members(
|
||||
data: BulkTeamMemberUpdateRequest,
|
||||
http_request: Request,
|
||||
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
|
||||
):
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
existing_team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
|
||||
if existing_team_row is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "Team id={} does not exist in db".format(data.team_id)},
|
||||
)
|
||||
existing_team = LiteLLM_TeamTable(**existing_team_row.model_dump())
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
|
||||
and not _is_user_team_admin(user_api_key_dict=user_api_key_dict, team_obj=existing_team)
|
||||
and not await _is_user_org_admin_for_team(user_api_key_dict=user_api_key_dict, team_obj=existing_team)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
|
||||
"/team/member/bulk_update", data.team_id
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if data.all_members_in_team:
|
||||
user_ids = [member.user_id for member in existing_team.members_with_roles if member.user_id is not None]
|
||||
else:
|
||||
user_ids = list(dict.fromkeys(data.user_ids or []))
|
||||
|
||||
max_batch_size = 500
|
||||
if len(user_ids) > max_batch_size:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Maximum {} team members can be updated at once. Found {} user_ids.".format(
|
||||
max_batch_size, len(user_ids)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
update_fields = data.update_fields.model_dump(exclude_unset=True)
|
||||
successful_updates: List[TeamMemberUpdateResponse] = []
|
||||
failed_updates: List[FailedTeamMemberUpdate] = []
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
response = await team_member_update(
|
||||
data=TeamMemberUpdateRequest(team_id=data.team_id, user_id=user_id, **update_fields),
|
||||
http_request=http_request,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
successful_updates.append(response)
|
||||
except HTTPException as exc:
|
||||
failed_updates.append(FailedTeamMemberUpdate(user_id=user_id, failed_reason=str(exc.detail)))
|
||||
|
||||
return BulkTeamMemberUpdateResponse(
|
||||
team_id=data.team_id,
|
||||
total_requested=len(user_ids),
|
||||
successful_updates=successful_updates,
|
||||
failed_updates=failed_updates,
|
||||
)
|
||||
|
||||
|
||||
def _create_results_from_response(
|
||||
members: List[Member],
|
||||
response: TeamAddMemberResponse,
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ def test_proxy_admin_viewer_config_update_route_rejected():
|
|||
"/team/unblock",
|
||||
"/team/permissions_update",
|
||||
"/team/permissions_bulk_update",
|
||||
"/team/member/bulk_update",
|
||||
# JWT key mapping write routes
|
||||
"/jwt/key/mapping/new",
|
||||
"/jwt/key/mapping/update",
|
||||
|
|
|
|||
|
|
@ -8,13 +8,18 @@ from starlette.requests import Request
|
|||
import litellm.proxy.proxy_server as proxy_server
|
||||
import litellm.proxy.management_endpoints.team_endpoints as team_endpoints
|
||||
from litellm.proxy._types import (
|
||||
BulkTeamMemberUpdateRequest,
|
||||
LiteLLM_TeamTable,
|
||||
LitellmUserRoles,
|
||||
Member,
|
||||
TeamMemberBulkUpdateFields,
|
||||
TeamMemberUpdateRequest,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.team_endpoints import team_member_update
|
||||
from litellm.proxy.management_endpoints.team_endpoints import (
|
||||
bulk_update_team_members,
|
||||
team_member_update,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -178,3 +183,51 @@ async def test_team_member_update_rejects_invalid_budget_duration(
|
|||
assert exc_info.value.status_code == 400
|
||||
assert "budget_duration" in str(exc_info.value.detail)
|
||||
upsert_mock.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bulk_team_member_update_applies_patch_and_returns_member_failures(monkeypatch):
|
||||
team_row = LiteLLM_TeamTable(
|
||||
team_id="team-1234",
|
||||
members_with_roles=[Member(user_id="user-1", role="user")],
|
||||
)
|
||||
prisma_client = MagicMock()
|
||||
prisma_client.db.litellm_teamtable.find_unique = AsyncMock(return_value=team_row)
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma_client)
|
||||
update_mock = AsyncMock(
|
||||
side_effect=[
|
||||
team_endpoints.TeamMemberUpdateResponse(team_id="team-1234", user_id="user-1", tpm_limit=42),
|
||||
HTTPException(status_code=404, detail={"error": "User is not a team member"}),
|
||||
]
|
||||
)
|
||||
monkeypatch.setattr(team_endpoints, "team_member_update", update_mock)
|
||||
|
||||
response = await bulk_update_team_members(
|
||||
data=BulkTeamMemberUpdateRequest(
|
||||
team_id="team-1234",
|
||||
user_ids=["user-1", "user-2", "user-1"],
|
||||
update_fields=TeamMemberBulkUpdateFields(tpm_limit=42),
|
||||
),
|
||||
http_request=Request({"type": "http", "method": "POST", "path": "/team/member/bulk_update"}),
|
||||
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN.value, user_id="admin"),
|
||||
)
|
||||
|
||||
assert response.total_requested == 2
|
||||
assert [member.user_id for member in response.successful_updates] == ["user-1"]
|
||||
assert response.failed_updates[0].user_id == "user-2"
|
||||
assert "not a team member" in response.failed_updates[0].failed_reason
|
||||
assert update_mock.await_args_list[0].kwargs["data"].model_dump(exclude_unset=True) == {
|
||||
"team_id": "team-1234",
|
||||
"user_id": "user-1",
|
||||
"tpm_limit": 42,
|
||||
}
|
||||
|
||||
|
||||
def test_bulk_team_member_update_requires_exactly_one_member_selector():
|
||||
with pytest.raises(ValueError, match="either user_ids or all_members_in_team"):
|
||||
BulkTeamMemberUpdateRequest(
|
||||
team_id="team-1234",
|
||||
user_ids=["user-1"],
|
||||
all_members_in_team=True,
|
||||
update_fields=TeamMemberBulkUpdateFields(tpm_limit=42),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2732,6 +2732,32 @@ export interface Member {
|
|||
allowed_models?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TeamMemberBulkUpdateFields {
|
||||
role?: "admin" | "user" | null;
|
||||
max_budget_in_team?: number | null;
|
||||
tpm_limit?: number | null;
|
||||
rpm_limit?: number | null;
|
||||
budget_duration?: string | null;
|
||||
allowed_models?: string[] | null;
|
||||
}
|
||||
|
||||
export const teamMemberBulkUpdateCall = async (
|
||||
accessToken: string,
|
||||
teamId: string,
|
||||
userIds: string[],
|
||||
allMembersInTeam: boolean,
|
||||
updateFields: TeamMemberBulkUpdateFields,
|
||||
) => {
|
||||
return apiClient.post(`/team/member/bulk_update`, {
|
||||
accessToken,
|
||||
body: {
|
||||
team_id: teamId,
|
||||
...(allMembersInTeam ? { all_members_in_team: true } : { user_ids: userIds }),
|
||||
update_fields: updateFields,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const teamMemberAddCall = async (accessToken: string, teamId: string, formValues: Member) => {
|
||||
try {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/team/member_add` : `/team/member_add`;
|
||||
|
|
|
|||
|
|
@ -889,6 +889,12 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
setSelectedEditMember={setSelectedEditMember}
|
||||
setIsEditMemberModalVisible={setIsEditMemberModalVisible}
|
||||
setIsAddMemberModalVisible={setIsAddMemberModalVisible}
|
||||
onMembersUpdated={async () => {
|
||||
if (!accessToken) return;
|
||||
const updatedTeamData = await teamInfoCall(accessToken, teamId);
|
||||
setTeamData(updatedTeamData);
|
||||
onUpdate(updatedTeamData);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Member } from "@/components/networking";
|
||||
import { Member, TeamMemberBulkUpdateFields, teamMemberBulkUpdateCall } from "@/components/networking";
|
||||
import BudgetDurationDropdown from "@/components/common_components/budget_duration_dropdown";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import NumericalInput from "@/components/shared/numerical_input";
|
||||
import { DateCell, MoneyCell } from "@/components/shared/table_cells";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Space, Tooltip, Typography } from "antd";
|
||||
import { Button, Checkbox, Form, Modal, Select, Space, Tooltip, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { useState } from "react";
|
||||
import MemberTable from "@/components/common_components/MemberTable";
|
||||
import { TeamData } from "./TeamInfo";
|
||||
|
||||
|
|
@ -17,6 +21,7 @@ interface TeamMemberTabProps {
|
|||
setSelectedEditMember: (member: Member) => void;
|
||||
setIsEditMemberModalVisible: (visible: boolean) => void;
|
||||
setIsAddMemberModalVisible: (visible: boolean) => void;
|
||||
onMembersUpdated?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function TeamMemberTab({
|
||||
|
|
@ -26,7 +31,11 @@ export default function TeamMemberTab({
|
|||
setSelectedEditMember,
|
||||
setIsEditMemberModalVisible,
|
||||
setIsAddMemberModalVisible,
|
||||
onMembersUpdated,
|
||||
}: TeamMemberTabProps) {
|
||||
const [isBulkUpdateVisible, setIsBulkUpdateVisible] = useState(false);
|
||||
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
|
||||
const [bulkUpdateForm] = Form.useForm();
|
||||
const formatNumber = (value: number | null): string => {
|
||||
if (value === null || value === undefined) return "0";
|
||||
|
||||
|
|
@ -79,7 +88,7 @@ export default function TeamMemberTab({
|
|||
};
|
||||
|
||||
const { data: uiSettingsData } = useUISettings();
|
||||
const { userId, userRole } = useAuthorized();
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
const disableTeamAdminDeleteTeamUser = Boolean(uiSettingsData?.values?.disable_team_admin_delete_team_user);
|
||||
const isUserTeamAdmin = isUserTeamAdminForSingleTeam(teamData.team_info.members_with_roles, userId || "");
|
||||
const isProxyAdmin = isProxyAdminRole(userRole || "");
|
||||
|
|
@ -183,31 +192,217 @@ export default function TeamMemberTab({
|
|||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<MemberTable
|
||||
members={teamData.team_info.members_with_roles}
|
||||
canEdit={canEditTeam}
|
||||
onEdit={(record) => {
|
||||
const membership = teamData.team_memberships.find((tm) => tm.user_id === record.user_id);
|
||||
const enhancedMember = {
|
||||
...record,
|
||||
max_budget_in_team: membership?.litellm_budget_table?.max_budget || null,
|
||||
tpm_limit: membership?.litellm_budget_table?.tpm_limit || null,
|
||||
rpm_limit: membership?.litellm_budget_table?.rpm_limit || null,
|
||||
budget_duration: membership?.litellm_budget_table?.budget_duration || null,
|
||||
allowed_models: membership?.litellm_budget_table?.allowed_models || [],
|
||||
};
|
||||
setSelectedEditMember(enhancedMember);
|
||||
setIsEditMemberModalVisible(true);
|
||||
}}
|
||||
onDelete={handleMemberDelete}
|
||||
onAddMember={() => setIsAddMemberModalVisible(true)}
|
||||
roleColumnTitle="Team Role"
|
||||
roleTooltip="This role applies only to this team and is independent from the user's proxy-level role."
|
||||
extraColumns={extraColumns}
|
||||
showDeleteForMember={() =>
|
||||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
const handleBulkUpdate = async (values: {
|
||||
all_members_in_team?: boolean;
|
||||
user_ids?: string[];
|
||||
apply_role?: boolean;
|
||||
role?: "admin" | "user";
|
||||
apply_max_budget?: boolean;
|
||||
max_budget_in_team?: number | null;
|
||||
apply_budget_duration?: boolean;
|
||||
budget_duration?: string | null;
|
||||
apply_tpm_limit?: boolean;
|
||||
tpm_limit?: number | null;
|
||||
apply_rpm_limit?: boolean;
|
||||
rpm_limit?: number | null;
|
||||
apply_allowed_models?: boolean;
|
||||
allowed_models?: string[];
|
||||
}) => {
|
||||
if (!accessToken) return;
|
||||
const updateFields: TeamMemberBulkUpdateFields = {
|
||||
...(values.apply_role ? { role: values.role } : {}),
|
||||
...(values.apply_max_budget ? { max_budget_in_team: values.max_budget_in_team ?? null } : {}),
|
||||
...(values.apply_budget_duration ? { budget_duration: values.budget_duration ?? null } : {}),
|
||||
...(values.apply_tpm_limit ? { tpm_limit: values.tpm_limit ?? null } : {}),
|
||||
...(values.apply_rpm_limit ? { rpm_limit: values.rpm_limit ?? null } : {}),
|
||||
...(values.apply_allowed_models ? { allowed_models: values.allowed_models ?? [] } : {}),
|
||||
};
|
||||
if (Object.keys(updateFields).length === 0) {
|
||||
NotificationsManager.fromBackend("Select at least one field to update");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsBulkUpdating(true);
|
||||
try {
|
||||
const response = await teamMemberBulkUpdateCall(
|
||||
accessToken,
|
||||
teamData.team_id,
|
||||
values.user_ids ?? [],
|
||||
values.all_members_in_team === true,
|
||||
updateFields,
|
||||
);
|
||||
await onMembersUpdated?.();
|
||||
setIsBulkUpdateVisible(false);
|
||||
bulkUpdateForm.resetFields();
|
||||
NotificationsManager.success(
|
||||
`${response.successful_updates.length} team member${response.successful_updates.length === 1 ? "" : "s"} updated`,
|
||||
);
|
||||
if (response.failed_updates.length > 0) {
|
||||
NotificationsManager.fromBackend(`${response.failed_updates.length} team member updates failed`);
|
||||
}
|
||||
/>
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend(error instanceof Error ? error.message : "Failed to bulk update team members");
|
||||
} finally {
|
||||
setIsBulkUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{canEditTeam && (
|
||||
<Button className="mb-3" onClick={() => setIsBulkUpdateVisible(true)}>
|
||||
Bulk Update Members
|
||||
</Button>
|
||||
)}
|
||||
<MemberTable
|
||||
members={teamData.team_info.members_with_roles}
|
||||
canEdit={canEditTeam}
|
||||
onEdit={(record) => {
|
||||
const membership = teamData.team_memberships.find((tm) => tm.user_id === record.user_id);
|
||||
const enhancedMember = {
|
||||
...record,
|
||||
max_budget_in_team: membership?.litellm_budget_table?.max_budget || null,
|
||||
tpm_limit: membership?.litellm_budget_table?.tpm_limit || null,
|
||||
rpm_limit: membership?.litellm_budget_table?.rpm_limit || null,
|
||||
budget_duration: membership?.litellm_budget_table?.budget_duration || null,
|
||||
allowed_models: membership?.litellm_budget_table?.allowed_models || [],
|
||||
};
|
||||
setSelectedEditMember(enhancedMember);
|
||||
setIsEditMemberModalVisible(true);
|
||||
}}
|
||||
onDelete={handleMemberDelete}
|
||||
onAddMember={() => setIsAddMemberModalVisible(true)}
|
||||
roleColumnTitle="Team Role"
|
||||
roleTooltip="This role applies only to this team and is independent from the user's proxy-level role."
|
||||
extraColumns={extraColumns}
|
||||
showDeleteForMember={() =>
|
||||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title="Bulk Update Team Members"
|
||||
open={isBulkUpdateVisible}
|
||||
onCancel={() => setIsBulkUpdateVisible(false)}
|
||||
onOk={() => bulkUpdateForm.submit()}
|
||||
okText="Update Members"
|
||||
confirmLoading={isBulkUpdating}
|
||||
>
|
||||
<Form form={bulkUpdateForm} layout="vertical" onFinish={handleBulkUpdate}>
|
||||
<Form.Item name="all_members_in_team" valuePropName="checked">
|
||||
<Checkbox>Update all {teamData.team_info.members_with_roles.length} members</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
!getFieldValue("all_members_in_team") && (
|
||||
<Form.Item
|
||||
name="user_ids"
|
||||
label="Members"
|
||||
rules={[{ required: true, message: "Select at least one member" }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select members"
|
||||
options={teamData.team_info.members_with_roles
|
||||
.filter((member) => member.user_id)
|
||||
.map((member) => ({
|
||||
label: member.user_email || member.user_id,
|
||||
value: member.user_id as string,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Typography.Text type="secondary">Choose the fields to apply to every selected member.</Typography.Text>
|
||||
<Form.Item name="apply_role" valuePropName="checked" className="mt-3 mb-1">
|
||||
<Checkbox>Team role</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_role") && (
|
||||
<Form.Item name="role" rules={[{ required: true, message: "Select a role" }]}>
|
||||
<Select
|
||||
placeholder="Select role"
|
||||
options={[
|
||||
{ label: "Admin", value: "admin" },
|
||||
{ label: "User", value: "user" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="apply_max_budget" valuePropName="checked" className="mb-1">
|
||||
<Checkbox>Team member budget (USD)</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_max_budget") && (
|
||||
<Form.Item name="max_budget_in_team">
|
||||
<NumericalInput
|
||||
min={0}
|
||||
step={0.01}
|
||||
placeholder="Leave blank to clear the budget"
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="apply_budget_duration" valuePropName="checked" className="mb-1">
|
||||
<Checkbox>Budget reset period</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_budget_duration") && (
|
||||
<Form.Item name="budget_duration">
|
||||
<BudgetDurationDropdown />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="apply_tpm_limit" valuePropName="checked" className="mb-1">
|
||||
<Checkbox>TPM limit</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_tpm_limit") && (
|
||||
<Form.Item name="tpm_limit">
|
||||
<NumericalInput min={0} placeholder="Leave blank to clear the TPM limit" style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="apply_rpm_limit" valuePropName="checked" className="mb-1">
|
||||
<Checkbox>RPM limit</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_rpm_limit") && (
|
||||
<Form.Item name="rpm_limit">
|
||||
<NumericalInput min={0} placeholder="Leave blank to clear the RPM limit" style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
<Form.Item name="apply_allowed_models" valuePropName="checked" className="mb-1">
|
||||
<Checkbox>Allowed models</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue("apply_allowed_models") && (
|
||||
<Form.Item name="allowed_models">
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Leave empty to inherit team models"
|
||||
options={(teamData.team_info.models || []).map((model) => ({ label: model, value: model }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
5802
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
5802
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue