Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/hooks/user_management_event_hooks.py: 68%

60 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2Hooks that are triggered when a litellm user event occurs 

3""" 

4 

5import asyncio 

6from datetime import datetime, timezone 

7from typing import Final 

8 

9import litellm 

10from litellm._logging import verbose_proxy_logger 

11from litellm._uuid import uuid 

12from litellm.proxy._types import ( 

13 AUDIT_ACTIONS, 

14 CommonProxyErrors, 

15 LiteLLM_AuditLogs, 

16 Litellm_EntityType, 

17 LitellmTableNames, 

18 NewUserRequest, 

19 NewUserResponse, 

20 UserAPIKeyAuth, 

21 WebhookEvent, 

22) 

23from litellm.proxy.management_helpers.audit_logs import ( 

24 create_audit_log_for_update, 

25 is_audit_logging_enabled, 

26) 

27from litellm.repositories.user_repository import UserRepository 

28 

29 

30class UserManagementEventHooks: 

31 @staticmethod 

32 async def async_user_created_hook( 

33 data: NewUserRequest, 

34 response: NewUserResponse, 

35 user_api_key_dict: UserAPIKeyAuth, 

36 ): 

37 """ 

38 This hook is called when a new user is created on litellm 

39 

40 Handles: 

41 - Creating an audit log for the user creation 

42 - Sending a user invitation email to the user 

43 """ 

44 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client 

45 

46 ######################################################### 

47 ########## Send User Invitation Email ################ 

48 ######################################################### 

49 await UserManagementEventHooks.async_send_user_invitation_email( 

50 data=data, 

51 response=response, 

52 user_api_key_dict=user_api_key_dict, 

53 ) 

54 

55 ######################################################### 

56 ########## CREATE AUDIT LOG ################ 

57 ######################################################### 

58 try: 

59 if prisma_client is None: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true

60 raise Exception(CommonProxyErrors.db_not_connected_error.value) 

61 if response.user_id is None: 61 ↛ 62line 61 didn't jump to line 62 because the condition on line 61 was never true

62 raise Exception("no user_id returned for the newly created user") 

63 user_row: Final = await UserRepository(prisma_client).find_by_id(response.user_id) 

64 if user_row is None: 64 ↛ 65line 64 didn't jump to line 65 because the condition on line 64 was never true

65 raise Exception(f"no user row found for user_id={response.user_id}") 

66 asyncio.create_task( 

67 UserManagementEventHooks.create_internal_user_audit_log( 

68 user_id=user_row.user_id, 

69 action="created", 

70 litellm_changed_by=user_api_key_dict.user_id, 

71 user_api_key_dict=user_api_key_dict, 

72 litellm_proxy_admin_name=litellm_proxy_admin_name, 

73 before_value=None, 

74 after_value=user_row.model_dump_json(exclude_none=True), 

75 ) 

76 ) 

77 except Exception as e: 

78 verbose_proxy_logger.warning("Unable to create audit log for user on `/user/new` - %s", e) 

79 

80 @staticmethod 

81 async def async_send_user_invitation_email( 

82 data: NewUserRequest, 

83 response: NewUserResponse, 

84 user_api_key_dict: UserAPIKeyAuth, 

85 ): 

86 """ 

87 Send a user invitation email to the user 

88 """ 

89 event: Final = WebhookEvent( 

90 event="internal_user_created", 

91 event_group=Litellm_EntityType.USER, 

92 event_message="Welcome to LiteLLM Proxy", 

93 token=response.token, 

94 spend=response.spend or 0.0, 

95 max_budget=response.max_budget, 

96 user_id=response.user_id, 

97 user_email=response.user_email, 

98 team_id=response.team_id, 

99 key_alias=response.key_alias, 

100 ) 

101 

102 sent_via_v2: Final = await UserManagementEventHooks._send_v2_user_invitation_emails( 

103 event=event, send_invite_email=data.send_invite_email 

104 ) 

105 

106 ######################################################### 

107 ########## LEGACY V1 USER INVITATION EMAIL (FALLBACK) #### 

108 ######################################################### 

109 if data.send_invite_email is True and not sent_via_v2: 

110 await UserManagementEventHooks.send_legacy_v1_user_invitation_email( 

111 data=data, 

112 response=response, 

113 user_api_key_dict=user_api_key_dict, 

114 event=event, 

115 ) 

116 

117 @staticmethod 

118 async def _send_v2_user_invitation_emails(event: WebhookEvent, send_invite_email: bool | None) -> bool: 

119 """ 

120 Send the modern (V2) invitation email via any registered enterprise email logger. 

121 

122 Returns True if at least one logger delivered, so the caller only falls back to 

