Skip to content

Commit cfc07f3

Browse files
authored
[Bug Fix] Invite links email should contain the correct invite id (#12130)
* use common helper create_invitation_for_user * use common util in proxy * fix create_invitation_for_user * refactor base email * test_get_invitation_link_creates_new_when_none_exist * fix code QA checks
1 parent edc84b6 commit cfc07f3

File tree

6 files changed

+280
-63
lines changed

6 files changed

+280
-63
lines changed

enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from litellm.integrations.email_templates.user_invitation_email import (
2323
USER_INVITATION_EMAIL_TEMPLATE,
2424
)
25-
from litellm.proxy._types import WebhookEvent
25+
from litellm.proxy._types import InvitationNew, UserAPIKeyAuth, WebhookEvent
2626
from litellm.types.integrations.slack_alerting import LITELLM_LOGO_URL
2727

2828

@@ -166,39 +166,81 @@ async def _get_invitation_link(self, user_id: Optional[str], base_url: str) -> s
166166
"""
167167
Get invitation link for the user
168168
"""
169-
import asyncio
169+
# Early validation
170+
if not user_id:
171+
verbose_proxy_logger.debug("No user_id provided for invitation link")
172+
return base_url
173+
174+
if not await self._is_prisma_client_available():
175+
return base_url
176+
177+
# Wait for any concurrent invitation creation to complete
178+
await self._wait_for_invitation_creation()
179+
180+
# Get or create invitation
181+
invitation = await self._get_or_create_invitation(user_id)
182+
if not invitation:
183+
verbose_proxy_logger.warning(f"Failed to get/create invitation for user_id: {user_id}")
184+
return base_url
185+
186+
return self._construct_invitation_link(invitation.id, base_url)
170187

188+
async def _is_prisma_client_available(self) -> bool:
189+
"""Check if Prisma client is available"""
171190
from litellm.proxy.proxy_server import prisma_client
191+
192+
if prisma_client is None:
193+
verbose_proxy_logger.debug("Prisma client not found. Unable to lookup invitation")
194+
return False
195+
return True
172196

173-
################################################################################
174-
########## Sleep for 10 seconds to wait for the invitation link to be created ###
175-
################################################################################
176-
# The UI, calls /invitation/new to generate the invitation link
177-
# We wait 10 seconds to ensure the link is created
178-
################################################################################
197+
async def _wait_for_invitation_creation(self) -> None:
198+
"""
199+
Wait for any concurrent invitation creation to complete.
200+
201+
The UI calls /invitation/new to generate the invitation link.
202+
We wait to ensure any pending invitation creation is completed.
203+
"""
204+
import asyncio
179205
await asyncio.sleep(10)
180206

207+
async def _get_or_create_invitation(self, user_id: str):
208+
"""
209+
Get existing invitation or create a new one for the user
210+
211+
Returns:
212+
Invitation object with id attribute, or None if failed
213+
"""
214+
from litellm.proxy.management_helpers.user_invitation import (
215+
create_invitation_for_user,
216+
)
217+
from litellm.proxy.proxy_server import prisma_client
218+
181219
if prisma_client is None:
182-
verbose_proxy_logger.debug(
183-
f"Prisma client not found. Unable to lookup user email for user_id: {user_id}"
220+
verbose_proxy_logger.error("Prisma client is None in _get_or_create_invitation")
221+
return None
222+
223+
try:
224+
# Try to get existing invitation
225+
existing_invitations = await prisma_client.db.litellm_invitationlink.find_many(
226+
where={"user_id": user_id},
227+
order={"created_at": "desc"},
184228
)
185-
return base_url
186-
187-
if user_id is None:
188-
return base_url
189-
190-
# get the latest invitation link for the user
191-
invitation_rows = await prisma_client.db.litellm_invitationlink.find_many(
192-
where={"user_id": user_id},
193-
order={"created_at": "desc"},
194-
)
195-
if len(invitation_rows) > 0:
196-
invitation_row = invitation_rows[0]
197-
return self._construct_invitation_link(
198-
invitation_id=invitation_row.id, base_url=base_url
229+
230+
if existing_invitations and len(existing_invitations) > 0:
231+
verbose_proxy_logger.debug(f"Found existing invitation for user_id: {user_id}")
232+
return existing_invitations[0]
233+
234+
# Create new invitation if none exists
235+
verbose_proxy_logger.debug(f"Creating new invitation for user_id: {user_id}")
236+
return await create_invitation_for_user(
237+
data=InvitationNew(user_id=user_id),
238+
user_api_key_dict=UserAPIKeyAuth(user_id=user_id),
199239
)
200-
201-
return base_url
240+
241+
except Exception as e:
242+
verbose_proxy_logger.error(f"Error getting/creating invitation for user_id {user_id}: {e}")
243+
return None
202244

203245
def _construct_invitation_link(self, invitation_id: str, base_url: str) -> str:
204246
"""

litellm/litellm_core_utils/litellm_logging.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@
152152
from .specialty_caches.dynamic_logging_cache import DynamicLoggingCache
153153

154154
if TYPE_CHECKING:
155-
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
156155
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
157156
try:
158157
from litellm_enterprise.enterprise_callbacks.callback_controls import (

litellm/model_prices_and_context_window_backup.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
"search_context_size_medium": 0.0,
2424
"search_context_size_high": 0.0
2525
},
26+
"file_search_cost_per_1k_calls": 0.0,
27+
"file_search_cost_per_gb_per_day": 0.0,
28+
"vector_store_cost_per_gb_per_day": 0.0,
29+
"computer_use_input_cost_per_1k_tokens": 0.0,
30+
"computer_use_output_cost_per_1k_tokens": 0.0,
31+
"code_interpreter_cost_per_session": 0.0,
2632
"supported_regions": [
2733
"global",
2834
"us-west-2",
@@ -4575,6 +4581,37 @@
45754581
"supports_tool_choice": true,
45764582
"supports_prompt_caching": true
45774583
},
4584+
"deepseek/deepseek-r1": {
4585+
"max_tokens": 8192,
4586+
"max_input_tokens": 65536,
4587+
"max_output_tokens": 8192,
4588+
"input_cost_per_token": 5.5e-07,
4589+
"input_cost_per_token_cache_hit": 1.4e-07,
4590+
"output_cost_per_token": 2.19e-06,
4591+
"litellm_provider": "deepseek",
4592+
"mode": "chat",
4593+
"supports_function_calling": true,
4594+
"supports_assistant_prefill": true,
4595+
"supports_tool_choice": true,
4596+
"supports_reasoning": true,
4597+
"supports_prompt_caching": true
4598+
},
4599+
"deepseek/deepseek-v3": {
4600+
"max_tokens": 8192,
4601+
"max_input_tokens": 65536,
4602+
"max_output_tokens": 8192,
4603+
"input_cost_per_token": 2.7e-07,
4604+
"input_cost_per_token_cache_hit": 7e-08,
4605+
"cache_read_input_token_cost": 7e-08,
4606+
"cache_creation_input_token_cost": 0.0,
4607+
"output_cost_per_token": 1.1e-06,
4608+
"litellm_provider": "deepseek",
4609+
"mode": "chat",
4610+
"supports_function_calling": true,
4611+
"supports_assistant_prefill": true,
4612+
"supports_tool_choice": true,
4613+
"supports_prompt_caching": true
4614+
},
45784615
"codestral/codestral-latest": {
45794616
"max_tokens": 8191,
45804617
"max_input_tokens": 32000,
@@ -15735,4 +15772,4 @@
1573515772
"notes": "ElevenLabs Scribe v1 experimental - enhanced version of the main Scribe model"
1573615773
}
1573715774
}
15738-
}
15775+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from datetime import timedelta
2+
3+
from fastapi import HTTPException
4+
5+
import litellm
6+
from litellm.proxy._types import CommonProxyErrors, InvitationNew, UserAPIKeyAuth
7+
8+
9+
async def create_invitation_for_user(
10+
data: InvitationNew,
11+
user_api_key_dict: UserAPIKeyAuth,
12+
):
13+
"""
14+
Create an invitation for the user to onboard to LiteLLM Admin UI.
15+
"""
16+
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
17+
18+
if prisma_client is None:
19+
raise HTTPException(
20+
status_code=400,
21+
detail={"error": CommonProxyErrors.db_not_connected_error.value},
22+
)
23+
24+
current_time = litellm.utils.get_utc_datetime()
25+
expires_at = current_time + timedelta(days=7)
26+
27+
try:
28+
response = await prisma_client.db.litellm_invitationlink.create(
29+
data={
30+
"user_id": data.user_id,
31+
"created_at": current_time,
32+
"expires_at": expires_at,
33+
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
34+
"updated_at": current_time,
35+
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
36+
} # type: ignore
37+
)
38+
return response
39+
except Exception as e:
40+
if "Foreign key constraint failed on the field" in str(e):
41+
raise HTTPException(
42+
status_code=400,
43+
detail={
44+
"error": "User id does not exist in 'LiteLLM_UserTable'. Fix this by creating user via `/user/new`."
45+
},
46+
)
47+
raise HTTPException(status_code=500, detail={"error": str(e)})

litellm/proxy/proxy_server.py

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ def generate_feedback_box():
323323
get_custom_url,
324324
get_error_message_str,
325325
get_server_root_path,
326+
handle_exception_on_proxy,
326327
hash_token,
327328
update_spend,
328329
)
@@ -7510,49 +7511,37 @@ async def new_invitation(
75107511
}'
75117512
```
75127513
"""
7513-
global prisma_client
7514-
7515-
if prisma_client is None:
7516-
raise HTTPException(
7517-
status_code=400,
7518-
detail={"error": CommonProxyErrors.db_not_connected_error.value},
7519-
)
7520-
7521-
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
7522-
raise HTTPException(
7523-
status_code=400,
7524-
detail={
7525-
"error": "{}, your role={}".format(
7526-
CommonProxyErrors.not_allowed_access.value,
7527-
user_api_key_dict.user_role,
7528-
)
7529-
},
7514+
try:
7515+
from litellm.proxy.management_helpers.user_invitation import (
7516+
create_invitation_for_user,
75307517
)
7518+
global prisma_client
75317519

7532-
current_time = litellm.utils.get_utc_datetime()
7533-
expires_at = current_time + timedelta(days=7)
7520+
if prisma_client is None:
7521+
raise HTTPException(
7522+
status_code=400,
7523+
detail={"error": CommonProxyErrors.db_not_connected_error.value},
7524+
)
75347525

7535-
try:
7536-
response = await prisma_client.db.litellm_invitationlink.create(
7537-
data={
7538-
"user_id": data.user_id,
7539-
"created_at": current_time,
7540-
"expires_at": expires_at,
7541-
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
7542-
"updated_at": current_time,
7543-
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
7544-
} # type: ignore
7545-
)
7546-
return response
7547-
except Exception as e:
7548-
if "Foreign key constraint failed on the field" in str(e):
7526+
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
75497527
raise HTTPException(
75507528
status_code=400,
75517529
detail={
7552-
"error": "User id does not exist in 'LiteLLM_UserTable'. Fix this by creating user via `/user/new`."
7530+
"error": "{}, your role={}".format(
7531+
CommonProxyErrors.not_allowed_access.value,
7532+
user_api_key_dict.user_role,
7533+
)
75537534
},
75547535
)
7555-
raise HTTPException(status_code=500, detail={"error": str(e)})
7536+
7537+
response = await create_invitation_for_user(
7538+
data=data,
7539+
user_api_key_dict=user_api_key_dict,
7540+
)
7541+
return response
7542+
except Exception as e:
7543+
raise handle_exception_on_proxy(e)
7544+
75567545

75577546

75587547
@router.get(

0 commit comments

Comments
 (0)