"use client"; import React, { useEffect, useState } from "react"; import { keyDeleteCall, modelAvailableCall } from "./networking"; import { add } from 'date-fns'; import { InformationCircleIcon, StatusOnlineIcon, TrashIcon, PencilAltIcon, RefreshIcon } from "@heroicons/react/outline"; import { keySpendLogsCall, PredictedSpendLogsCall, keyUpdateCall, modelInfoCall, regenerateKeyCall } from "./networking"; import { Badge, Card, Table, Grid, Col, Button, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Dialog, DialogPanel, Text, Title, Subtitle, Icon, BarChart, TextInput, } from "@tremor/react"; import { Select as Select3, SelectItem, MultiSelect, MultiSelectItem } from "@tremor/react"; import { Button as Button2, Modal, Form, Input, Select as Select2, InputNumber, message, Select, Tooltip, DatePicker, } from "antd"; import { CopyToClipboard } from "react-copy-to-clipboard"; const { Option } = Select; const isLocal = process.env.NODE_ENV === "development"; const proxyBaseUrl = isLocal ? "http://localhost:4000" : null; if (isLocal != true) { console.log = function() {}; } interface EditKeyModalProps { visible: boolean; onCancel: () => void; token: any; // Assuming TeamType is a type representing your team object onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted } interface ModelLimitModalProps { visible: boolean; onCancel: () => void; token: ItemData; onSubmit: (updatedMetadata: any) => void; accessToken: string; } // Define the props type interface ViewKeyTableProps { userID: string; userRole: string | null; accessToken: string; selectedTeam: any | null; data: any[] | null; setData: React.Dispatch>; teams: any[] | null; premiumUser: boolean; } interface ItemData { key_alias: string | null; key_name: string; spend: string; max_budget: string | null; models: string[]; tpm_limit: string | null; rpm_limit: string | null; token: string; token_id: string | null; id: number; team_id: string; metadata: any; user_id: string | null; expires: any; budget_duration: string | null; budget_reset_at: string | null; // Add any other properties that exist in the item data } const ViewKeyTable: React.FC = ({ userID, userRole, accessToken, selectedTeam, data, setData, teams, premiumUser }) => { const [isButtonClicked, setIsButtonClicked] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [keyToDelete, setKeyToDelete] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [spendData, setSpendData] = useState<{ day: string; spend: number }[] | null>( null ); const [predictedSpendString, setPredictedSpendString] = useState(""); const [editModalVisible, setEditModalVisible] = useState(false); const [infoDialogVisible, setInfoDialogVisible] = useState(false); const [selectedToken, setSelectedToken] = useState(null); const [userModels, setUserModels] = useState([]); const initialKnownTeamIDs: Set = new Set(); const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false); const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false); const [regeneratedKey, setRegeneratedKey] = useState(null); const [regenerateFormData, setRegenerateFormData] = useState(null); const [regenerateForm] = Form.useForm(); const [newExpiryTime, setNewExpiryTime] = useState(null); const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs); useEffect(() => { const calculateNewExpiryTime = (duration: string | undefined) => { if (!duration) { return null; } try { const now = new Date(); let newExpiry: Date; if (duration.endsWith('s')) { newExpiry = add(now, { seconds: parseInt(duration) }); } else if (duration.endsWith('h')) { newExpiry = add(now, { hours: parseInt(duration) }); } else if (duration.endsWith('d')) { newExpiry = add(now, { days: parseInt(duration) }); } else { throw new Error('Invalid duration format'); } return newExpiry.toLocaleString('en-US', { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }); } catch (error) { return null; } }; console.log("in calculateNewExpiryTime for selectedToken", selectedToken); // When a new duration is entered if (regenerateFormData?.duration) { setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); } else { setNewExpiryTime(null); } console.log("calculateNewExpiryTime:", newExpiryTime); }, [selectedToken, regenerateFormData?.duration]); useEffect(() => { const fetchUserModels = async () => { try { if (userID === null) { return; } if (accessToken !== null && userRole !== null) { const model_available = await modelAvailableCall(accessToken, userID, userRole); let available_model_names = model_available["data"].map( (element: { id: string }) => element.id ); console.log("available_model_names:", available_model_names); setUserModels(available_model_names); } } catch (error) { console.error("Error fetching user models:", error); } }; fetchUserModels(); }, [accessToken, userID, userRole]); const handleModelLimitClick = (token: ItemData) => { setSelectedToken(token); setModelLimitModalVisible(true); }; const handleModelLimitSubmit = async (updatedMetadata: any) => { if (accessToken == null || selectedToken == null) { return; } const formValues = { ...selectedToken, metadata: updatedMetadata, key: selectedToken.token, }; try { let newKeyValues = await keyUpdateCall(accessToken, formValues); console.log("Model limits updated:", newKeyValues); // Update the keys with the updated key if (data) { const updatedData = data.map((key) => key.token === selectedToken.token ? newKeyValues : key ); setData(updatedData); } message.success("Model-specific limits updated successfully"); } catch (error) { console.error("Error updating model-specific limits:", error); message.error("Failed to update model-specific limits"); } setModelLimitModalVisible(false); setSelectedToken(null); }; useEffect(() => { if (teams) { const teamIDSet: Set = new Set(); teams.forEach((team: any, index: number) => { const team_obj: string = team.team_id teamIDSet.add(team_obj); }); setKnownTeamIDs(teamIDSet) } }, [teams]) const EditKeyModal: React.FC = ({ visible, onCancel, token, onSubmit }) => { const [form] = Form.useForm(); const [keyTeam, setKeyTeam] = useState(selectedTeam); const [errorModels, setErrorModels] = useState([]); const [errorBudget, setErrorBudget] = useState(false); 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 (
<> { const errorModels = value.filter((model: string) => ( !keyTeam.models.includes(model) && model !== "all-team-models" && model !== "all-proxy-models" && !keyTeam.models.includes("all-proxy-models") )); console.log(`errorModels: ${errorModels}`) if (errorModels.length > 0) { return Promise.reject(`Some models are not part of the new team\'s models - ${errorModels}Team models: ${keyTeam.models}`); } else { return Promise.resolve(); } } } ]}> { if (value && keyTeam && keyTeam.max_budget !== null && value > keyTeam.max_budget) { console.log(`keyTeam.max_budget: ${keyTeam.max_budget}`) throw new Error(`Budget cannot exceed team max budget: $${keyTeam.max_budget}`); } }, }, ]} > {teams?.map((team_obj, index) => ( setKeyTeam(team_obj)} > {team_obj.team_alias} ))} { if (value && keyTeam && keyTeam.tpm_limit !== null && value > keyTeam.tpm_limit) { console.log(`keyTeam.tpm_limit: ${keyTeam.tpm_limit}`) throw new Error(`tpm_limit cannot exceed team max tpm_limit: $${keyTeam.tpm_limit}`); } }, }, ]} > { if (value && keyTeam && keyTeam.rpm_limit !== null && value > keyTeam.rpm_limit) { console.log(`keyTeam.rpm_limit: ${keyTeam.rpm_limit}`) throw new Error(`rpm_limit cannot exceed team max rpm_limit: $${keyTeam.rpm_limit}`); } }, }, ]} >
Edit Key
); }; const ModelLimitModal: React.FC = ({ visible, onCancel, token, onSubmit, accessToken }) => { const [modelLimits, setModelLimits] = useState<{ [key: string]: { tpm: number, rpm: number } }>({}); const [availableModels, setAvailableModels] = useState([]); const [newModelRow, setNewModelRow] = useState(null); useEffect(() => { 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 } } = {}; Object.keys({ ...tpmLimits, ...rpmLimits }).forEach(model => { combinedLimits[model] = { tpm: tpmLimits[model] || 0, rpm: rpmLimits[model] || 0 }; }); setModelLimits(combinedLimits); } const fetchAvailableModels = async () => { try { const modelDataResponse = await modelInfoCall(accessToken, "", ""); const allModelGroups: string[] = Array.from(new Set(modelDataResponse.data.map((model: any) => model.model_name))); setAvailableModels(allModelGroups); } catch (error) { console.error("Error fetching model data:", error); message.error("Failed to fetch available models"); } }; fetchAvailableModels(); }, [token, accessToken]); const handleLimitChange = (model: string, type: 'tpm' | 'rpm', value: number | null) => { setModelLimits(prev => ({ ...prev, [model]: { ...prev[model], [type]: value || 0 } })); }; const handleAddLimit = () => { setNewModelRow(''); }; const handleModelSelect = (model: string) => { if (!modelLimits[model]) { setModelLimits(prev => ({ ...prev, [model]: { tpm: 0, rpm: 0 } })); } setNewModelRow(null); }; const handleRemoveModel = (model: string) => { setModelLimits(prev => { const { [model]: _, ...rest } = prev; return rest; }); }; const handleSubmit = () => { const updatedMetadata = { ...token.metadata, model_tpm_limit: Object.fromEntries(Object.entries(modelLimits).map(([model, limits]) => [model, limits.tpm])), model_rpm_limit: Object.fromEntries(Object.entries(modelLimits).map(([model, limits]) => [model, limits.rpm])), }; onSubmit(updatedMetadata); }; return (
Model TPM Limit RPM Limit Actions {Object.entries(modelLimits).map(([model, limits]) => ( {model} handleLimitChange(model, 'tpm', value)} /> handleLimitChange(model, 'rpm', value)} /> ))} {newModelRow !== null && ( - - )}
); }; const handleEditClick = (token: any) => { console.log("handleEditClick:", token); // set token.token to token.token_id if token_id is not null if (token.token == null) { if (token.token_id !== null) { token.token = token.token_id; } } // Convert the budget_duration to the corresponding select option let budgetDuration = null; if (token.budget_duration) { switch (token.budget_duration) { case "24h": budgetDuration = "daily"; break; case "7d": budgetDuration = "weekly"; break; case "30d": budgetDuration = "monthly"; break; default: budgetDuration = "None"; } } setSelectedToken({ ...token, budget_duration: budgetDuration }); //setSelectedToken(token); setEditModalVisible(true); }; const handleEditCancel = () => { setEditModalVisible(false); setSelectedToken(null); }; const handleEditSubmit = async (formValues: Record) => { /** * Call API to update team with teamId and values * * Client-side validation: For selected team, ensure models in team + max budget < team max budget */ if (accessToken == null) { return; } const currentKey = formValues.token; formValues.key = currentKey; // Convert the budget_duration back to the API expected format if (formValues.budget_duration) { switch (formValues.budget_duration) { case "daily": formValues.budget_duration = "24h"; break; case "weekly": formValues.budget_duration = "7d"; break; case "monthly": formValues.budget_duration = "30d"; break; } } console.log("handleEditSubmit:", formValues); let newKeyValues = await keyUpdateCall(accessToken, formValues); console.log("handleEditSubmit: newKeyValues", newKeyValues); // Update the keys with the update key if (data) { const updatedData = data.map((key) => key.token === currentKey ? newKeyValues : key ); setData(updatedData); } message.success("Key updated successfully"); setEditModalVisible(false); setSelectedToken(null); }; const handleDelete = async (token: any) => { console.log("handleDelete:", token); if (token.token == null) { if (token.token_id !== null) { token.token = token.token_id; } } if (data == null) { return; } // Set the key to delete and open the confirmation modal setKeyToDelete(token.token); localStorage.removeItem("userData" + userID); setIsDeleteModalOpen(true); }; const confirmDelete = async () => { if (keyToDelete == null || data == null) { return; } try { await keyDeleteCall(accessToken, keyToDelete); // Successfully completed the deletion. Update the state to trigger a rerender. const filteredData = data.filter((item) => item.token !== keyToDelete); setData(filteredData); } catch (error) { console.error("Error deleting the key:", error); // Handle any error situations, such as displaying an error message to the user. } // Close the confirmation modal and reset the keyToDelete setIsDeleteModalOpen(false); setKeyToDelete(null); }; const cancelDelete = () => { // Close the confirmation modal and reset the keyToDelete setIsDeleteModalOpen(false); setKeyToDelete(null); }; const handleRegenerateClick = (token: any) => { setSelectedToken(token); setNewExpiryTime(null); regenerateForm.setFieldsValue({ key_alias: token.key_alias, max_budget: token.max_budget, tpm_limit: token.tpm_limit, rpm_limit: token.rpm_limit, duration: token.duration || '', }); setRegenerateDialogVisible(true); }; const handleRegenerateFormChange = (field: string, value: any) => { setRegenerateFormData((prev: any) => ({ ...prev, [field]: value, })); }; const handleRegenerateKey = async () => { if (!premiumUser) { message.error("Regenerate API Key is an Enterprise feature. Please upgrade to use this feature."); return; } if (selectedToken == null) { return; } try { const formValues = await regenerateForm.validateFields(); const response = await regenerateKeyCall(accessToken, selectedToken.token, formValues); setRegeneratedKey(response.key); // Update the data state with the new key_name if (data) { const updatedData = data.map(item => item.token === selectedToken?.token ? { ...item, key_name: response.key_name, ...formValues } : item ); setData(updatedData); } setRegenerateDialogVisible(false); regenerateForm.resetFields(); message.success("API Key regenerated successfully"); } catch (error) { console.error("Error regenerating key:", error); message.error("Failed to regenerate API Key"); } }; if (data == null) { return; } console.log("RERENDER TRIGGERED"); return (
Key Alias Secret Key Created Expires Spend (USD) Budget (USD) Budget Reset Models Rate Limits Rate Limits per model {data.map((item) => { console.log(item); // skip item if item.team_id == "litellm-dashboard" if (item.team_id === "litellm-dashboard") { return null; } if (selectedTeam) { /** * 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 && !knownTeamIDs.has(item.team_id)) { // do nothing -> returns a row with this key } else if (item.team_id != selectedTeam.team_id) { return null; } console.log(`item team id: ${item.team_id}, is returned`) } return ( {item.key_alias != null ? ( {item.key_alias} ) : ( Not Set )} {item.key_name} {item.created_at != null ? (

{new Date(item.created_at).toLocaleDateString()}

) : (

Not available

)}
{item.expires != null ? (

{new Date(item.expires).toLocaleDateString()}

) : (

Never

)}
{(() => { try { return parseFloat(item.spend).toFixed(4); } catch (error) { return item.spend; } })()} {item.max_budget != null ? ( {item.max_budget} ) : ( Unlimited )} {item.budget_reset_at != null ? (

{new Date(item.budget_reset_at).toLocaleString()}

) : (

Never

)}
{/* */} {/* {item.team_alias && item.team_alias != "None" ? item.team_alias : item.team_id} */} {/* {JSON.stringify(item.metadata).slice(0, 400)} */} {Array.isArray(item.models) ? (
{item.models.length === 0 ? ( <> {selectedTeam && selectedTeam.models && selectedTeam.models.length > 0 ? ( selectedTeam.models.map((model: string, index: number) => ( model === "all-proxy-models" ? ( All Proxy Models ) : model === "all-team-models" ? ( All Team Models ) : ( {model.length > 30 ? `${model.slice(0, 30)}...` : model} ) )) ) : ( // If selected team is None or selected team's models are empty, show all models all-proxy-models )} ) : ( item.models.map((model: string, index: number) => ( model === "all-proxy-models" ? ( All Proxy Models ) : model === "all-team-models" ? ( All Team Models ) : ( {model.length > 30 ? `${model.slice(0, 30)}...` : model} ) )) )}
) : null}
TPM: {item.tpm_limit ? item.tpm_limit : "Unlimited"}{" "}

RPM:{" "} {item.rpm_limit ? item.rpm_limit : "Unlimited"}
{ setSelectedToken(item); setInfoDialogVisible(true); }} icon={InformationCircleIcon} size="sm" /> { setInfoDialogVisible(false); setSelectedToken(null); }} footer={null} width={800} > {selectedToken && ( <>

Spend

{(() => { try { return parseFloat(selectedToken.spend).toFixed(4); } catch (error) { return selectedToken.spend; } })()}

Budget

{selectedToken.max_budget != null ? ( <> {selectedToken.max_budget} {selectedToken.budget_duration && ( <>
Budget will be reset at {selectedToken.budget_reset_at ? new Date(selectedToken.budget_reset_at).toLocaleString() : 'Never'} )} ) : ( <>Unlimited )}

Expires

{selectedToken.expires != null ? ( <> {new Date(selectedToken.expires).toLocaleString(undefined, { day: 'numeric', month: 'long', year: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' })} ) : ( <>Never )}

Token Name {selectedToken.key_alias ? selectedToken.key_alias : selectedToken.key_name} Token ID {selectedToken.token} User ID {selectedToken.user_id} Metadata
{JSON.stringify(selectedToken.metadata)} 
)}
handleEditClick(item)} /> handleRegenerateClick(item)} icon={RefreshIcon} size="sm" /> handleDelete(item)} icon={TrashIcon} size="sm" />
); })}
{isDeleteModalOpen && (
{/* Modal Panel */} {/* Confirmation Modal Content */}

Delete Key

Are you sure you want to delete this key ?

)}
{selectedToken && ( )} {selectedToken && ( setModelLimitModalVisible(false)} token={selectedToken} onSubmit={handleModelLimitSubmit} accessToken={accessToken} /> )} {/* Regenerate Key Form Modal */} { setRegenerateDialogVisible(false); regenerateForm.resetFields(); }} footer={[ , ]} > {premiumUser ? (
{ if ('duration' in changedValues) { handleRegenerateFormChange('duration', changedValues.duration); } }} >
Current expiry: { selectedToken?.expires != null ? ( new Date(selectedToken.expires).toLocaleString() ) : ( 'Never' ) }
{newExpiryTime && (
New expiry: {newExpiryTime}
)}
) : (

Upgrade to use this feature

)}
{/* Regenerated Key Display Modal */} {regeneratedKey && ( setRegeneratedKey(null)} footer={[ ]} > Regenerated Key

Please replace your old key with the new key generated. 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.

Key Alias:
                {selectedToken?.key_alias || 'No alias set'}
              
New API Key:
                {regeneratedKey}
              
message.success("API Key copied to clipboard")}>
)}
); }; export default ViewKeyTable;