123 the legacy email when V2 did not send (enterprise package absent, no email logger 

124 configured, or every send raised). 

125 """ 

126 if send_invite_email is not True: 

127 return False 

128 

129 try: 

130 from litellm_enterprise.enterprise_callbacks.send_emails.base_email import ( 

131 BaseEmailLogger, 

132 ) 

133 except ImportError: 

134 verbose_proxy_logger.warning( 

135 "Defaulting to using Legacy Email Hooks." + CommonProxyErrors.missing_enterprise_package.value 

136 ) 

137 return False 

138 

139 email_loggers: Final = tuple( 

140 email_logger 

141 for email_logger in litellm.logging_callback_manager.get_custom_loggers_for_type( 

142 callback_type=BaseEmailLogger 

143 ) 

144 if isinstance(email_logger, BaseEmailLogger) 

145 ) 

146 if len(email_loggers) == 0: 146 ↛ 149line 146 didn't jump to line 149 because the condition on line 146 was always true

147 return False 

148 

149 send_outcomes: Final = await asyncio.gather( 

150 *(email_logger.send_user_invitation_email(event=event) for email_logger in email_loggers), 

151 return_exceptions=True, 

152 ) 

153 for outcome in send_outcomes: 

154 if isinstance(outcome, BaseException): 

155 verbose_proxy_logger.error( 

156 "Error sending v2 user invitation email for user_id=%s: %s", 

157 event.user_id, 

158 str(outcome), 

159 ) 

160 

161 return any(not isinstance(outcome, BaseException) for outcome in send_outcomes) 

162 

163 @staticmethod 

164 async def send_legacy_v1_user_invitation_email( 

165 data: NewUserRequest, 

166 response: NewUserResponse, 

167 user_api_key_dict: UserAPIKeyAuth, 

168 event: WebhookEvent, 

169 ): 

170 """ 

171 Send a user invitation email to the user 

172 """ 

173 from litellm.proxy.proxy_server import general_settings, proxy_logging_obj 

174 

175 # check if user has setup email alerting 

176 if "email" not in general_settings.get("alerting", []): 176 ↛ 182line 176 didn't jump to line 182 because the condition on line 176 was always true

177 raise ValueError( 

178 "Email alerting not setup on config.yaml. Please set `alerting=['email']. \nDocs: https://docs.litellm.ai/docs/proxy/email`" 

179 ) 

180 

181 # If user configured email alerting - send an Email letting their end-user know the key was created 

182 asyncio.create_task( 

183 proxy_logging_obj.slack_alerting_instance.send_key_created_or_user_invited_email( 

184 webhook_event=event, 

185 ) 

186 ) 

187 

188 @staticmethod 

189 async def create_internal_user_audit_log( 

190 user_id: str, 

191 action: AUDIT_ACTIONS, 

192 litellm_changed_by: str | None, 

193 user_api_key_dict: UserAPIKeyAuth, 

194 litellm_proxy_admin_name: str | None, 

195 before_value: str | None = None, 

196 after_value: str | None = None, 

197 ): 

198 """ 

199 Create an audit log for an internal user. 

200 

201 Parameters: 

202 - user_id: str - The id of the user to create the audit log for. 

203 - action: AUDIT_ACTIONS - The action to create the audit log for. 

204 - user_row: LiteLLM_UserTable - The user row to create the audit log for. 

205 - litellm_changed_by: Optional[str] - The user id of the user who is changing the user. 

206 - user_api_key_dict: UserAPIKeyAuth - The user api key dictionary. 

207 - litellm_proxy_admin_name: Optional[str] - The name of the proxy admin. 

208 """ 

209 if not is_audit_logging_enabled(): 209 ↛ 212line 209 didn't jump to line 212 because the condition on line 209 was always true

210 return 

211 

212 from litellm.proxy.management_helpers.audit_logs import ( 

213 get_audit_log_changed_by, 

214 ) 

215 

216 await create_audit_log_for_update( 

217 request_data=LiteLLM_AuditLogs( 

218 id=str(uuid.uuid4()), 

219 updated_at=datetime.now(timezone.utc), 

220 changed_by=get_audit_log_changed_by( 

221 litellm_changed_by=litellm_changed_by, 

222 user_api_key_dict=user_api_key_dict, 

223 litellm_proxy_admin_name=litellm_proxy_admin_name, 

224 ), 

225 changed_by_api_key=user_api_key_dict.api_key, 

226 table_name=LitellmTableNames.USER_TABLE_NAME, 

227 object_id=user_id, 

228 action=action, 

229 updated_values=after_value, 

230 before_value=before_value, 

231 ) 

232 )