forked from phoenix/litellm-mirror
Merge pull request #3874 from BerriAI/litellm_make_email_alerts_free
[Feat] Add, Test Email Alerts on Admin UI
This commit is contained in:
commit
2069e36d1d
3 changed files with 303 additions and 47 deletions
|
@ -41,6 +41,7 @@ class ProviderRegionOutageModel(BaseOutageModel):
|
||||||
|
|
||||||
# we use this for the email header, please send a test email if you change this. verify it looks good on email
|
# we use this for the email header, please send a test email if you change this. verify it looks good on email
|
||||||
LITELLM_LOGO_URL = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
|
LITELLM_LOGO_URL = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
|
||||||
|
LITELLM_SUPPORT_CONTACT = "support@berri.ai"
|
||||||
|
|
||||||
|
|
||||||
class LiteLLMBase(BaseModel):
|
class LiteLLMBase(BaseModel):
|
||||||
|
@ -1171,6 +1172,10 @@ Model Info:
|
||||||
await self._check_if_using_premium_email_feature(
|
await self._check_if_using_premium_email_feature(
|
||||||
premium_user, email_logo_url, email_support_contact
|
premium_user, email_logo_url, email_support_contact
|
||||||
)
|
)
|
||||||
|
if email_logo_url is None:
|
||||||
|
email_logo_url = LITELLM_LOGO_URL
|
||||||
|
if email_support_contact is None:
|
||||||
|
email_support_contact = LITELLM_SUPPORT_CONTACT
|
||||||
|
|
||||||
event_name = webhook_event.event_message
|
event_name = webhook_event.event_message
|
||||||
recipient_email = webhook_event.user_email
|
recipient_email = webhook_event.user_email
|
||||||
|
@ -1271,6 +1276,11 @@ Model Info:
|
||||||
premium_user, email_logo_url, email_support_contact
|
premium_user, email_logo_url, email_support_contact
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if email_logo_url is None:
|
||||||
|
email_logo_url = LITELLM_LOGO_URL
|
||||||
|
if email_support_contact is None:
|
||||||
|
email_support_contact = LITELLM_SUPPORT_CONTACT
|
||||||
|
|
||||||
event_name = webhook_event.event_message
|
event_name = webhook_event.event_message
|
||||||
recipient_email = webhook_event.user_email
|
recipient_email = webhook_event.user_email
|
||||||
user_name = webhook_event.user_id
|
user_name = webhook_event.user_id
|
||||||
|
|
|
@ -2930,48 +2930,54 @@ class ProxyConfig:
|
||||||
global llm_router, llm_model_list, master_key, general_settings
|
global llm_router, llm_model_list, master_key, general_settings
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
if llm_router is None and master_key is not None:
|
try:
|
||||||
verbose_proxy_logger.debug(f"len new_models: {len(new_models)}")
|
if llm_router is None and master_key is not None:
|
||||||
|
verbose_proxy_logger.debug(f"len new_models: {len(new_models)}")
|
||||||
|
|
||||||
_model_list: list = []
|
_model_list: list = []
|
||||||
for m in new_models:
|
for m in new_models:
|
||||||
_litellm_params = m.litellm_params
|
_litellm_params = m.litellm_params
|
||||||
if isinstance(_litellm_params, dict):
|
if isinstance(_litellm_params, dict):
|
||||||
# decrypt values
|
# decrypt values
|
||||||
for k, v in _litellm_params.items():
|
for k, v in _litellm_params.items():
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
# decode base64
|
# decode base64
|
||||||
decoded_b64 = base64.b64decode(v)
|
decoded_b64 = base64.b64decode(v)
|
||||||
# decrypt value
|
# decrypt value
|
||||||
_litellm_params[k] = decrypt_value(
|
_litellm_params[k] = decrypt_value(
|
||||||
value=decoded_b64, master_key=master_key # type: ignore
|
value=decoded_b64, master_key=master_key # type: ignore
|
||||||
)
|
)
|
||||||
_litellm_params = LiteLLM_Params(**_litellm_params)
|
_litellm_params = LiteLLM_Params(**_litellm_params)
|
||||||
else:
|
else:
|
||||||
verbose_proxy_logger.error(
|
verbose_proxy_logger.error(
|
||||||
f"Invalid model added to proxy db. Invalid litellm params. litellm_params={_litellm_params}"
|
f"Invalid model added to proxy db. Invalid litellm params. litellm_params={_litellm_params}"
|
||||||
|
)
|
||||||
|
continue # skip to next model
|
||||||
|
|
||||||
|
_model_info = self.get_model_info_with_id(model=m)
|
||||||
|
_model_list.append(
|
||||||
|
Deployment(
|
||||||
|
model_name=m.model_name,
|
||||||
|
litellm_params=_litellm_params,
|
||||||
|
model_info=_model_info,
|
||||||
|
).to_json(exclude_none=True)
|
||||||
)
|
)
|
||||||
continue # skip to next model
|
if len(_model_list) > 0:
|
||||||
|
verbose_proxy_logger.debug(f"_model_list: {_model_list}")
|
||||||
|
llm_router = litellm.Router(model_list=_model_list)
|
||||||
|
verbose_proxy_logger.debug(f"updated llm_router: {llm_router}")
|
||||||
|
else:
|
||||||
|
verbose_proxy_logger.debug(f"len new_models: {len(new_models)}")
|
||||||
|
## DELETE MODEL LOGIC
|
||||||
|
await self._delete_deployment(db_models=new_models)
|
||||||
|
|
||||||
_model_info = self.get_model_info_with_id(model=m)
|
## ADD MODEL LOGIC
|
||||||
_model_list.append(
|
self._add_deployment(db_models=new_models)
|
||||||
Deployment(
|
|
||||||
model_name=m.model_name,
|
|
||||||
litellm_params=_litellm_params,
|
|
||||||
model_info=_model_info,
|
|
||||||
).to_json(exclude_none=True)
|
|
||||||
)
|
|
||||||
if len(_model_list) > 0:
|
|
||||||
verbose_proxy_logger.debug(f"_model_list: {_model_list}")
|
|
||||||
llm_router = litellm.Router(model_list=_model_list)
|
|
||||||
verbose_proxy_logger.debug(f"updated llm_router: {llm_router}")
|
|
||||||
else:
|
|
||||||
verbose_proxy_logger.debug(f"len new_models: {len(new_models)}")
|
|
||||||
## DELETE MODEL LOGIC
|
|
||||||
await self._delete_deployment(db_models=new_models)
|
|
||||||
|
|
||||||
## ADD MODEL LOGIC
|
except Exception as e:
|
||||||
self._add_deployment(db_models=new_models)
|
verbose_proxy_logger.error(
|
||||||
|
f"Error adding/deleting model to llm_router: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
if llm_router is not None:
|
if llm_router is not None:
|
||||||
llm_model_list = llm_router.get_model_list()
|
llm_model_list = llm_router.get_model_list()
|
||||||
|
@ -3020,11 +3026,20 @@ class ProxyConfig:
|
||||||
## ALERTING ## [TODO] move this to the _update_general_settings() block
|
## ALERTING ## [TODO] move this to the _update_general_settings() block
|
||||||
_general_settings = config_data.get("general_settings", {})
|
_general_settings = config_data.get("general_settings", {})
|
||||||
if "alerting" in _general_settings:
|
if "alerting" in _general_settings:
|
||||||
general_settings["alerting"] = _general_settings["alerting"]
|
if (
|
||||||
proxy_logging_obj.alerting = general_settings["alerting"]
|
general_settings["alerting"] is not None
|
||||||
proxy_logging_obj.slack_alerting_instance.alerting = general_settings[
|
and isinstance(general_settings["alerting"], list)
|
||||||
"alerting"
|
and _general_settings["alerting"] is not None
|
||||||
]
|
and isinstance(_general_settings["alerting"], list)
|
||||||
|
):
|
||||||
|
for alert in _general_settings["alerting"]:
|
||||||
|
if alert not in general_settings["alerting"]:
|
||||||
|
general_settings["alerting"].append(alert)
|
||||||
|
|
||||||
|
proxy_logging_obj.alerting = general_settings["alerting"]
|
||||||
|
proxy_logging_obj.slack_alerting_instance.alerting = general_settings[
|
||||||
|
"alerting"
|
||||||
|
]
|
||||||
|
|
||||||
if "alert_types" in _general_settings:
|
if "alert_types" in _general_settings:
|
||||||
general_settings["alert_types"] = _general_settings["alert_types"]
|
general_settings["alert_types"] = _general_settings["alert_types"]
|
||||||
|
@ -11328,10 +11343,10 @@ async def update_config(config_info: ConfigYAML):
|
||||||
if k == "alert_to_webhook_url":
|
if k == "alert_to_webhook_url":
|
||||||
# check if slack is already enabled. if not, enable it
|
# check if slack is already enabled. if not, enable it
|
||||||
if "alerting" not in _existing_settings:
|
if "alerting" not in _existing_settings:
|
||||||
_existing_settings["alerting"] = ["slack"]
|
_existing_settings["alerting"].append("slack")
|
||||||
elif isinstance(_existing_settings["alerting"], list):
|
elif isinstance(_existing_settings["alerting"], list):
|
||||||
if "slack" not in _existing_settings["alerting"]:
|
if "slack" not in _existing_settings["alerting"]:
|
||||||
_existing_settings["alerting"] = ["slack"]
|
_existing_settings["alerting"].append("slack")
|
||||||
_existing_settings[k] = v
|
_existing_settings[k] = v
|
||||||
config["general_settings"] = _existing_settings
|
config["general_settings"] = _existing_settings
|
||||||
|
|
||||||
|
@ -11817,6 +11832,36 @@ async def get_config():
|
||||||
"alerts_to_webhook": _alerts_to_webhook,
|
"alerts_to_webhook": _alerts_to_webhook,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
# pass email alerting vars
|
||||||
|
_email_vars = [
|
||||||
|
"SMTP_HOST",
|
||||||
|
"SMTP_PORT",
|
||||||
|
"SMTP_USERNAME",
|
||||||
|
"SMTP_PASSWORD",
|
||||||
|
"SMTP_SENDER_EMAIL",
|
||||||
|
"TEST_EMAIL_ADDRESS",
|
||||||
|
"EMAIL_LOGO_URL",
|
||||||
|
"EMAIL_SUPPORT_CONTACT",
|
||||||
|
]
|
||||||
|
_email_env_vars = {}
|
||||||
|
for _var in _email_vars:
|
||||||
|
env_variable = environment_variables.get(_var, None)
|
||||||
|
if env_variable is None:
|
||||||
|
_email_env_vars[_var] = None
|
||||||
|
else:
|
||||||
|
# decode + decrypt the value
|
||||||
|
decoded_b64 = base64.b64decode(env_variable)
|
||||||
|
_decrypted_value = decrypt_value(
|
||||||
|
value=decoded_b64, master_key=master_key
|
||||||
|
)
|
||||||
|
_email_env_vars[_var] = _decrypted_value
|
||||||
|
|
||||||
|
alerting_data.append(
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"variables": _email_env_vars,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if llm_router is None:
|
if llm_router is None:
|
||||||
_router_settings = {}
|
_router_settings = {}
|
||||||
|
@ -11903,7 +11948,7 @@ async def test_endpoint(request: Request):
|
||||||
async def health_services_endpoint(
|
async def health_services_endpoint(
|
||||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
service: Literal[
|
service: Literal[
|
||||||
"slack_budget_alerts", "langfuse", "slack", "openmeter", "webhook"
|
"slack_budget_alerts", "langfuse", "slack", "openmeter", "webhook", "email"
|
||||||
] = fastapi.Query(description="Specify the service being hit."),
|
] = fastapi.Query(description="Specify the service being hit."),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -11920,6 +11965,7 @@ async def health_services_endpoint(
|
||||||
)
|
)
|
||||||
if service not in [
|
if service not in [
|
||||||
"slack_budget_alerts",
|
"slack_budget_alerts",
|
||||||
|
"email",
|
||||||
"langfuse",
|
"langfuse",
|
||||||
"slack",
|
"slack",
|
||||||
"openmeter",
|
"openmeter",
|
||||||
|
@ -12052,6 +12098,32 @@ async def health_services_endpoint(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if service == "email":
|
||||||
|
webhook_event = WebhookEvent(
|
||||||
|
event="key_created",
|
||||||
|
event_group="key",
|
||||||
|
event_message="Test Email Alert",
|
||||||
|
token=user_api_key_dict.token or "",
|
||||||
|
key_alias="Email Test key (This is only a test alert key. DO NOT USE THIS IN PRODUCTION.)",
|
||||||
|
spend=0,
|
||||||
|
max_budget=0,
|
||||||
|
user_id=user_api_key_dict.user_id,
|
||||||
|
user_email=os.getenv("TEST_EMAIL_ADDRESS"),
|
||||||
|
team_id=user_api_key_dict.team_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# use create task - this can take 10 seconds. don't keep ui users waiting for notification to check their email
|
||||||
|
asyncio.create_task(
|
||||||
|
proxy_logging_obj.slack_alerting_instance.send_key_created_email(
|
||||||
|
webhook_event=webhook_event
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"message": "Mock Email Alert sent, verify Email Alert Received",
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
|
|
|
@ -188,6 +188,43 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||||
console.log("Selected values:", values);
|
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 = () => {
|
const handleSaveAlerts = () => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return;
|
return;
|
||||||
|
@ -369,7 +406,8 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||||
<TabList variant="line" defaultValue="1">
|
<TabList variant="line" defaultValue="1">
|
||||||
<Tab value="1">Logging Callbacks</Tab>
|
<Tab value="1">Logging Callbacks</Tab>
|
||||||
<Tab value="2">Alerting Types</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>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
@ -526,6 +564,142 @@ const Settings: React.FC<SettingsPageProps> = ({
|
||||||
premiumUser={premiumUser}
|
premiumUser={premiumUser}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</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>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue