litellm-mirror/ui/litellm-dashboard/src/components/settings.tsx

578 lines
18 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
Card,
Title,
Subtitle,
Table,
TableHead,
TableRow,
Badge,
TableHeaderCell,
TableCell,
TableBody,
Metric,
Text,
Grid,
Button,
TextInput,
Switch,
Col,
TabPanel,
TabPanels,
TabGroup,
TabList,
Tab,
Callout,
} from "@tremor/react";
import {
getCallbacksCall,
setCallbacksCall,
serviceHealthCheck,
} from "./networking";
import { Modal, Form, Input, Select, Button as Button2, message } from "antd";
import StaticGenerationSearchParamsBailoutProvider from "next/dist/client/components/static-generation-searchparams-bailout-provider";
import AlertingSettings from "./alerting/alerting_settings";
interface SettingsPageProps {
accessToken: string | null;
userRole: string | null;
userID: string | null;
}
interface AlertingVariables {
SLACK_WEBHOOK_URL: string | null;
LANGFUSE_PUBLIC_KEY: string | null;
LANGFUSE_SECRET_KEY: string | null;
LANGFUSE_HOST: string | null;
OPENMETER_API_KEY: string | null;
}
interface AlertingObject {
name: string;
variables: AlertingVariables;
}
const defaultLoggingObject: AlertingObject[] = [
{
name: "slack",
variables: {
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: null,
LANGFUSE_SECRET_KEY: null,
OPENMETER_API_KEY: null,
SLACK_WEBHOOK_URL: null,
},
},
{
name: "langfuse",
variables: {
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: null,
LANGFUSE_SECRET_KEY: null,
OPENMETER_API_KEY: null,
SLACK_WEBHOOK_URL: null,
},
},
{
name: "openmeter",
variables: {
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: null,
LANGFUSE_SECRET_KEY: null,
OPENMETER_API_KEY: null,
SLACK_WEBHOOK_URL: null,
},
},
];
const Settings: React.FC<SettingsPageProps> = ({
accessToken,
userRole,
userID,
}) => {
const [callbacks, setCallbacks] =
useState<AlertingObject[]>(defaultLoggingObject);
const [alerts, setAlerts] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [form] = Form.useForm();
const [selectedCallback, setSelectedCallback] = useState<string | null>(null);
const [selectedAlertValues, setSelectedAlertValues] = useState([]);
const [catchAllWebhookURL, setCatchAllWebhookURL] = useState<string>("");
const [alertToWebhooks, setAlertToWebhooks] = useState<
Record<string, string>
>({});
const [activeAlerts, setActiveAlerts] = useState<string[]>([]);
const handleSwitchChange = (alertName: string) => {
if (activeAlerts.includes(alertName)) {
setActiveAlerts(activeAlerts.filter((alert) => alert !== alertName));
} else {
setActiveAlerts([...activeAlerts, alertName]);
}
};
const alerts_to_UI_NAME: Record<string, string> = {
llm_exceptions: "LLM Exceptions",
llm_too_slow: "LLM Responses Too Slow",
llm_requests_hanging: "LLM Requests Hanging",
budget_alerts: "Budget Alerts (API Keys, Users)",
db_exceptions: "Database Exceptions (Read/Write)",
daily_reports: "Weekly/Monthly Spend Reports",
outage_alerts: "Outage Alerts",
region_outage_alerts: "Region Outage Alerts",
};
useEffect(() => {
if (!accessToken || !userRole || !userID) {
return;
}
getCallbacksCall(accessToken, userID, userRole).then((data) => {
console.log("callbacks", data);
let updatedCallbacks: any[] = defaultLoggingObject;
updatedCallbacks = updatedCallbacks.map((item: any) => {
const callback = data.callbacks.find(
(cb: any) => cb.name === item.name
);
if (callback) {
return {
...item,
variables: { ...item.variables, ...callback.variables },
};
} else {
return item;
}
});
setCallbacks(updatedCallbacks);
// setCallbacks(callbacks_data);
let alerts_data = data.alerts;
console.log("alerts_data", alerts_data);
if (alerts_data) {
if (alerts_data.length > 0) {
let _alert_info = alerts_data[0];
console.log("_alert_info", _alert_info);
let catch_all_webhook = _alert_info.variables.SLACK_WEBHOOK_URL;
console.log("catch_all_webhook", catch_all_webhook);
let active_alerts = _alert_info.active_alerts;
setActiveAlerts(active_alerts);
setCatchAllWebhookURL(catch_all_webhook);
setAlertToWebhooks(_alert_info.alerts_to_webhook);
}
}
setAlerts(alerts_data);
});
}, [accessToken, userRole, userID]);
const isAlertOn = (alertName: string) => {
return activeAlerts && activeAlerts.includes(alertName);
};
const handleAddCallback = () => {
console.log("Add callback clicked");
setIsModalVisible(true);
};
const handleCancel = () => {
setIsModalVisible(false);
form.resetFields();
setSelectedCallback(null);
};
const handleChange = (values: any) => {
setSelectedAlertValues(values);
// Here, you can perform any additional logic with the selected values
console.log("Selected values:", values);
};
const handleSaveAlerts = () => {
if (!accessToken) {
return;
}
const updatedAlertToWebhooks: Record<string, string> = {};
Object.entries(alerts_to_UI_NAME).forEach(([key, value]) => {
const webhookInput = document.querySelector(
`input[name="${key}"]`
) as HTMLInputElement;
console.log("key", key);
console.log("webhookInput", webhookInput);
const newWebhookValue = webhookInput?.value || "";
console.log("newWebhookValue", newWebhookValue);
updatedAlertToWebhooks[key] = newWebhookValue;
});
console.log("updatedAlertToWebhooks", updatedAlertToWebhooks);
const payload = {
general_settings: {
alert_to_webhook_url: updatedAlertToWebhooks,
alert_types: activeAlerts,
},
};
console.log("payload", payload);
try {
setCallbacksCall(accessToken, payload);
} catch (error) {
message.error("Failed to update alerts: " + error, 20);
}
message.success("Alerts updated successfully");
};
const handleSaveChanges = (callback: any) => {
if (!accessToken) {
return;
}
const updatedVariables = Object.fromEntries(
Object.entries(callback.variables).map(([key, value]) => [
key,
(document.querySelector(`input[name="${key}"]`) as HTMLInputElement)
?.value || value,
])
);
console.log("updatedVariables", updatedVariables);
console.log("updateAlertTypes", selectedAlertValues);
const payload = {
environment_variables: updatedVariables,
litellm_settings: {
success_callback: [callback.name],
},
};
try {
setCallbacksCall(accessToken, payload);
} catch (error) {
message.error("Failed to update callback: " + error, 20);
}
message.success("Callback updated successfully");
};
const handleOk = () => {
if (!accessToken) {
return;
}
// Handle form submission
form.validateFields().then((values) => {
// Call API to add the callback
console.log("Form values:", values);
let payload;
if (values.callback === "langfuse") {
payload = {
environment_variables: {
LANGFUSE_PUBLIC_KEY: values.langfusePublicKey,
LANGFUSE_SECRET_KEY: values.langfusePrivateKey,
},
litellm_settings: {
success_callback: [values.callback],
},
};
setCallbacksCall(accessToken, payload);
let newCallback: AlertingObject = {
name: values.callback,
variables: {
SLACK_WEBHOOK_URL: null,
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: values.langfusePublicKey,
LANGFUSE_SECRET_KEY: values.langfusePrivateKey,
OPENMETER_API_KEY: null,
},
};
// add langfuse to callbacks
setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
} else if (values.callback === "slack") {
console.log(`values.slackWebhookUrl: ${values.slackWebhookUrl}`);
payload = {
general_settings: {
alerting: ["slack"],
alerting_threshold: 300,
},
environment_variables: {
SLACK_WEBHOOK_URL: values.slackWebhookUrl,
},
};
setCallbacksCall(accessToken, payload);
// add slack to callbacks
console.log(`values.callback: ${values.callback}`);
let newCallback: AlertingObject = {
name: values.callback,
variables: {
SLACK_WEBHOOK_URL: values.slackWebhookUrl,
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: null,
LANGFUSE_SECRET_KEY: null,
OPENMETER_API_KEY: null,
},
};
setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
} else if (values.callback == "openmeter") {
console.log(`values.openMeterApiKey: ${values.openMeterApiKey}`);
payload = {
environment_variables: {
OPENMETER_API_KEY: values.openMeterApiKey,
},
litellm_settings: {
success_callback: [values.callback],
},
};
setCallbacksCall(accessToken, payload);
let newCallback: AlertingObject = {
name: values.callback,
variables: {
SLACK_WEBHOOK_URL: null,
LANGFUSE_HOST: null,
LANGFUSE_PUBLIC_KEY: null,
LANGFUSE_SECRET_KEY: null,
OPENMETER_API_KEY: values.openMeterAPIKey,
},
};
// add langfuse to callbacks
setCallbacks(callbacks ? [...callbacks, newCallback] : [newCallback]);
} else {
payload = {
error: "Invalid callback value",
};
}
setIsModalVisible(false);
form.resetFields();
setSelectedCallback(null);
});
};
const handleCallbackChange = (value: string) => {
setSelectedCallback(value);
};
if (!accessToken) {
return null;
}
console.log(`callbacks: ${callbacks}`);
return (
<div className="w-full mx-4">
<Grid numItems={1} className="gap-2 p-8 w-full mt-2">
<Callout
title="[UI] Presidio PII + Guardrails Coming Soon. https://docs.litellm.ai/docs/proxy/pii_masking"
color="sky"
></Callout>
<TabGroup>
<TabList variant="line" defaultValue="1">
<Tab value="1">Logging Callbacks</Tab>
<Tab value="2">Alerting Types</Tab>
<Tab value="2">Alerting Settings</Tab>
</TabList>
<TabPanels>
<TabPanel>
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Callback</TableHeaderCell>
<TableHeaderCell>Callback Env Vars</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{callbacks
.filter((callback) => callback.name !== "slack")
.map((callback, index) => (
<TableRow key={index}>
<TableCell>
<Badge color="emerald">{callback.name}</Badge>
</TableCell>
<TableCell>
<ul>
{Object.entries(callback.variables ?? {})
.filter(([key, value]) =>
key.toLowerCase().includes(callback.name)
)
.map(([key, value]) => (
<li key={key}>
<Text className="mt-2">{key}</Text>
{key === "LANGFUSE_HOST" ? (
<p>
default value=https://cloud.langfuse.com
</p>
) : (
<div></div>
)}
<TextInput
name={key}
defaultValue={value as string}
type="password"
/>
</li>
))}
</ul>
<Button
className="mt-2"
onClick={() => handleSaveChanges(callback)}
>
Save Changes
</Button>
<Button
onClick={() =>
serviceHealthCheck(accessToken, callback.name)
}
className="mx-2"
>
Test Callback
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</TabPanel>
<TabPanel>
<Card>
<Text className="my-2">
Alerts are only supported for Slack Webhook URLs. Get your
webhook urls from{" "}
<a
href="https://api.slack.com/messaging/webhooks"
target="_blank"
style={{ color: "blue" }}
>
here
</a>
</Text>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
<TableHeaderCell>Slack Webhook URL</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(alerts_to_UI_NAME).map(
([key, value], index) => (
<TableRow key={index}>
<TableCell>
<Switch
id="switch"
name="switch"
checked={isAlertOn(key)}
onChange={() => handleSwitchChange(key)}
/>
</TableCell>
<TableCell>
<Text>{value}</Text>
</TableCell>
<TableCell>
<TextInput
name={key}
type="password"
defaultValue={
alertToWebhooks && alertToWebhooks[key]
? alertToWebhooks[key]
: (catchAllWebhookURL as string)
}
></TextInput>
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
<Button size="xs" className="mt-2" onClick={handleSaveAlerts}>
Save Changes
</Button>
<Button
onClick={() => serviceHealthCheck(accessToken, "slack")}
className="mx-2"
>
Test Alerts
</Button>
</Card>
</TabPanel>
<TabPanel>
<AlertingSettings accessToken={accessToken} />
</TabPanel>
</TabPanels>
</TabGroup>
</Grid>
<Modal
title="Add Callback"
visible={isModalVisible}
onOk={handleOk}
width={800}
onCancel={handleCancel}
footer={null}
>
<Form form={form} layout="vertical" onFinish={handleOk}>
<Form.Item
label="Callback"
name="callback"
rules={[{ required: true, message: "Please select a callback" }]}
>
<Select onChange={handleCallbackChange}>
<Select.Option value="langfuse">langfuse</Select.Option>
<Select.Option value="openmeter">openmeter</Select.Option>
</Select>
</Form.Item>
{selectedCallback === "langfuse" && (
<>
<Form.Item
label="LANGFUSE_PUBLIC_KEY"
name="langfusePublicKey"
rules={[
{ required: true, message: "Please enter the public key" },
]}
>
<TextInput type="password" />
</Form.Item>
<Form.Item
label="LANGFUSE_PRIVATE_KEY"
name="langfusePrivateKey"
rules={[
{ required: true, message: "Please enter the private key" },
]}
>
<TextInput type="password" />
</Form.Item>
</>
)}
{selectedCallback == "openmeter" && (
<>
<Form.Item
label="OPENMETER_API_KEY"
name="openMeterApiKey"
rules={[
{
required: true,
message: "Please enter the openmeter api key",
},
]}
>
<TextInput type="password" />
</Form.Item>
</>
)}
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 htmlType="submit">Save</Button2>
</div>
</Form>
</Modal>
</div>
);
};
export default Settings;