From 759ff3f750729f840b934bb3af09dd9844ce99f5 Mon Sep 17 00:00:00 2001 From: Nick Wong Date: Fri, 10 May 2024 15:42:07 -0700 Subject: [PATCH] added code to enforce unique key and team aliases in the ui --- .../src/components/create_key_button.tsx | 395 +++++++++------- ui/litellm-dashboard/src/components/teams.tsx | 439 +++++++++++------- 2 files changed, 495 insertions(+), 339 deletions(-) diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index 648eee9ca..0e158778f 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -2,8 +2,17 @@ import React, { useState, useEffect, useRef } from "react"; import { Button, TextInput, Grid, Col } from "@tremor/react"; -import { Card, Metric, Text, Title, Subtitle, Accordion, AccordionHeader, AccordionBody, } from "@tremor/react"; -import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { + Card, + Metric, + Text, + Title, + Subtitle, + Accordion, + AccordionHeader, + AccordionBody, +} from "@tremor/react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; import { Button as Button2, Modal, @@ -13,7 +22,11 @@ import { Select, message, } from "antd"; -import { keyCreateCall, slackBudgetAlertsHealthCheck, modelAvailableCall } from "./networking"; +import { + keyCreateCall, + slackBudgetAlertsHealthCheck, + modelAvailableCall, +} from "./networking"; const { Option } = Select; @@ -59,7 +72,11 @@ const CreateKey: React.FC = ({ } if (accessToken !== null) { - const model_available = await modelAvailableCall(accessToken, userID, userRole); + const model_available = await modelAvailableCall( + accessToken, + userID, + userRole + ); let available_model_names = model_available["data"].map( (element: { id: string }) => element.id ); @@ -70,12 +87,25 @@ const CreateKey: React.FC = ({ console.error("Error fetching user models:", error); } }; - + fetchUserModels(); }, [accessToken, userID, userRole]); const handleCreate = async (formValues: Record) => { try { + const newKeyAlias = formValues?.key_alias ?? ""; + const newKeyTeamId = formValues?.team_id ?? null; + const existingKeyAliases = + data + ?.filter((k) => k.team_id === newKeyTeamId) + .map((k) => k.key_alias) ?? []; + + if (existingKeyAliases.includes(newKeyAlias)) { + throw new Error( + `Key alias ${newKeyAlias} already exists for team with ID ${newKeyTeamId}, please provide another key alias` + ); + } + message.info("Making API Call"); setIsModalVisible(true); const response = await keyCreateCall(accessToken, userID, formValues); @@ -89,12 +119,13 @@ const CreateKey: React.FC = ({ localStorage.removeItem("userData" + userID); } catch (error) { console.error("Error creating the key:", error); + message.error(`Error creating the key: ${error}`, 20); } }; const handleCopy = () => { - message.success('API Key copied to clipboard'); -}; + message.success("API Key copied to clipboard"); + }; useEffect(() => { let tempModelsToPick = []; @@ -119,7 +150,6 @@ const CreateKey: React.FC = ({ setModelsToPick(tempModelsToPick); }, [team, userModels]); - return (
@@ -141,140 +171,164 @@ const CreateKey: React.FC = ({ wrapperCol={{ span: 16 }} labelAlign="left" > - <> - - - - - - - - - - - Optional Settings - - - { - if (value && team && team.max_budget !== null && value > team.max_budget) { - throw new Error(`Budget cannot exceed team max budget: $${team.max_budget}`); - } - }, - }, - ]} - > - - - + + + + - { - if (value && team && team.tpm_limit !== null && value > team.tpm_limit) { - throw new Error(`TPM limit cannot exceed team TPM limit: ${team.tpm_limit}`); - } - }, - }, - ]} - > - - - { - if (value && team && team.rpm_limit !== null && value > team.rpm_limit) { - throw new Error(`RPM limit cannot exceed team RPM limit: ${team.rpm_limit}`); - } - }, - }, - ]} - > - - - - - - - - + > + + + + + + + + + Optional Settings + + + { + if ( + value && + team && + team.max_budget !== null && + value > team.max_budget + ) { + throw new Error( + `Budget cannot exceed team max budget: $${team.max_budget}` + ); + } + }, + }, + ]} + > + + + + + + { + if ( + value && + team && + team.tpm_limit !== null && + value > team.tpm_limit + ) { + throw new Error( + `TPM limit cannot exceed team TPM limit: ${team.tpm_limit}` + ); + } + }, + }, + ]} + > + + + { + if ( + value && + team && + team.rpm_limit !== null && + value > team.rpm_limit + ) { + throw new Error( + `RPM limit cannot exceed team RPM limit: ${team.rpm_limit}` + ); + } + }, + }, + ]} + > + + + + + + + + + + + - - - -
Create Key
@@ -288,36 +342,45 @@ const CreateKey: React.FC = ({ footer={null} > - - Save your Key - -

- Please save this secret key somewhere safe and accessible. For - security reasons, you will not be able to view it again{" "} - through your LiteLLM account. If you lose this secret key, you - will need to generate a new one. -

- - - {apiKey != null ? ( -
+ Save your Key + +

+ Please save this secret key somewhere safe and accessible. For + security reasons, you will not be able to view it again{" "} + through your LiteLLM account. If you lose this secret key, you + will need to generate a new one. +

+ + + {apiKey != null ? ( +
API Key: -
-
{apiKey}
-
- - +
+
+                      {apiKey}
+                    
+
+ + - - {/* */} -
- ) : ( - Key being created, this might take 30s - )} - - +
+ ) : ( + Key being created, this might take 30s + )} +
)} diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index b24801799..d22789420 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -2,7 +2,13 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; import { Typography } from "antd"; import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking"; -import { InformationCircleIcon, PencilAltIcon, PencilIcon, StatusOnlineIcon, TrashIcon } from "@heroicons/react/outline"; +import { + InformationCircleIcon, + PencilAltIcon, + PencilIcon, + StatusOnlineIcon, + TrashIcon, +} from "@heroicons/react/outline"; import { Button as Button2, Modal, @@ -46,8 +52,12 @@ interface EditTeamModalProps { onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted } - -import { teamCreateCall, teamMemberAddCall, Member, modelAvailableCall } from "./networking"; +import { + teamCreateCall, + teamMemberAddCall, + Member, + modelAvailableCall, +} from "./networking"; const Team: React.FC = ({ teams, @@ -63,7 +73,6 @@ const Team: React.FC = ({ const [value, setValue] = useState(""); const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedTeam, setSelectedTeam] = useState( teams ? teams[0] : null ); @@ -76,127 +85,125 @@ const Team: React.FC = ({ // store team info as {"team_id": team_info_object} const [perTeamInfo, setPerTeamInfo] = useState>({}); + const EditTeamModal: React.FC = ({ + visible, + onCancel, + team, + onSubmit, + }) => { + const [form] = Form.useForm(); - const EditTeamModal: React.FC = ({ visible, onCancel, team, onSubmit }) => { - const [form] = Form.useForm(); + const handleOk = () => { + form + .validateFields() + .then((values) => { + const updatedValues = { ...values, team_id: team.team_id }; + onSubmit(updatedValues); + form.resetFields(); + }) + .catch((error) => { + console.error("Validation failed:", error); + }); + }; - const handleOk = () => { - form - .validateFields() - .then((values) => { - const updatedValues = {...values, team_id: team.team_id}; - onSubmit(updatedValues); - form.resetFields(); - }) - .catch((error) => { - console.error("Validation failed:", error); - }); -}; - - return ( + return ( -
- <> - - - - - - - {"All Proxy Models"} - - {userModels && userModels.map((model) => ( - - {model} - - ))} - - - - - - - - - - - - - - -
- Edit Team -
-
-
- ); -}; - -const handleEditClick = (team: any) => { - setSelectedTeam(team); - setEditModalVisible(true); -}; - -const handleEditCancel = () => { - setEditModalVisible(false); - setSelectedTeam(null); -}; - -const handleEditSubmit = async (formValues: Record) => { - // Call API to update team with teamId and values - const teamId = formValues.team_id; // get team_id - - console.log("handleEditSubmit:", formValues); - if (accessToken == null) { - return; - } - - let newTeamValues = await teamUpdateCall(accessToken, formValues); - - // Update the teams state with the updated team data - if (teams) { - const updatedTeams = teams.map((team) => - team.team_id === teamId ? newTeamValues.data : team +
+ <> + + + + + + + {"All Proxy Models"} + + {userModels && + userModels.map((model) => ( + + {model} + + ))} + + + + + + + + + + + + + +
+ Edit Team +
+
+ ); - setTeams(updatedTeams); - } - message.success("Team updated successfully"); + }; - setEditModalVisible(false); - setSelectedTeam(null); -}; + const handleEditClick = (team: any) => { + setSelectedTeam(team); + setEditModalVisible(true); + }; + + const handleEditCancel = () => { + setEditModalVisible(false); + setSelectedTeam(null); + }; + + const handleEditSubmit = async (formValues: Record) => { + // Call API to update team with teamId and values + const teamId = formValues.team_id; // get team_id + + console.log("handleEditSubmit:", formValues); + if (accessToken == null) { + return; + } + + let newTeamValues = await teamUpdateCall(accessToken, formValues); + + // Update the teams state with the updated team data + if (teams) { + const updatedTeams = teams.map((team) => + team.team_id === teamId ? newTeamValues.data : team + ); + setTeams(updatedTeams); + } + message.success("Team updated successfully"); + + setEditModalVisible(false); + setSelectedTeam(null); + }; const handleOk = () => { setIsTeamModalVisible(false); @@ -224,9 +231,6 @@ const handleEditSubmit = async (formValues: Record) => { setIsDeleteModalOpen(true); }; - - - const confirmDelete = async () => { if (teamToDelete == null || teams == null || accessToken == null) { return; @@ -235,7 +239,9 @@ const handleEditSubmit = async (formValues: Record) => { try { await teamDeleteCall(accessToken, teamToDelete); // Successfully completed the deletion. Update the state to trigger a rerender. - const filteredData = teams.filter((item) => item.team_id !== teamToDelete); + const filteredData = teams.filter( + (item) => item.team_id !== teamToDelete + ); setTeams(filteredData); } catch (error) { console.error("Error deleting the team:", error); @@ -253,8 +259,6 @@ const handleEditSubmit = async (formValues: Record) => { setTeamToDelete(null); }; - - useEffect(() => { const fetchUserModels = async () => { try { @@ -263,7 +267,11 @@ const handleEditSubmit = async (formValues: Record) => { } if (accessToken !== null) { - const model_available = await modelAvailableCall(accessToken, userID, userRole); + const model_available = await modelAvailableCall( + accessToken, + userID, + userRole + ); let available_model_names = model_available["data"].map( (element: { id: string }) => element.id ); @@ -275,7 +283,6 @@ const handleEditSubmit = async (formValues: Record) => { } }; - const fetchTeamInfo = async () => { try { if (userID === null || userRole === null || accessToken === null) { @@ -288,22 +295,21 @@ const handleEditSubmit = async (formValues: Record) => { console.log("fetching team info:"); - let _team_id_to_info: Record = {}; for (let i = 0; i < teams?.length; i++) { let _team_id = teams[i].team_id; const teamInfo = await teamInfoCall(accessToken, _team_id); console.log("teamInfo response:", teamInfo); if (teamInfo !== null) { - _team_id_to_info = {..._team_id_to_info, [_team_id]: teamInfo}; + _team_id_to_info = { ..._team_id_to_info, [_team_id]: teamInfo }; } } setPerTeamInfo(_team_id_to_info); - } catch (error) { - console.error("Error fetching team info:", error); - } - }; - + } catch (error) { + console.error("Error fetching team info:", error); + } + }; + fetchUserModels(); fetchTeamInfo(); }, [accessToken, userID, userRole, teams]); @@ -311,6 +317,15 @@ const handleEditSubmit = async (formValues: Record) => { const handleCreate = async (formValues: Record) => { try { if (accessToken != null) { + const newTeamAlias = formValues?.team_alias; + const existingTeamAliases = teams?.map((t) => t.team_alias) ?? []; + + if (existingTeamAliases.includes(newTeamAlias)) { + throw new Error( + `Team alias ${newTeamAlias} already exists, please pick another alias` + ); + } + message.info("Creating Team"); const response: any = await teamCreateCall(accessToken, formValues); if (teams !== null) { @@ -364,7 +379,7 @@ const handleEditSubmit = async (formValues: Record) => { console.error("Error creating the team:", error); } }; - console.log(`received teams ${teams}`); + console.log(`received teams ${JSON.stringify(teams)}`); return (
@@ -387,55 +402,124 @@ const handleEditSubmit = async (formValues: Record) => { {teams && teams.length > 0 ? teams.map((team: any) => ( - {team["team_alias"]} - {team["spend"]} - + + {team["team_alias"]} + + + {team["spend"]} + + {team["max_budget"] ? team["max_budget"] : "No limit"} - + {Array.isArray(team.models) ? ( -
+
{team.models.length === 0 ? ( All Proxy Models ) : ( - team.models.map((model: string, index: number) => ( - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - {model.length > 30 ? `${model.slice(0, 30)}...` : model} - - ) - )) + team.models.map( + (model: string, index: number) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + + {model.length > 30 + ? `${model.slice(0, 30)}...` + : model} + + + ) + ) )}
) : null} - - + - TPM:{" "} - {team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "} + TPM: {team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "}

RPM:{" "} {team.rpm_limit ? team.rpm_limit : "Unlimited"}
- {perTeamInfo && team.team_id && perTeamInfo[team.team_id] && perTeamInfo[team.team_id].keys && perTeamInfo[team.team_id].keys.length} Keys - {perTeamInfo && team.team_id && perTeamInfo[team.team_id] && perTeamInfo[team.team_id].team_info && perTeamInfo[team.team_id].team_info.members_with_roles && perTeamInfo[team.team_id].team_info.members_with_roles.length} Members + + {perTeamInfo && + team.team_id && + perTeamInfo[team.team_id] && + perTeamInfo[team.team_id].keys && + perTeamInfo[team.team_id].keys.length}{" "} + Keys + + + {perTeamInfo && + team.team_id && + perTeamInfo[team.team_id] && + perTeamInfo[team.team_id].team_info && + perTeamInfo[team.team_id].team_info + .members_with_roles && + perTeamInfo[team.team_id].team_info + .members_with_roles.length}{" "} + Members + - handleEditClick(team)} /> - handleDelete(team.team_id)} icon={TrashIcon} size="sm" @@ -481,7 +565,11 @@ const handleEditSubmit = async (formValues: Record) => {
- @@ -515,10 +603,12 @@ const handleEditSubmit = async (formValues: Record) => { labelAlign="left" > <> - @@ -528,7 +618,10 @@ const handleEditSubmit = async (formValues: Record) => { placeholder="Select models" style={{ width: "100%" }} > - + All Proxy Models {userModels.map((model) => ( @@ -606,8 +699,8 @@ const handleEditSubmit = async (formValues: Record) => { {member["user_email"] ? member["user_email"] : member["user_id"] - ? member["user_id"] - : null} + ? member["user_id"] + : null} {member["role"]} @@ -618,13 +711,13 @@ const handleEditSubmit = async (formValues: Record) => { {selectedTeam && ( - - )} + + )}