Raju2024's picture
Upload 1072 files
e3278e4 verified
"use client";
import React, { useEffect, useState } from "react";
import {
keyDeleteCall,
modelAvailableCall,
getGuardrailsList,
} 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,
Textarea,
} from "@tremor/react";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
fetchAvailableModelsForTeamOrKey,
getModelDisplayName,
} from "./key_team_helpers/fetch_available_models_team_key";
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";
import TextArea from "antd/es/input/TextArea";
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<React.SetStateAction<any[] | null>>;
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<ViewKeyTableProps> = ({
userID,
userRole,
accessToken,
selectedTeam,
data,
setData,
teams,
premiumUser,
}) => {
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 [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<ItemData | null>(null);
const [userModels, setUserModels] = useState<string[]>([]);
const initialKnownTeamIDs: Set<string> = new Set();
const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false);
const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false);
const [regeneratedKey, setRegeneratedKey] = useState<string | null>(null);
const [regenerateFormData, setRegenerateFormData] = useState<any>(null);
const [regenerateForm] = Form.useForm();
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
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(() => {
let allKeys: any[] = [];
// If no teams, return personal keys
if (!teams || teams.length === 0) {
return data;
}
teams.forEach((team) => {
// For default team or when user is not admin, use personal keys (data)
if (team.team_id === "default-team" || !isUserTeamAdmin(team)) {
if (selectedTeam && selectedTeam.team_id === team.team_id && data) {
allKeys = [
...allKeys,
...data.filter((key) => key.team_id === team.team_id),
];
}
}
// For teams where user is admin, use team keys
else if (isUserTeamAdmin(team)) {
if (selectedTeam && selectedTeam.team_id === team.team_id) {
allKeys = [
...allKeys,
...(data?.filter((key) => key?.team_id === team?.team_id) || []),
];
}
}
});
// If no team is selected, show all accessible keys
if ((!selectedTeam || selectedTeam.team_alias === "Default Team") && data) {
const personalKeys = data.filter(
(key) => !key.team_id || key.team_id === "default-team"
);
const adminTeamKeys = teams
.filter((team) => isUserTeamAdmin(team))
.flatMap((team) => team.keys || []);
allKeys = [...personalKeys, ...adminTeamKeys];
}
// Filter out litellm-dashboard keys
allKeys = allKeys.filter((key) => key.team_id !== "litellm-dashboard");
// Remove duplicates based on token
const uniqueKeys = Array.from(
new Map(allKeys.map((key) => [key.token, key])).values()
);
return uniqueKeys;
}, [data, teams, selectedTeam, userID]);
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 || userRole === null || accessToken === null) {
return;
}
const models = await fetchAvailableModelsForTeamOrKey(
userID,
userRole,
accessToken
);
if (models) {
setUserModels(models);
}
} 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<string> = 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<EditKeyModalProps> = ({
visible,
onCancel,
token,
onSubmit,
}) => {
const [form] = Form.useForm();
const [keyTeam, setKeyTeam] = useState(selectedTeam);
const [errorModels, setErrorModels] = useState<string[]>([]);
const [errorBudget, setErrorBudget] = useState<boolean>(false);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
useEffect(() => {
const fetchGuardrails = async () => {
try {
const response = await getGuardrailsList(accessToken);
const guardrailNames = response.guardrails.map(
(g: { guardrail_name: string }) => g.guardrail_name
);
setGuardrailsList(guardrailNames);
} catch (error) {
console.error("Failed to fetch guardrails:", error);
}
};
fetchGuardrails();
}, [accessToken]);
let metadataString = "";
try {
// Create a copy of metadata without guardrails for display
const displayMetadata = { ...token.metadata };
delete displayMetadata.guardrails;
metadataString = JSON.stringify(displayMetadata, null, 2);
} catch (error) {
console.error("Error stringifying metadata:", error);
metadataString = "";
}
// Extract existing guardrails from metadata
let existingGuardrails: string[] = [];
try {
existingGuardrails = token.metadata?.guardrails || [];
} catch (error) {
console.error("Error extracting guardrails:", error);
}
const initialValues =
token ?
{
...token,
budget_duration: token.budget_duration,
metadata: metadataString,
guardrails: existingGuardrails,
}
: { metadata: metadataString, guardrails: [] };
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 (
<Modal
title="Edit Key"
visible={visible}
width={800}
footer={null}
onOk={handleOk}
onCancel={onCancel}
>
<Form
form={form}
onFinish={handleEditSubmit}
initialValues={initialValues}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<>
<Form.Item name="key_alias" label="Key Alias">
<TextInput />
</Form.Item>
<Form.Item
label="Models"
name="models"
rules={[
{
validator: (rule, value) => {
if (keyTeam.team_alias === "Default Team") {
return Promise.resolve();
}
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();
}
},
},
]}
>
<Select
mode="multiple"
placeholder="Select models"
style={{ width: "100%" }}
>
<Option key="all-team-models" value="all-team-models">
All Team Models
</Option>
{keyTeam.team_alias === "Default Team" ?
userModels
.filter((model) => model !== "all-proxy-models")
.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
: keyTeam.models.map((model: string) => (
<Option key={model} value={model}>
{getModelDisplayName(model)}
</Option>
))
}
</Select>
</Form.Item>
<Form.Item
className="mt-8"
label="Max Budget (USD)"
name="max_budget"
help={`Budget cannot exceed team max budget: ${keyTeam?.max_budget !== null && keyTeam?.max_budget !== undefined ? keyTeam?.max_budget : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
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}`
);
}
},
},
]}
>
<InputNumber step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="Reset Budget"
name="budget_duration"
help={`Current Reset Budget: ${
token.budget_duration
}, budget will be reset: ${token.budget_reset_at ? new Date(token.budget_reset_at).toLocaleString() : "Never"}`}
>
<Select placeholder="n/a">
<Select.Option value="daily">daily</Select.Option>
<Select.Option value="weekly">weekly</Select.Option>
<Select.Option value="monthly">monthly</Select.Option>
</Select>
</Form.Item>
<Form.Item label="token" name="token" hidden={true}></Form.Item>
<Form.Item
label="Team"
name="team_id"
className="mt-8"
help="the team this key belongs to"
>
<Select3 value={token.team_alias}>
{teams?.map((team_obj, index) => (
<SelectItem
key={index}
value={team_obj.team_id}
onClick={() => setKeyTeam(team_obj)}
>
{team_obj.team_alias}
</SelectItem>
))}
</Select3>
</Form.Item>
<Form.Item
className="mt-8"
label="TPM Limit (tokens per minute)"
name="tpm_limit"
help={`tpm_limit cannot exceed team tpm_limit ${keyTeam?.tpm_limit !== null && keyTeam?.tpm_limit !== undefined ? keyTeam?.tpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
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}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
className="mt-8"
label="RPM Limit (requests per minute)"
name="rpm_limit"
help={`rpm_limit cannot exceed team max rpm_limit: ${keyTeam?.rpm_limit !== null && keyTeam?.rpm_limit !== undefined ? keyTeam?.rpm_limit : "unlimited"}`}
rules={[
{
validator: async (_, value) => {
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}`
);
}
},
},
]}
>
<InputNumber step={1} precision={1} width={200} />
</Form.Item>
<Form.Item
label={
<span>
Guardrails{" "}
<Tooltip title="Setup your first guardrail">
<a
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</a>
</Tooltip>
</span>
}
name="guardrails"
className="mt-8"
help="Select existing guardrails or enter new ones"
>
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Select or enter guardrails"
options={guardrailsList.map((name) => ({
value: name,
label: name,
}))}
/>
</Form.Item>
<Form.Item
label="Metadata (ensure this is valid JSON)"
name="metadata"
>
<TextArea
rows={10}
onChange={(e) => {
form.setFieldsValue({ metadata: e.target.value });
}}
/>
</Form.Item>
</>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Edit Key</Button2>
</div>
</Form>
</Modal>
);
};
const ModelLimitModal: React.FC<ModelLimitModalProps> = ({
visible,
onCancel,
token,
onSubmit,
accessToken,
}) => {
const [modelLimits, setModelLimits] = useState<{
[key: string]: { tpm: number; rpm: number };
}>({});
const [availableModels, setAvailableModels] = useState<string[]>([]);
const [newModelRow, setNewModelRow] = useState<string | null>(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 (
<Modal
title="Edit Model-Specific Limits"
visible={visible}
onCancel={onCancel}
footer={null}
width={800}
>
<div className="space-y-4">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Model</TableHeaderCell>
<TableHeaderCell>TPM Limit</TableHeaderCell>
<TableHeaderCell>RPM Limit</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(modelLimits).map(([model, limits]) => (
<TableRow key={model}>
<TableCell>{model}</TableCell>
<TableCell>
<InputNumber
value={limits.tpm}
onChange={(value) =>
handleLimitChange(model, "tpm", value)
}
/>
</TableCell>
<TableCell>
<InputNumber
value={limits.rpm}
onChange={(value) =>
handleLimitChange(model, "rpm", value)
}
/>
</TableCell>
<TableCell>
<Button onClick={() => handleRemoveModel(model)}>
Remove
</Button>
</TableCell>
</TableRow>
))}
{newModelRow !== null && (
<TableRow>
<TableCell>
<Select
style={{ width: 200 }}
placeholder="Select a model"
onChange={handleModelSelect}
value={newModelRow || undefined}
>
{availableModels
.filter((m) => !modelLimits.hasOwnProperty(m))
.map((m) => (
<Option key={m} value={m}>
{m}
</Option>
))}
</Select>
</TableCell>
<TableCell>-</TableCell>
<TableCell>-</TableCell>
<TableCell>
<Button onClick={() => setNewModelRow(null)}>Cancel</Button>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<Button onClick={handleAddLimit} disabled={newModelRow !== null}>
Add Limit
</Button>
</div>
<div className="flex justify-end space-x-4 mt-6">
<Button onClick={onCancel}>Cancel</Button>
<Button onClick={handleSubmit}>Save</Button>
</div>
</Modal>
);
};
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<string, any>) => {
/**
* 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 metadata back to an object if it exists and is a string
if (formValues.metadata && typeof formValues.metadata === "string") {
try {
const parsedMetadata = JSON.parse(formValues.metadata);
// Only add guardrails if they are set in form values
formValues.metadata = {
...parsedMetadata,
...(formValues.guardrails?.length > 0 ?
{ guardrails: formValues.guardrails }
: {}),
};
} catch (error) {
console.error("Error parsing metadata JSON:", error);
message.error(
"Invalid metadata JSON for formValue " + formValues.metadata
);
return;
}
} else {
// If metadata is not a string (or doesn't exist), only add guardrails if they are set
formValues.metadata = {
...(formValues.metadata || {}),
...(formValues.guardrails?.length > 0 ?
{ guardrails: formValues.guardrails }
: {}),
};
}
// 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);
try {
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);
} catch (error) {
console.error("Error updating key:", error);
message.error("Failed to update key");
}
};
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 (
<div>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4 mt-2">
<Table className="mt-5 max-h-[300px] min-h-[300px]">
<TableHead>
<TableRow>
<TableHeaderCell>Key Alias</TableHeaderCell>
<TableHeaderCell>Secret Key</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Expires</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Budget Reset</TableHeaderCell>
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>Rate Limits</TableHeaderCell>
<TableHeaderCell>Rate Limits per model</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{all_keys_to_display &&
all_keys_to_display.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 (
<TableRow key={item.token}>
<TableCell
style={{
maxWidth: "2px",
whiteSpace: "pre-wrap",
overflow: "hidden",
}}
>
{item.key_alias != null ?
<Text>{item.key_alias}</Text>
: <Text>Not Set</Text>}
</TableCell>
<TableCell>
<Text>{item.key_name}</Text>
</TableCell>
<TableCell>
{item.created_at != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.created_at).toLocaleDateString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Not available</p>}
</TableCell>
<TableCell>
{item.expires != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.expires).toLocaleDateString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Never</p>}
</TableCell>
<TableCell>
<Text>
{(() => {
try {
return parseFloat(item.spend).toFixed(4);
} catch (error) {
return item.spend;
}
})()}
</Text>
</TableCell>
<TableCell>
{item.max_budget != null ?
<Text>{item.max_budget}</Text>
: <Text>Unlimited</Text>}
</TableCell>
<TableCell>
{item.budget_reset_at != null ?
<div>
<p style={{ fontSize: "0.70rem" }}>
{new Date(item.budget_reset_at).toLocaleString()}
</p>
</div>
: <p style={{ fontSize: "0.70rem" }}>Never</p>}
</TableCell>
{/* <TableCell style={{ maxWidth: '2px' }}>
<ViewKeySpendReport
token={item.token}
accessToken={accessToken}
keySpend={item.spend}
keyBudget={item.max_budget}
keyName={item.key_name}
/>
</TableCell> */}
{/* <TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>
<Text>{item.team_alias && item.team_alias != "None" ? item.team_alias : item.team_id}</Text>
</TableCell> */}
{/* <TableCell style={{ maxWidth: "4px", whiteSpace: "pre-wrap", overflow: "hidden" }}>
<Text>{JSON.stringify(item.metadata).slice(0, 400)}</Text>
</TableCell> */}
<TableCell>
{Array.isArray(item.models) ?
<div
style={{ display: "flex", flexDirection: "column" }}
>
{item.models.length === 0 ?
<>
{
(
selectedTeam &&
selectedTeam.models &&
selectedTeam.models.length > 0
) ?
selectedTeam.models.map(
(model: string, index: number) =>
model === "all-proxy-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
: model === "all-team-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Team Models</Text>
</Badge>
: <Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30 ?
`${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
// If selected team is None or selected team's models are empty, show all models
: <Badge
size={"xs"}
className="mb-1"
color="blue"
>
<Text>all-proxy-models</Text>
</Badge>
}
</>
: item.models.map((model: string, index: number) =>
model === "all-proxy-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Proxy Models</Text>
</Badge>
: model === "all-team-models" ?
<Badge
key={index}
size={"xs"}
className="mb-1"
color="red"
>
<Text>All Team Models</Text>
</Badge>
: <Badge
key={index}
size={"xs"}
className="mb-1"
color="blue"
>
<Text>
{model.length > 30 ?
`${getModelDisplayName(model).slice(0, 30)}...`
: getModelDisplayName(model)}
</Text>
</Badge>
)
}
</div>
: null}
</TableCell>
<TableCell>
<Text>
TPM: {item.tpm_limit ? item.tpm_limit : "Unlimited"}{" "}
<br></br> RPM:{" "}
{item.rpm_limit ? item.rpm_limit : "Unlimited"}
</Text>
</TableCell>
<TableCell>
<Button
size="xs"
onClick={() => handleModelLimitClick(item)}
>
Edit Limits
</Button>
</TableCell>
<TableCell>
<Icon
onClick={() => {
setSelectedToken(item);
setInfoDialogVisible(true);
}}
icon={InformationCircleIcon}
size="sm"
/>
<Modal
open={infoDialogVisible}
onCancel={() => {
setInfoDialogVisible(false);
setSelectedToken(null);
}}
footer={null}
width={800}
>
{selectedToken && (
<>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-8">
<Card>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Spend
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{(() => {
try {
return parseFloat(
selectedToken.spend
).toFixed(4);
} catch (error) {
return selectedToken.spend;
}
})()}
</p>
</div>
</Card>
<Card key={item.name}>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Budget
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
{selectedToken.max_budget != null ?
<>
{selectedToken.max_budget}
{selectedToken.budget_duration && (
<>
<br />
Budget will be reset at{" "}
{selectedToken.budget_reset_at ?
new Date(
selectedToken.budget_reset_at
).toLocaleString()
: "Never"}
</>
)}
</>
: <>Unlimited</>}
</p>
</div>
</Card>
<Card key={item.name}>
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
Expires
</p>
<div className="mt-2 flex items-baseline space-x-2.5">
<p className="text-tremor-default font-small text-tremor-content-strong dark:text-dark-tremor-content-strong">
{selectedToken.expires != null ?
<>
{new Date(
selectedToken.expires
).toLocaleString(undefined, {
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
})}
</>
: <>Never</>}
</p>
</div>
</Card>
</div>
<Card className="my-4">
<Title>Token Name</Title>
<Text className="my-1">
{selectedToken.key_alias ?
selectedToken.key_alias
: selectedToken.key_name}
</Text>
<Title>Token ID</Title>
<Text className="my-1 text-[12px]">
{selectedToken.token}
</Text>
<Title>User ID</Title>
<Text className="my-1 text-[12px]">
{selectedToken.user_id}
</Text>
<Title>Metadata</Title>
<Text className="my-1">
<pre>
{JSON.stringify(selectedToken.metadata)}{" "}
</pre>
</Text>
</Card>
<Button
className="mx-auto flex items-center"
onClick={() => {
setInfoDialogVisible(false);
setSelectedToken(null);
}}
>
Close
</Button>
</>
)}
</Modal>
<Icon
icon={PencilAltIcon}
size="sm"
onClick={() => handleEditClick(item)}
/>
<Icon
onClick={() => handleRegenerateClick(item)}
icon={RefreshIcon}
size="sm"
/>
<Icon
onClick={() => handleDelete(item)}
icon={TrashIcon}
size="sm"
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{isDeleteModalOpen && (
<div className="fixed z-10 inset-0 overflow-y-auto">
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 transition-opacity"
aria-hidden="true"
>
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
{/* Modal Panel */}
<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>
{/* Confirmation Modal Content */}
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900">
Delete Key
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Are you sure you want to delete this key ?
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<Button onClick={confirmDelete} color="red" className="ml-2">
Delete
</Button>
<Button onClick={cancelDelete}>Cancel</Button>
</div>
</div>
</div>
</div>
)}
</Card>
{selectedToken && (
<EditKeyModal
visible={editModalVisible}
onCancel={handleEditCancel}
token={selectedToken}
onSubmit={handleEditSubmit}
/>
)}
{selectedToken && (
<ModelLimitModal
visible={modelLimitModalVisible}
onCancel={() => setModelLimitModalVisible(false)}
token={selectedToken}
onSubmit={handleModelLimitSubmit}
accessToken={accessToken}
/>
)}
{/* Regenerate Key Form Modal */}
<Modal
title="Regenerate API Key"
visible={regenerateDialogVisible}
onCancel={() => {
setRegenerateDialogVisible(false);
regenerateForm.resetFields();
}}
footer={[
<Button
key="cancel"
onClick={() => {
setRegenerateDialogVisible(false);
regenerateForm.resetFields();
}}
className="mr-2"
>
Cancel
</Button>,
<Button
key="regenerate"
onClick={handleRegenerateKey}
disabled={!premiumUser}
>
{premiumUser ? "Regenerate" : "Upgrade to Regenerate"}
</Button>,
]}
>
{premiumUser ?
<Form
form={regenerateForm}
layout="vertical"
onValuesChange={(changedValues, allValues) => {
if ("duration" in changedValues) {
handleRegenerateFormChange("duration", changedValues.duration);
}
}}
>
<Form.Item name="key_alias" label="Key Alias">
<TextInput disabled={true} />
</Form.Item>
<Form.Item name="max_budget" label="Max Budget (USD)">
<InputNumber
step={0.01}
precision={2}
style={{ width: "100%" }}
/>
</Form.Item>
<Form.Item name="tpm_limit" label="TPM Limit">
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item name="rpm_limit" label="RPM Limit">
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item
name="duration"
label="Expire Key (eg: 30s, 30h, 30d)"
className="mt-8"
>
<TextInput placeholder="" />
</Form.Item>
<div className="mt-2 text-sm text-gray-500">
Current expiry:{" "}
{selectedToken?.expires != null ?
new Date(selectedToken.expires).toLocaleString()
: "Never"}
</div>
{newExpiryTime && (
<div className="mt-2 text-sm text-green-600">
New expiry: {newExpiryTime}
</div>
)}
</Form>
: <div>
<p className="mb-2 text-gray-500 italic text-[12px]">
Upgrade to use this feature
</p>
<Button variant="primary" className="mb-2">
<a
href="https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat"
target="_blank"
>
Get Free Trial
</a>
</Button>
</div>
}
</Modal>
{/* Regenerated Key Display Modal */}
{regeneratedKey && (
<Modal
visible={!!regeneratedKey}
onCancel={() => setRegeneratedKey(null)}
footer={[
<Button key="close" onClick={() => setRegeneratedKey(null)}>
Close
</Button>,
]}
>
<Grid numItems={1} className="gap-2 w-full">
<Title>Regenerated Key</Title>
<Col numColSpan={1}>
<p>
Please replace your old key with the new key generated. For
security reasons, <b>you will not be able to view it again</b>{" "}
through your LiteLLM account. If you lose this secret key, you
will need to generate a new one.
</p>
</Col>
<Col numColSpan={1}>
<Text className="mt-3">Key Alias:</Text>
<div
style={{
background: "#f8f8f8",
padding: "10px",
borderRadius: "5px",
marginBottom: "10px",
}}
>
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>
{selectedToken?.key_alias || "No alias set"}
</pre>
</div>
<Text className="mt-3">New API Key:</Text>
<div
style={{
background: "#f8f8f8",
padding: "10px",
borderRadius: "5px",
marginBottom: "10px",
}}
>
<pre style={{ wordWrap: "break-word", whiteSpace: "normal" }}>
{regeneratedKey}
</pre>
</div>
<CopyToClipboard
text={regeneratedKey}
onCopy={() => message.success("API Key copied to clipboard")}
>
<Button className="mt-3">Copy API Key</Button>
</CopyToClipboard>
</Col>
</Grid>
</Modal>
)}
</div>
);
};
export default ViewKeyTable;