mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 03:34:10 +00:00
Litellm UI stable version 02 12 2025 (#8497)
* fix(key_management_endpoints.py): fix `/key/list` to include `return_full_object` as a top-level query param Allows user to specify they want the keys as a list of objects * refactor(key_list.tsx): initial refactor of key table in user dashboard offloads key filtering logic to backend api prevents common error of user not being able to see their keys * fix(key_management_endpoints.py): allow internal user to query `/key/list` to see their keys * fix(key_management_endpoints.py): add validation checks and filtering to `/key/list` endpoint allow internal user to see their keys. not anybody else's * fix(view_key_table.tsx): fix issue where internal user could not see default team keys * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error * fix: fix linting error
This commit is contained in:
parent
40e3af0428
commit
1195fe2a44
9 changed files with 351 additions and 108 deletions
|
@ -69,8 +69,10 @@ import {
|
|||
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
|
||||
import useKeyList from "./key_team_helpers/key_list";
|
||||
import { KeyResponse } from "./key_team_helpers/key_list";
|
||||
const { Option } = Select;
|
||||
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
if (isLocal != true) {
|
||||
|
@ -87,7 +89,7 @@ interface EditKeyModalProps {
|
|||
interface ModelLimitModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
token: ItemData;
|
||||
token: KeyResponse;
|
||||
onSubmit: (updatedMetadata: any) => void;
|
||||
accessToken: string;
|
||||
}
|
||||
|
@ -125,6 +127,19 @@ interface ItemData {
|
|||
// Add any other properties that exist in the item data
|
||||
}
|
||||
|
||||
interface ModelLimits {
|
||||
[key: string]: number; // Index signature allowing string keys
|
||||
}
|
||||
|
||||
interface CombinedLimit {
|
||||
tpm: number;
|
||||
rpm: number;
|
||||
}
|
||||
|
||||
interface CombinedLimits {
|
||||
[key: string]: CombinedLimit; // Index signature allowing string keys
|
||||
}
|
||||
|
||||
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||
userID,
|
||||
userRole,
|
||||
|
@ -139,15 +154,22 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [keyToDelete, setKeyToDelete] = useState<string | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<ItemData | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<KeyResponse | null>(null);
|
||||
const [spendData, setSpendData] = useState<
|
||||
{ day: string; spend: number }[] | null
|
||||
>(null);
|
||||
const [predictedSpendString, setPredictedSpendString] = useState("");
|
||||
|
||||
const { keys, isLoading, error, refresh } = useKeyList({
|
||||
selectedTeam,
|
||||
currentOrg,
|
||||
accessToken
|
||||
});
|
||||
|
||||
console.log("keys", keys);
|
||||
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [infoDialogVisible, setInfoDialogVisible] = useState(false);
|
||||
const [selectedToken, setSelectedToken] = useState<ItemData | null>(null);
|
||||
const [selectedToken, setSelectedToken] = useState<KeyResponse | null>(null);
|
||||
const [userModels, setUserModels] = useState<string[]>([]);
|
||||
const initialKnownTeamIDs: Set<string> = new Set();
|
||||
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
|
||||
|
@ -160,75 +182,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||
|
||||
// Function to check if user is admin of a team
|
||||
const isUserTeamAdmin = (team: any) => {
|
||||
if (!team.members_with_roles) return false;
|
||||
return team.members_with_roles.some(
|
||||
(member: any) => member.role === "admin" && member.user_id === userID
|
||||
);
|
||||
};
|
||||
|
||||
// Combine all keys that user should have access to
|
||||
const all_keys_to_display = React.useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
// Helper function for default team org check
|
||||
const matchesDefaultTeamOrg = (key: any) => {
|
||||
console.log(`Checking if key matches default team org: ${JSON.stringify(key)}, currentOrg: ${JSON.stringify(currentOrg)}`)
|
||||
if (!currentOrg || currentOrg.organization_id === null) {
|
||||
return !('organization_id' in key) || key.organization_id === null;
|
||||
}
|
||||
return key.organization_id === currentOrg.organization_id;
|
||||
};
|
||||
|
||||
let allKeys: any[] = [];
|
||||
|
||||
// Handle no team selected or Default Team case
|
||||
if (!selectedTeam || selectedTeam.team_alias === "Default Team") {
|
||||
|
||||
console.log(`inside personal keys`)
|
||||
// Get personal keys (with org check)
|
||||
const personalKeys = data.filter(key =>
|
||||
key.team_id == null &&
|
||||
matchesDefaultTeamOrg(key)
|
||||
);
|
||||
|
||||
console.log(`personalKeys: ${JSON.stringify(personalKeys)}`)
|
||||
|
||||
// Get admin team keys (no org check)
|
||||
const adminTeamKeys = data.filter(key => {
|
||||
const keyTeam = teams?.find(team => team.team_id === key.team_id);
|
||||
return keyTeam && isUserTeamAdmin(keyTeam) && key.team_id !== "default-team";
|
||||
});
|
||||
|
||||
console.log(`adminTeamKeys: ${JSON.stringify(adminTeamKeys)}`)
|
||||
|
||||
allKeys = [...personalKeys, ...adminTeamKeys];
|
||||
}
|
||||
// Handle specific team selected
|
||||
else {
|
||||
const selectedTeamData = teams?.find(t => t.team_id === selectedTeam.team_id);
|
||||
if (selectedTeamData) {
|
||||
const teamKeys = data.filter(key => {
|
||||
if (selectedTeamData.team_id === "default-team") {
|
||||
return key.team_id == null && matchesDefaultTeamOrg(key);
|
||||
}
|
||||
return key.team_id === selectedTeamData.team_id;
|
||||
});
|
||||
allKeys = teamKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Final filtering and deduplication
|
||||
return Array.from(
|
||||
new Map(
|
||||
allKeys
|
||||
.filter(key => key.team_id !== "litellm-dashboard")
|
||||
.map(key => [key.token, key])
|
||||
).values()
|
||||
);
|
||||
}, [data, teams, selectedTeam, currentOrg]);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||
if (!duration) {
|
||||
|
@ -298,7 +251,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
fetchUserModels();
|
||||
}, [accessToken, userID, userRole]);
|
||||
|
||||
const handleModelLimitClick = (token: ItemData) => {
|
||||
const handleModelLimitClick = (token: KeyResponse) => {
|
||||
setSelectedToken(token);
|
||||
setModelLimitModalVisible(true);
|
||||
};
|
||||
|
@ -668,13 +621,12 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
if (token.metadata) {
|
||||
const tpmLimits = token.metadata.model_tpm_limit || {};
|
||||
const rpmLimits = token.metadata.model_rpm_limit || {};
|
||||
const combinedLimits: { [key: string]: { tpm: number; rpm: number } } =
|
||||
{};
|
||||
const combinedLimits: CombinedLimits = {};
|
||||
|
||||
Object.keys({ ...tpmLimits, ...rpmLimits }).forEach((model) => {
|
||||
combinedLimits[model] = {
|
||||
tpm: tpmLimits[model] || 0,
|
||||
rpm: rpmLimits[model] || 0,
|
||||
tpm: (tpmLimits as ModelLimits)[model] || 0,
|
||||
rpm: (rpmLimits as ModelLimits)[model] || 0,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -1061,10 +1013,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
console.log("RERENDER TRIGGERED");
|
||||
return (
|
||||
<div>
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
|
||||
|
@ -1084,8 +1032,8 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{all_keys_to_display &&
|
||||
all_keys_to_display.map((item) => {
|
||||
{keys &&
|
||||
keys.map((item) => {
|
||||
console.log(item);
|
||||
// skip item if item.team_id == "litellm-dashboard"
|
||||
if (item.team_id === "litellm-dashboard") {
|
||||
|
@ -1095,9 +1043,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
/**
|
||||
* if selected team id is null -> show the keys with no team id or team id's that don't exist in db
|
||||
*/
|
||||
console.log(
|
||||
`item team id: ${item.team_id}, knownTeamIDs.has(item.team_id): ${knownTeamIDs.has(item.team_id)}, selectedTeam id: ${selectedTeam.team_id}`
|
||||
);
|
||||
|
||||
if (
|
||||
selectedTeam.team_id == null &&
|
||||
item.team_id !== null &&
|
||||
|
@ -1147,7 +1093,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<Text>
|
||||
{(() => {
|
||||
try {
|
||||
return parseFloat(item.spend).toFixed(4);
|
||||
return item.spend.toFixed(4);
|
||||
} catch (error) {
|
||||
return item.spend;
|
||||
}
|
||||
|
@ -1324,9 +1270,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
{(() => {
|
||||
try {
|
||||
return parseFloat(
|
||||
selectedToken.spend
|
||||
).toFixed(4);
|
||||
return selectedToken.spend.toFixed(4);
|
||||
} catch (error) {
|
||||
return selectedToken.spend;
|
||||
}
|
||||
|
@ -1334,7 +1278,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card key={item.name}>
|
||||
<Card key={item.key_name}>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Budget
|
||||
</p>
|
||||
|
@ -1359,7 +1303,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card key={item.name}>
|
||||
<Card key={item.key_name}>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Expires
|
||||
</p>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue