forked from phoenix/litellm-mirror
Merge branch 'main' into litellm_invite_link_flow_2
This commit is contained in:
commit
f9862be049
100 changed files with 5297 additions and 883 deletions
|
@ -18,7 +18,7 @@ import Usage from "../components/usage";
|
|||
import { jwtDecode } from "jwt-decode";
|
||||
import { Typography } from "antd";
|
||||
|
||||
export function formatUserRole(userRole: string) {
|
||||
function formatUserRole(userRole: string) {
|
||||
if (!userRole) {
|
||||
return "Undefined Role";
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ import {
|
|||
User,
|
||||
setCallbacksCall,
|
||||
invitationCreateCall,
|
||||
getPossibleUserRoles,
|
||||
} from "./networking";
|
||||
|
||||
const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
|
@ -83,6 +84,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
useState(false);
|
||||
const router = useRouter();
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [isInstructionsModalVisible, setIsInstructionsModalVisible] = useState(false);
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<string, Record<string, string>>>(null);
|
||||
|
||||
|
||||
let nonSssoUrl;
|
||||
try {
|
||||
|
@ -163,6 +167,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
console.log(`proxy admins: ${proxyAdmins}`);
|
||||
console.log(`combinedList: ${combinedList}`);
|
||||
setAdmins(combinedList);
|
||||
|
||||
const availableUserRoles = await getPossibleUserRoles(accessToken);
|
||||
setPossibleUIRoles(availableUserRoles);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -435,7 +442,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
? member["user_id"]
|
||||
: null}
|
||||
</TableCell>
|
||||
<TableCell>{member["user_role"]}</TableCell>
|
||||
<TableCell> {possibleUIRoles?.[member?.user_role]?.ui_label || "-"}</TableCell>
|
||||
<TableCell>
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
|
|
|
@ -149,6 +149,12 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (inputMessage.trim() === "") return;
|
||||
|
||||
|
@ -260,6 +266,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
type="text"
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyDown={handleKeyDown} // Add this line
|
||||
placeholder="Type your message..."
|
||||
/>
|
||||
<Button
|
||||
|
|
138
ui/litellm-dashboard/src/components/edit_user.tsx
Normal file
138
ui/litellm-dashboard/src/components/edit_user.tsx
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
TextInput,
|
||||
Button,
|
||||
Select,
|
||||
SelectItem,
|
||||
Text,
|
||||
Title,
|
||||
Subtitle,
|
||||
} from '@tremor/react';
|
||||
|
||||
import {
|
||||
Button as Button2,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select as Select2,
|
||||
InputNumber,
|
||||
message,
|
||||
} from "antd";
|
||||
|
||||
interface EditUserModalProps {
|
||||
visible: boolean;
|
||||
possibleUIRoles: null | Record<string, Record<string, string>>;
|
||||
onCancel: () => void;
|
||||
user: any;
|
||||
onSubmit: (data: any) => void;
|
||||
}
|
||||
|
||||
const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => {
|
||||
const [editedUser, setEditedUser] = useState(user);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
}, [user]);
|
||||
|
||||
const handleCancel = async () => {
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (formValues: Record<string, any>) => {
|
||||
// Call API to update team with teamId and values
|
||||
onSubmit(formValues);
|
||||
form.resetFields();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Modal
|
||||
visible={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
title={"Edit User " + user.user_id}
|
||||
width={1000}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleEditSubmit}
|
||||
initialValues={user} // Pass initial values here
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label="User Email"
|
||||
tooltip="Email of the User"
|
||||
name="user_email">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="user_id"
|
||||
name="user_id"
|
||||
hidden={true}
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Role"
|
||||
name="user_role"
|
||||
>
|
||||
<Select2>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
|
||||
<SelectItem key={role} value={role} title={ui_label}>
|
||||
<div className='flex'>
|
||||
{ui_label} <p className="ml-2" style={{ color: "gray", fontSize: "12px" }}>{description}</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select2>
|
||||
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Spend (USD)"
|
||||
name="spend"
|
||||
tooltip="(float) - Spend of all LLM calls completed by this user"
|
||||
>
|
||||
<InputNumber min={0} step={1} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Budget (USD)"
|
||||
name="max_budget"
|
||||
tooltip="(float) - Maximum budget of this user"
|
||||
>
|
||||
<InputNumber min={0} step={1} />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
</Form>
|
||||
|
||||
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUserModal;
|
|
@ -79,7 +79,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||
<Text>Users</Text>
|
||||
<Text>Internal Users</Text>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
|
||||
|
@ -91,7 +91,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="9" onClick={() => setPage("budgets")}>
|
||||
<Text>Rate Limits</Text>
|
||||
<Text>Budgets</Text>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -49,6 +49,8 @@ import {
|
|||
getCallbacksCall,
|
||||
setCallbacksCall,
|
||||
modelSettingsCall,
|
||||
adminGlobalActivityExceptions,
|
||||
adminGlobalActivityExceptionsPerDeployment,
|
||||
} from "./networking";
|
||||
import { BarChart, AreaChart } from "@tremor/react";
|
||||
import {
|
||||
|
@ -109,6 +111,13 @@ interface RetryPolicyObject {
|
|||
[key: string]: { [retryPolicyKey: string]: number } | undefined;
|
||||
}
|
||||
|
||||
|
||||
interface GlobalExceptionActivityData {
|
||||
sum_num_rate_limit_exceptions: number;
|
||||
daily_data: { date: string; num_rate_limit_exceptions: number; }[];
|
||||
}
|
||||
|
||||
|
||||
//["OpenAI", "Azure OpenAI", "Anthropic", "Gemini (Google AI Studio)", "Amazon Bedrock", "OpenAI-Compatible Endpoints (Groq, Together AI, Mistral AI, etc.)"]
|
||||
|
||||
interface ProviderFields {
|
||||
|
@ -301,6 +310,9 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
|||
useState<RetryPolicyObject | null>(null);
|
||||
const [defaultRetry, setDefaultRetry] = useState<number>(0);
|
||||
|
||||
const [globalExceptionData, setGlobalExceptionData] = useState<GlobalExceptionActivityData>({} as GlobalExceptionActivityData);
|
||||
const [globalExceptionPerDeployment, setGlobalExceptionPerDeployment] = useState<any[]>([]);
|
||||
|
||||
function formatCreatedAt(createdAt: string | null) {
|
||||
if (createdAt) {
|
||||
const date = new Date(createdAt);
|
||||
|
@ -643,6 +655,29 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
|||
dateValue.to?.toISOString()
|
||||
);
|
||||
|
||||
const dailyExceptions = await adminGlobalActivityExceptions(
|
||||
accessToken,
|
||||
dateValue.from?.toISOString().split('T')[0],
|
||||
dateValue.to?.toISOString().split('T')[0],
|
||||
_initial_model_group,
|
||||
);
|
||||
|
||||
setGlobalExceptionData(dailyExceptions);
|
||||
|
||||
const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment(
|
||||
accessToken,
|
||||
dateValue.from?.toISOString().split('T')[0],
|
||||
dateValue.to?.toISOString().split('T')[0],
|
||||
_initial_model_group,
|
||||
)
|
||||
|
||||
setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment);
|
||||
|
||||
console.log("dailyExceptions:", dailyExceptions);
|
||||
|
||||
console.log("dailyExceptionsPerDeplyment:", dailyExceptionsPerDeplyment);
|
||||
|
||||
|
||||
console.log("slowResponses:", slowResponses);
|
||||
|
||||
setSlowResponsesData(slowResponses);
|
||||
|
@ -905,6 +940,30 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
|||
console.log("slowResponses:", slowResponses);
|
||||
|
||||
setSlowResponsesData(slowResponses);
|
||||
|
||||
|
||||
if (modelGroup) {
|
||||
const dailyExceptions = await adminGlobalActivityExceptions(
|
||||
accessToken,
|
||||
startTime?.toISOString().split('T')[0],
|
||||
endTime?.toISOString().split('T')[0],
|
||||
modelGroup,
|
||||
);
|
||||
|
||||
setGlobalExceptionData(dailyExceptions);
|
||||
|
||||
const dailyExceptionsPerDeplyment = await adminGlobalActivityExceptionsPerDeployment(
|
||||
accessToken,
|
||||
startTime?.toISOString().split('T')[0],
|
||||
endTime?.toISOString().split('T')[0],
|
||||
modelGroup,
|
||||
)
|
||||
|
||||
setGlobalExceptionPerDeployment(dailyExceptionsPerDeplyment);
|
||||
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch model metrics", error);
|
||||
}
|
||||
|
@ -1475,7 +1534,8 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
|||
)}
|
||||
{selectedProvider != Providers.Bedrock &&
|
||||
selectedProvider != Providers.Vertex_AI &&
|
||||
dynamicProviderForm === undefined && (
|
||||
(dynamicProviderForm === undefined ||
|
||||
dynamicProviderForm.fields.length == 0) && (
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: "Required" }]}
|
||||
label="API Key"
|
||||
|
@ -1777,18 +1837,110 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
|
|||
</Card>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Card className="mt-4">
|
||||
<Title>Exceptions per Model</Title>
|
||||
<BarChart
|
||||
className="h-72"
|
||||
data={modelExceptions}
|
||||
index="model"
|
||||
categories={allExceptions}
|
||||
stack={true}
|
||||
colors={["indigo-300", "rose-200", "#ffcc33"]}
|
||||
yAxisWidth={30}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Grid numItems={1} className="gap-2 w-full mt-2">
|
||||
<Card>
|
||||
<Title>All Up Rate Limit Errors (429) for {selectedModelGroup}</Title>
|
||||
<Grid numItems={1}>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Num Rate Limit Errors { (globalExceptionData.sum_num_rate_limit_exceptions)}</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalExceptionData.daily_data}
|
||||
index="date"
|
||||
colors={['rose']}
|
||||
categories={['num_rate_limit_exceptions']}
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
|
||||
{/* <BarChart
|
||||
className="h-40"
|
||||
data={modelExceptions}
|
||||
index="model"
|
||||
categories={allExceptions}
|
||||
stack={true}
|
||||
yAxisWidth={30}
|
||||
/> */}
|
||||
|
||||
|
||||
</Col>
|
||||
|
||||
</Grid>
|
||||
|
||||
|
||||
</Card>
|
||||
|
||||
{
|
||||
premiumUser ? (
|
||||
<>
|
||||
{globalExceptionPerDeployment.map((globalActivity, index) => (
|
||||
<Card key={index}>
|
||||
<Title>{globalActivity.api_base ? globalActivity.api_base : "Unknown API Base"}</Title>
|
||||
<Grid numItems={1}>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Num Rate Limit Errors (429) {(globalActivity.sum_num_rate_limit_exceptions)}</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
index="date"
|
||||
colors={['rose']}
|
||||
categories={['num_rate_limit_exceptions']}
|
||||
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
|
||||
</Col>
|
||||
</Grid>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
) :
|
||||
<>
|
||||
{globalExceptionPerDeployment && globalExceptionPerDeployment.length > 0 &&
|
||||
globalExceptionPerDeployment.slice(0, 1).map((globalActivity, index) => (
|
||||
<Card key={index}>
|
||||
<Title>✨ Rate Limit Errors by Deployment</Title>
|
||||
<p className="mb-2 text-gray-500 italic text-[12px]">Upgrade to see exceptions for all deployments</p>
|
||||
<Button variant="primary" className="mb-2">
|
||||
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
|
||||
Get Free Trial
|
||||
</a>
|
||||
</Button>
|
||||
<Card>
|
||||
<Title>{globalActivity.api_base}</Title>
|
||||
<Grid numItems={1}>
|
||||
<Col>
|
||||
<Subtitle
|
||||
style={{
|
||||
fontSize: "15px",
|
||||
fontWeight: "normal",
|
||||
color: "#535452",
|
||||
}}
|
||||
>
|
||||
Num Rate Limit Errors {(globalActivity.sum_num_rate_limit_exceptions)}
|
||||
</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
index="date"
|
||||
colors={['rose']}
|
||||
categories={['num_rate_limit_exceptions']}
|
||||
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
|
||||
</Grid>
|
||||
</Card>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
</Grid>
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="flex items-center">
|
||||
|
|
|
@ -39,7 +39,9 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
|
||||
// const userColors = require('./ui_colors.json') || {};
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
||||
const logoutUrl = proxyBaseUrl ? `${proxyBaseUrl}` : `/`;
|
||||
|
||||
const items: MenuProps["items"] = [
|
||||
{
|
||||
|
@ -52,6 +54,14 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: (
|
||||
<Link href={logoutUrl}>
|
||||
<p>Logout</p>
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
@ -1270,6 +1270,100 @@ export const adminGlobalActivityPerModel = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const adminGlobalActivityExceptions = async (
|
||||
accessToken: String,
|
||||
startTime: String | undefined,
|
||||
endTime: String | undefined,
|
||||
modelGroup: String,
|
||||
) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/global/activity/exceptions`
|
||||
: `/global/activity/exceptions`;
|
||||
|
||||
if (startTime && endTime) {
|
||||
url += `?start_date=${startTime}&end_date=${endTime}`;
|
||||
}
|
||||
|
||||
if (modelGroup) {
|
||||
url += `&model_group=${modelGroup}`;
|
||||
}
|
||||
|
||||
const requestOptions: {
|
||||
method: string;
|
||||
headers: {
|
||||
Authorization: string;
|
||||
};
|
||||
} = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch spend data:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const adminGlobalActivityExceptionsPerDeployment = async (
|
||||
accessToken: String,
|
||||
startTime: String | undefined,
|
||||
endTime: String | undefined,
|
||||
modelGroup: String,
|
||||
) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/global/activity/exceptions/deployment`
|
||||
: `/global/activity/exceptions/deployment`;
|
||||
|
||||
if (startTime && endTime) {
|
||||
url += `?start_date=${startTime}&end_date=${endTime}`;
|
||||
}
|
||||
|
||||
if (modelGroup) {
|
||||
url += `&model_group=${modelGroup}`;
|
||||
}
|
||||
|
||||
const requestOptions: {
|
||||
method: string;
|
||||
headers: {
|
||||
Authorization: string;
|
||||
};
|
||||
} = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(url, requestOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch spend data:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const adminTopModelsCall = async (accessToken: String) => {
|
||||
try {
|
||||
let url = proxyBaseUrl
|
||||
|
@ -1465,6 +1559,34 @@ export const userGetAllUsersCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const getPossibleUserRoles = async (
|
||||
accessToken: String,
|
||||
) => {
|
||||
try {
|
||||
const url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/user/available_roles`
|
||||
: `/user/available_roles`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text();
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("response from user/available_role", data);
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const teamCreateCall = async (
|
||||
accessToken: string,
|
||||
formValues: Record<string, any> // Assuming formValues is an object
|
||||
|
|
|
@ -188,6 +188,43 @@ const Settings: React.FC<SettingsPageProps> = ({
|
|||
console.log("Selected values:", values);
|
||||
};
|
||||
|
||||
const handleSaveEmailSettings = () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let updatedVariables: Record<string, string> = {};
|
||||
|
||||
alerts
|
||||
.filter((alert) => alert.name === "email")
|
||||
.forEach((alert) => {
|
||||
Object.entries(alert.variables ?? {}).forEach(([key, value]) => {
|
||||
const inputElement = document.querySelector(`input[name="${key}"]`) as HTMLInputElement;
|
||||
if (inputElement && inputElement.value) {
|
||||
updatedVariables[key] = inputElement?.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log("updatedVariables", updatedVariables);
|
||||
//filter out null / undefined values for updatedVariables
|
||||
|
||||
const payload = {
|
||||
general_settings: {
|
||||
alerting: ["email"],
|
||||
},
|
||||
environment_variables: updatedVariables,
|
||||
};
|
||||
try {
|
||||
setCallbacksCall(accessToken, payload);
|
||||
} catch (error) {
|
||||
message.error("Failed to update alerts: " + error, 20);
|
||||
}
|
||||
|
||||
message.success("Email settings updated successfully");
|
||||
}
|
||||
|
||||
const handleSaveAlerts = () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
|
@ -369,7 +406,8 @@ const Settings: React.FC<SettingsPageProps> = ({
|
|||
<TabList variant="line" defaultValue="1">
|
||||
<Tab value="1">Logging Callbacks</Tab>
|
||||
<Tab value="2">Alerting Types</Tab>
|
||||
<Tab value="2">Alerting Settings</Tab>
|
||||
<Tab value="3">Alerting Settings</Tab>
|
||||
<Tab value="4">Email Alerts</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
@ -526,6 +564,142 @@ const Settings: React.FC<SettingsPageProps> = ({
|
|||
premiumUser={premiumUser}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<Title>Email Settings</Title>
|
||||
<Text>
|
||||
<a href="https://docs.litellm.ai/docs/proxy/email" target="_blank" style={{ color: "blue" }}> LiteLLM Docs: email alerts</a> <br/>
|
||||
</Text>
|
||||
<div className="flex w-full">
|
||||
{alerts
|
||||
.filter((alert) => alert.name === "email")
|
||||
.map((alert, index) => (
|
||||
<TableCell key={index}>
|
||||
|
||||
<ul>
|
||||
<Grid numItems={2}>
|
||||
{Object.entries(alert.variables ?? {}).map(([key, value]) => (
|
||||
<li key={key} className="mx-2 my-2">
|
||||
|
||||
{ premiumUser!= true && (key === "EMAIL_LOGO_URL" || key === "EMAIL_SUPPORT_CONTACT") ? (
|
||||
<div>
|
||||
<a
|
||||
href="https://forms.gle/W3U4PZpJGFHWtHyA9"
|
||||
target="_blank"
|
||||
>
|
||||
<Text className="mt-2">
|
||||
{" "}
|
||||
✨ {key}
|
||||
|
||||
</Text>
|
||||
|
||||
</a>
|
||||
<TextInput
|
||||
name={key}
|
||||
defaultValue={value as string}
|
||||
type="password"
|
||||
disabled={true}
|
||||
style={{ width: "400px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<div>
|
||||
<Text className="mt-2">{key}</Text>
|
||||
<TextInput
|
||||
name={key}
|
||||
defaultValue={value as string}
|
||||
type="password"
|
||||
style={{ width: "400px" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{/* Added descriptions for input fields */}
|
||||
<p style={{ fontSize: "small", fontStyle: "italic" }}>
|
||||
{key === "SMTP_HOST" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP host address, e.g. `smtp.resend.com`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{key === "SMTP_PORT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP port number, e.g. `587`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{key === "SMTP_USERNAME" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP username, e.g. `username`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
{key === "SMTP_PASSWORD" && (
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
)}
|
||||
|
||||
{key === "SMTP_SENDER_EMAIL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the sender email address, e.g. `sender@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{key === "TEST_EMAIL_ADDRESS" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Email Address to send `Test Email Alert` to. example: `info@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{key === "EMAIL_LOGO_URL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the Logo that appears in the email, pass a url to your logo
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{key === "EMAIL_SUPPORT_CONTACT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the support email address that appears in the email. Default is support@berri.ai
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</Grid>
|
||||
</ul>
|
||||
</TableCell>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="mt-2"
|
||||
onClick={() => handleSaveEmailSettings()}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
serviceHealthCheck(accessToken, "email")
|
||||
}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Email Alerts
|
||||
</Button>
|
||||
|
||||
</Card>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</Grid>
|
||||
|
|
|
@ -162,6 +162,17 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
console.log("keys in usage", keys);
|
||||
console.log("premium user in usage", premiumUser);
|
||||
|
||||
function valueFormatterNumbers(number: number) {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
maximumFractionDigits: 0,
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
});
|
||||
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
|
||||
const updateEndUserData = async (startTime: Date | undefined, endTime: Date | undefined, uiSelectedKey: string | null) => {
|
||||
if (!startTime || !endTime || !accessToken) {
|
||||
return;
|
||||
|
@ -482,10 +493,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
<Title>All Up</Title>
|
||||
<Grid numItems={2}>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {globalActivity.sum_api_requests}</Subtitle>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests { valueFormatterNumbers(globalActivity.sum_api_requests)}</Subtitle>
|
||||
<AreaChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
index="date"
|
||||
colors={['cyan']}
|
||||
categories={['api_requests']}
|
||||
|
@ -494,10 +506,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
|
||||
</Col>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {globalActivity.sum_total_tokens}</Subtitle>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens { valueFormatterNumbers(globalActivity.sum_total_tokens)}</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
index="date"
|
||||
colors={['cyan']}
|
||||
categories={['total_tokens']}
|
||||
|
@ -517,24 +530,26 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
<Title>{globalActivity.model}</Title>
|
||||
<Grid numItems={2}>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {globalActivity.sum_api_requests}</Subtitle>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)}</Subtitle>
|
||||
<AreaChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
index="date"
|
||||
colors={['cyan']}
|
||||
categories={['api_requests']}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {globalActivity.sum_total_tokens}</Subtitle>
|
||||
<Subtitle style={{ fontSize: "15px", fontWeight: "normal", color: "#535452"}}>Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)}</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
index="date"
|
||||
colors={['cyan']}
|
||||
categories={['total_tokens']}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -565,7 +580,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
color: "#535452",
|
||||
}}
|
||||
>
|
||||
API Requests {globalActivity.sum_api_requests}
|
||||
API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)}
|
||||
</Subtitle>
|
||||
<AreaChart
|
||||
className="h-40"
|
||||
|
@ -573,6 +588,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
index="date"
|
||||
colors={['cyan']}
|
||||
categories={['api_requests']}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
</Col>
|
||||
|
@ -584,13 +600,14 @@ const UsagePage: React.FC<UsagePageProps> = ({
|
|||
color: "#535452",
|
||||
}}
|
||||
>
|
||||
Tokens {globalActivity.sum_total_tokens}
|
||||
Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)}
|
||||
</Subtitle>
|
||||
<BarChart
|
||||
className="h-40"
|
||||
data={globalActivity.daily_data}
|
||||
index="date"
|
||||
colors={['cyan']}
|
||||
valueFormatter={valueFormatterNumbers}
|
||||
categories={['total_tokens']}
|
||||
onValueChange={(v) => console.log(v)}
|
||||
/>
|
||||
|
|
|
@ -24,12 +24,22 @@ import {
|
|||
Icon,
|
||||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import { userInfoCall } from "./networking";
|
||||
|
||||
import {
|
||||
message,
|
||||
} from "antd";
|
||||
|
||||
import { userInfoCall, userUpdateUserCall, getPossibleUserRoles } from "./networking";
|
||||
import { Badge, BadgeDelta, Button } from "@tremor/react";
|
||||
import RequestAccess from "./request_model_access";
|
||||
import CreateUser from "./create_user_button";
|
||||
import EditUserModal from "./edit_user";
|
||||
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||
import InformationCircleIcon from "@heroicons/react/outline/InformationCircleIcon";
|
||||
import {
|
||||
PencilAltIcon,
|
||||
InformationCircleIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
|
||||
interface ViewUserDashboardProps {
|
||||
accessToken: string | null;
|
||||
|
@ -55,8 +65,40 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
const [currentPage, setCurrentPage] = useState(0);
|
||||
const [openDialogId, setOpenDialogId] = React.useState<null | number>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<null | any>(null);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<Record<string, Record<string, string>>>({});
|
||||
const defaultPageSize = 25;
|
||||
|
||||
const handleEditCancel = async () => {
|
||||
setSelectedUser(null);
|
||||
setEditModalVisible(false);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (editedUser: any) => {
|
||||
console.log("inside handleEditSubmit:", editedUser);
|
||||
|
||||
if (!accessToken || !token || !userRole || !userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await userUpdateUserCall(accessToken, editedUser, null);
|
||||
message.success(`User ${editedUser.user_id} updated successfully`);
|
||||
} catch (error) {
|
||||
console.error("There was an error updating the user", error);
|
||||
}
|
||||
if (userData) {
|
||||
const updatedUserData = userData.map((user) =>
|
||||
user.user_id === editedUser.user_id ? editedUser : user
|
||||
);
|
||||
setUserData(updatedUserData);
|
||||
}
|
||||
setSelectedUser(null);
|
||||
setEditModalVisible(false);
|
||||
// Close the modal
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || !token || !userRole || !userID) {
|
||||
return;
|
||||
|
@ -74,11 +116,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
);
|
||||
console.log("user data response:", userDataResponse);
|
||||
setUserData(userDataResponse);
|
||||
|
||||
const availableUserRoles = await getPossibleUserRoles(accessToken);
|
||||
setPossibleUIRoles(availableUserRoles);
|
||||
|
||||
} catch (error) {
|
||||
console.error("There was an error fetching the model data", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (accessToken && token && userRole && userID) {
|
||||
fetchData();
|
||||
}
|
||||
|
@ -126,14 +173,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<Grid className="gap-2 p-2 h-[80vh] w-full mt-8">
|
||||
<Grid className="gap-2 p-2 h-[90vh] w-full mt-8">
|
||||
<CreateUser userID={userID} accessToken={accessToken} teams={teams} />
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[90vh] mb-4">
|
||||
<div className="mb-4 mt-1">
|
||||
<Text>
|
||||
These are Users on LiteLLM that created API Keys. Automatically
|
||||
tracked by LiteLLM
|
||||
</Text>
|
||||
</div>
|
||||
<TabGroup>
|
||||
<TabPanels>
|
||||
|
@ -143,25 +186,23 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
<TableRow>
|
||||
<TableHeaderCell>User ID</TableHeaderCell>
|
||||
<TableHeaderCell>User Email</TableHeaderCell>
|
||||
<TableHeaderCell>User Models</TableHeaderCell>
|
||||
<TableHeaderCell>Role</TableHeaderCell>
|
||||
<TableHeaderCell>User Spend ($ USD)</TableHeaderCell>
|
||||
<TableHeaderCell>User Max Budget ($ USD)</TableHeaderCell>
|
||||
<TableHeaderCell>User API Key Aliases</TableHeaderCell>
|
||||
<TableHeaderCell>API Keys</TableHeaderCell>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{userData.map((user: any) => (
|
||||
<TableRow key={user.user_id}>
|
||||
<TableCell>{user.user_id}</TableCell>
|
||||
<TableCell>{user.user_email}</TableCell>
|
||||
|
||||
<TableCell>{user.user_id || "-"}</TableCell>
|
||||
<TableCell>{user.user_email || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{user.models && user.models.length > 0
|
||||
? user.models
|
||||
: "All Models"}
|
||||
{possibleUIRoles?.[user?.user_role]?.ui_label || "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.spend ? user.spend?.toFixed(2) : 0}
|
||||
{user.spend ? user.spend?.toFixed(2) : "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.max_budget ? user.max_budget : "Unlimited"}
|
||||
|
@ -173,9 +214,13 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
(key: any) => key !== null
|
||||
).length > 0 ? (
|
||||
<Badge size={"xs"} color={"indigo"}>
|
||||
{user.key_aliases
|
||||
.filter((key: any) => key !== null)
|
||||
.join(", ")}
|
||||
{
|
||||
user.key_aliases.filter(
|
||||
(key: any) => key !== null
|
||||
).length
|
||||
|
||||
}
|
||||
Keys
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size={"xs"} color={"gray"}>
|
||||
|
@ -188,12 +233,23 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</Badge>
|
||||
)}
|
||||
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
|
||||
{/* <Icon icon={InformationCircleIcon} onClick= {() => {
|
||||
</Grid>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
<Icon icon={PencilAltIcon} onClick= {() => {
|
||||
setSelectedUser(user)
|
||||
setEditModalVisible(true)
|
||||
}}>View Keys</Icon>
|
||||
{/*
|
||||
<Icon icon={TrashIcon} onClick= {() => {
|
||||
setOpenDialogId(user.user_id)
|
||||
setSelectedItem(user)
|
||||
}}>View Keys</Icon> */}
|
||||
</Grid>
|
||||
|
||||
</TableCell>
|
||||
|
||||
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
@ -226,30 +282,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
<EditUserModal
|
||||
visible={editModalVisible}
|
||||
possibleUIRoles={possibleUIRoles}
|
||||
onCancel={handleEditCancel}
|
||||
user={selectedUser}
|
||||
onSubmit={handleEditSubmit}
|
||||
/>
|
||||
</Card>
|
||||
{renderPagination()}
|
||||
</Grid>
|
||||
{/* <Dialog
|
||||
open={openDialogId !== null}
|
||||
onClose={() => {
|
||||
setOpenDialogId(null);
|
||||
}}
|
||||
|
||||
>
|
||||
<DialogPanel>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Title>Key Aliases</Title>
|
||||
|
||||
<Text>
|
||||
{selectedItem && selectedItem.key_aliases
|
||||
? selectedItem.key_aliases.filter(key => key !== null).length > 0
|
||||
? selectedItem.key_aliases.filter(key => key !== null).join(', ')
|
||||
: 'No Keys'
|
||||
: "No Keys"}
|
||||
</Text>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</Dialog> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue