Skip to content

[Bug Fix] Invite links email should contain the correct invite id #12130

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from litellm.integrations.email_templates.user_invitation_email import (
USER_INVITATION_EMAIL_TEMPLATE,
)
from litellm.proxy._types import WebhookEvent
from litellm.proxy._types import InvitationNew, UserAPIKeyAuth, WebhookEvent
from litellm.types.integrations.slack_alerting import LITELLM_LOGO_URL


Expand Down Expand Up @@ -166,39 +166,81 @@ async def _get_invitation_link(self, user_id: Optional[str], base_url: str) -> s
"""
Get invitation link for the user
"""
import asyncio
# Early validation
if not user_id:
verbose_proxy_logger.debug("No user_id provided for invitation link")
return base_url

if not await self._is_prisma_client_available():
return base_url

# Wait for any concurrent invitation creation to complete
await self._wait_for_invitation_creation()

# Get or create invitation
invitation = await self._get_or_create_invitation(user_id)
if not invitation:
verbose_proxy_logger.warning(f"Failed to get/create invitation for user_id: {user_id}")
return base_url

return self._construct_invitation_link(invitation.id, base_url)

async def _is_prisma_client_available(self) -> bool:
"""Check if Prisma client is available"""
from litellm.proxy.proxy_server import prisma_client

if prisma_client is None:
verbose_proxy_logger.debug("Prisma client not found. Unable to lookup invitation")
return False
return True

################################################################################
########## Sleep for 10 seconds to wait for the invitation link to be created ###
################################################################################
# The UI, calls /invitation/new to generate the invitation link
# We wait 10 seconds to ensure the link is created
################################################################################
async def _wait_for_invitation_creation(self) -> None:
"""
Wait for any concurrent invitation creation to complete.

The UI calls /invitation/new to generate the invitation link.
We wait to ensure any pending invitation creation is completed.
"""
import asyncio
await asyncio.sleep(10)

async def _get_or_create_invitation(self, user_id: str):
"""
Get existing invitation or create a new one for the user

Returns:
Invitation object with id attribute, or None if failed
"""
from litellm.proxy.management_helpers.user_invitation import (
create_invitation_for_user,
)
from litellm.proxy.proxy_server import prisma_client

if prisma_client is None:
verbose_proxy_logger.debug(
f"Prisma client not found. Unable to lookup user email for user_id: {user_id}"
verbose_proxy_logger.error("Prisma client is None in _get_or_create_invitation")
return None

try:
# Try to get existing invitation
existing_invitations = await prisma_client.db.litellm_invitationlink.find_many(
where={"user_id": user_id},
order={"created_at": "desc"},
)
return base_url

if user_id is None:
return base_url

# get the latest invitation link for the user
invitation_rows = await prisma_client.db.litellm_invitationlink.find_many(
where={"user_id": user_id},
order={"created_at": "desc"},
)
if len(invitation_rows) > 0:
invitation_row = invitation_rows[0]
return self._construct_invitation_link(
invitation_id=invitation_row.id, base_url=base_url

if existing_invitations and len(existing_invitations) > 0:
verbose_proxy_logger.debug(f"Found existing invitation for user_id: {user_id}")
return existing_invitations[0]

# Create new invitation if none exists
verbose_proxy_logger.debug(f"Creating new invitation for user_id: {user_id}")
return await create_invitation_for_user(
data=InvitationNew(user_id=user_id),
user_api_key_dict=UserAPIKeyAuth(user_id=user_id),
)

return base_url

except Exception as e:
verbose_proxy_logger.error(f"Error getting/creating invitation for user_id {user_id}: {e}")
return None

def _construct_invitation_link(self, invitation_id: str, base_url: str) -> str:
"""
Expand Down
1 change: 0 additions & 1 deletion litellm/litellm_core_utils/litellm_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@
from .specialty_caches.dynamic_logging_cache import DynamicLoggingCache

if TYPE_CHECKING:
from litellm.llms.base_llm.files.transformation import BaseFileEndpoints
from litellm.llms.base_llm.passthrough.transformation import BasePassthroughConfig
try:
from litellm_enterprise.enterprise_callbacks.callback_controls import (
Expand Down
39 changes: 38 additions & 1 deletion litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"search_context_size_medium": 0.0,
"search_context_size_high": 0.0
},
"file_search_cost_per_1k_calls": 0.0,
"file_search_cost_per_gb_per_day": 0.0,
"vector_store_cost_per_gb_per_day": 0.0,
"computer_use_input_cost_per_1k_tokens": 0.0,
"computer_use_output_cost_per_1k_tokens": 0.0,
"code_interpreter_cost_per_session": 0.0,
"supported_regions": [
"global",
"us-west-2",
Expand Down Expand Up @@ -4575,6 +4581,37 @@
"supports_tool_choice": true,
"supports_prompt_caching": true
},
"deepseek/deepseek-r1": {
"max_tokens": 8192,
"max_input_tokens": 65536,
"max_output_tokens": 8192,
"input_cost_per_token": 5.5e-07,
"input_cost_per_token_cache_hit": 1.4e-07,
"output_cost_per_token": 2.19e-06,
"litellm_provider": "deepseek",
"mode": "chat",
"supports_function_calling": true,
"supports_assistant_prefill": true,
"supports_tool_choice": true,
"supports_reasoning": true,
"supports_prompt_caching": true
},
"deepseek/deepseek-v3": {
"max_tokens": 8192,
"max_input_tokens": 65536,
"max_output_tokens": 8192,
"input_cost_per_token": 2.7e-07,
"input_cost_per_token_cache_hit": 7e-08,
"cache_read_input_token_cost": 7e-08,
"cache_creation_input_token_cost": 0.0,
"output_cost_per_token": 1.1e-06,
"litellm_provider": "deepseek",
"mode": "chat",
"supports_function_calling": true,
"supports_assistant_prefill": true,
"supports_tool_choice": true,
"supports_prompt_caching": true
},
"codestral/codestral-latest": {
"max_tokens": 8191,
"max_input_tokens": 32000,
Expand Down Expand Up @@ -15735,4 +15772,4 @@
"notes": "ElevenLabs Scribe v1 experimental - enhanced version of the main Scribe model"
}
}
}
}
47 changes: 47 additions & 0 deletions litellm/proxy/management_helpers/user_invitation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from datetime import timedelta

from fastapi import HTTPException

import litellm
from litellm.proxy._types import CommonProxyErrors, InvitationNew, UserAPIKeyAuth


async def create_invitation_for_user(
data: InvitationNew,
user_api_key_dict: UserAPIKeyAuth,
):
"""
Create an invitation for the user to onboard to LiteLLM Admin UI.
"""
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client

if prisma_client is None:
raise HTTPException(
status_code=400,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)

current_time = litellm.utils.get_utc_datetime()
expires_at = current_time + timedelta(days=7)

try:
response = await prisma_client.db.litellm_invitationlink.create(
data={
"user_id": data.user_id,
"created_at": current_time,
"expires_at": expires_at,
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
"updated_at": current_time,
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
} # type: ignore
)
return response
except Exception as e:
if "Foreign key constraint failed on the field" in str(e):
raise HTTPException(
status_code=400,
detail={
"error": "User id does not exist in 'LiteLLM_UserTable'. Fix this by creating user via `/user/new`."
},
)
raise HTTPException(status_code=500, detail={"error": str(e)})
59 changes: 24 additions & 35 deletions litellm/proxy/proxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ def generate_feedback_box():
get_custom_url,
get_error_message_str,
get_server_root_path,
handle_exception_on_proxy,
hash_token,
update_spend,
)
Expand Down Expand Up @@ -7510,49 +7511,37 @@ async def new_invitation(
}'
```
"""
global prisma_client

if prisma_client is None:
raise HTTPException(
status_code=400,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)

if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=400,
detail={
"error": "{}, your role={}".format(
CommonProxyErrors.not_allowed_access.value,
user_api_key_dict.user_role,
)
},
try:
from litellm.proxy.management_helpers.user_invitation import (
create_invitation_for_user,
)
global prisma_client

current_time = litellm.utils.get_utc_datetime()
expires_at = current_time + timedelta(days=7)
if prisma_client is None:
raise HTTPException(
status_code=400,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)

try:
response = await prisma_client.db.litellm_invitationlink.create(
data={
"user_id": data.user_id,
"created_at": current_time,
"expires_at": expires_at,
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
"updated_at": current_time,
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
} # type: ignore
)
return response
except Exception as e:
if "Foreign key constraint failed on the field" in str(e):
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=400,
detail={
"error": "User id does not exist in 'LiteLLM_UserTable'. Fix this by creating user via `/user/new`."
"error": "{}, your role={}".format(
CommonProxyErrors.not_allowed_access.value,
user_api_key_dict.user_role,
)
},
)
raise HTTPException(status_code=500, detail={"error": str(e)})

response = await create_invitation_for_user(
data=data,
user_api_key_dict=user_api_key_dict,
)
return response
except Exception as e:
raise handle_exception_on_proxy(e)



@router.get(
Expand Down
Loading
Loading