mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
578 lines
18 KiB
TypeScript
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;
|