mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 18:54:30 +00:00
Add remaining org CRUD endpoints + support deleting orgs on UI (#8561)
* feat(organization_endpoints.py): expose new `/organization/delete` endpoint. Cascade org deletion to member, teams and keys Ensures any org deletion is handled correctly * test(test_organizations.py): add simple test to ensure org deletion works * feat(organization_endpoints.py): expose /organization/update endpoint, and define response models for org delete + update * fix(organizations.tsx): support org delete on UI + move org/delete endpoint to use DELETE * feat(organization_endpoints.py): support `/organization/member_update` endpoint Allow admin to update member's role within org * feat(organization_endpoints.py): support deleting member from org * test(test_organizations.py): add e2e test to ensure org member flow works * fix(organization_endpoints.py): fix code qa check * fix(schema.prisma): don't introduce ondelete:cascade - breaking change * docs(organization_endpoints.py): document missing params * refactor(organization_view.tsx): initial commit creating a generic update member component shared between org and team member classes * feat(organization_view.tsx): support updating org member role on UI * feat(organization_view.tsx): allow proxy admin to delete members from org
This commit is contained in:
parent
1bac18a98c
commit
01ace78dcc
9 changed files with 932 additions and 94 deletions
|
@ -19,6 +19,10 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.management_endpoints.budget_management_endpoints import (
|
||||
new_budget,
|
||||
update_budget,
|
||||
)
|
||||
from litellm.proxy.management_helpers.utils import (
|
||||
get_new_internal_user_defaults,
|
||||
management_endpoint_wrapper,
|
||||
|
@ -179,24 +183,105 @@ async def new_organization(
|
|||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
@router.patch(
|
||||
"/organization/update",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=LiteLLM_OrganizationTableWithMembers,
|
||||
)
|
||||
async def update_organization():
|
||||
"""[TODO] Not Implemented yet. Let us know if you need this - https://github.com/BerriAI/litellm/issues"""
|
||||
raise NotImplementedError("Not Implemented Yet")
|
||||
async def update_organization(
|
||||
data: LiteLLM_OrganizationTableUpdate,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update an organization
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
if user_api_key_dict.user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "Cannot associate a user_id to this action. Check `/key/info` to validate if 'user_id' is set."
|
||||
},
|
||||
)
|
||||
|
||||
if data.updated_by is None:
|
||||
data.updated_by = user_api_key_dict.user_id
|
||||
|
||||
response = await prisma_client.db.litellm_organizationtable.update(
|
||||
where={"organization_id": data.organization_id},
|
||||
data=data.model_dump(exclude_none=True),
|
||||
include={"members": True, "teams": True, "litellm_budget_table": True},
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
@router.delete(
|
||||
"/organization/delete",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=List[LiteLLM_OrganizationTableWithMembers],
|
||||
)
|
||||
async def delete_organization():
|
||||
"""[TODO] Not Implemented yet. Let us know if you need this - https://github.com/BerriAI/litellm/issues"""
|
||||
raise NotImplementedError("Not Implemented Yet")
|
||||
async def delete_organization(
|
||||
data: DeleteOrganizationRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Delete an organization
|
||||
|
||||
# Parameters:
|
||||
|
||||
- organization_ids: List[str] - The organization ids to delete.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail={"error": "Only proxy admins can delete organizations"},
|
||||
)
|
||||
|
||||
deleted_orgs = []
|
||||
for organization_id in data.organization_ids:
|
||||
# delete all teams in the organization
|
||||
await prisma_client.db.litellm_teamtable.delete_many(
|
||||
where={"organization_id": organization_id}
|
||||
)
|
||||
# delete all members in the organization
|
||||
await prisma_client.db.litellm_organizationmembership.delete_many(
|
||||
where={"organization_id": organization_id}
|
||||
)
|
||||
# delete all keys in the organization
|
||||
await prisma_client.db.litellm_verificationtoken.delete_many(
|
||||
where={"organization_id": organization_id}
|
||||
)
|
||||
# delete the organization
|
||||
deleted_org = await prisma_client.db.litellm_organizationtable.delete(
|
||||
where={"organization_id": organization_id},
|
||||
include={"members": True, "teams": True, "litellm_budget_table": True},
|
||||
)
|
||||
if deleted_org is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": f"Organization={organization_id} not found"},
|
||||
)
|
||||
deleted_orgs.append(deleted_org)
|
||||
|
||||
return deleted_orgs
|
||||
|
||||
|
||||
@router.get(
|
||||
|
@ -429,6 +514,213 @@ async def organization_member_add(
|
|||
)
|
||||
|
||||
|
||||
async def find_member_if_email(
|
||||
user_email: str, prisma_client: PrismaClient
|
||||
) -> LiteLLM_UserTable:
|
||||
"""
|
||||
Find a member if the user_email is in LiteLLM_UserTable
|
||||
"""
|
||||
|
||||
try:
|
||||
existing_user_email_row: BaseModel = (
|
||||
await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_email": user_email}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Unique user not found for user_email={user_email}. Potential duplicate OR non-existent user_email in LiteLLM_UserTable. Use 'user_id' instead."
|
||||
},
|
||||
)
|
||||
existing_user_email_row_pydantic = LiteLLM_UserTable(
|
||||
**existing_user_email_row.model_dump()
|
||||
)
|
||||
return existing_user_email_row_pydantic
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/organization/member_update",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=LiteLLM_OrganizationMembershipTable,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def organization_member_update(
|
||||
data: OrganizationMemberUpdateRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Update a member's role in an organization
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
# Check if organization exists
|
||||
existing_organization_row = (
|
||||
await prisma_client.db.litellm_organizationtable.find_unique(
|
||||
where={"organization_id": data.organization_id}
|
||||
)
|
||||
)
|
||||
if existing_organization_row is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Organization not found for organization_id={getattr(data, 'organization_id', None)}"
|
||||
},
|
||||
)
|
||||
|
||||
# Check if member exists in organization
|
||||
if data.user_email is not None and data.user_id is None:
|
||||
existing_user_email_row = await find_member_if_email(
|
||||
data.user_email, prisma_client
|
||||
)
|
||||
data.user_id = existing_user_email_row.user_id
|
||||
|
||||
try:
|
||||
existing_organization_membership = (
|
||||
await prisma_client.db.litellm_organizationmembership.find_unique(
|
||||
where={
|
||||
"user_id_organization_id": {
|
||||
"user_id": data.user_id,
|
||||
"organization_id": data.organization_id,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Error finding organization membership for user_id={data.user_id} in organization={data.organization_id}: {e}"
|
||||
},
|
||||
)
|
||||
if existing_organization_membership is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={
|
||||
"error": f"Member not found in organization for user_id={data.user_id}"
|
||||
},
|
||||
)
|
||||
|
||||
# Update member role
|
||||
if data.role is not None:
|
||||
await prisma_client.db.litellm_organizationmembership.update(
|
||||
where={
|
||||
"user_id_organization_id": {
|
||||
"user_id": data.user_id,
|
||||
"organization_id": data.organization_id,
|
||||
}
|
||||
},
|
||||
data={"user_role": data.role},
|
||||
)
|
||||
if data.max_budget_in_organization is not None:
|
||||
# if budget_id is None, create a new budget
|
||||
budget_id = existing_organization_membership.budget_id or str(uuid.uuid4())
|
||||
if existing_organization_membership.budget_id is None:
|
||||
new_budget_obj = BudgetNewRequest(
|
||||
budget_id=budget_id, max_budget=data.max_budget_in_organization
|
||||
)
|
||||
await new_budget(
|
||||
budget_obj=new_budget_obj, user_api_key_dict=user_api_key_dict
|
||||
)
|
||||
else:
|
||||
# update budget table with new max_budget
|
||||
await update_budget(
|
||||
budget_obj=BudgetNewRequest(
|
||||
budget_id=budget_id, max_budget=data.max_budget_in_organization
|
||||
),
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
|
||||
# update organization membership with new budget_id
|
||||
await prisma_client.db.litellm_organizationmembership.update(
|
||||
where={
|
||||
"user_id_organization_id": {
|
||||
"user_id": data.user_id,
|
||||
"organization_id": data.organization_id,
|
||||
}
|
||||
},
|
||||
data={"budget_id": budget_id},
|
||||
)
|
||||
final_organization_membership: Optional[BaseModel] = (
|
||||
await prisma_client.db.litellm_organizationmembership.find_unique(
|
||||
where={
|
||||
"user_id_organization_id": {
|
||||
"user_id": data.user_id,
|
||||
"organization_id": data.organization_id,
|
||||
}
|
||||
},
|
||||
include={"litellm_budget_table": True},
|
||||
)
|
||||
)
|
||||
|
||||
if final_organization_membership is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": f"Member not found in organization={data.organization_id} for user_id={data.user_id}"
|
||||
},
|
||||
)
|
||||
|
||||
final_organization_membership_pydantic = LiteLLM_OrganizationMembershipTable(
|
||||
**final_organization_membership.model_dump(exclude_none=True)
|
||||
)
|
||||
return final_organization_membership_pydantic
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error updating member in organization: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/organization/member_delete",
|
||||
tags=["organization management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def organization_member_delete(
|
||||
data: OrganizationMemberDeleteRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
Delete a member from an organization
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
if data.user_email is not None and data.user_id is None:
|
||||
existing_user_email_row = await find_member_if_email(
|
||||
data.user_email, prisma_client
|
||||
)
|
||||
data.user_id = existing_user_email_row.user_id
|
||||
|
||||
member_to_delete = await prisma_client.db.litellm_organizationmembership.delete(
|
||||
where={
|
||||
"user_id_organization_id": {
|
||||
"user_id": data.user_id,
|
||||
"organization_id": data.organization_id,
|
||||
}
|
||||
}
|
||||
)
|
||||
return member_to_delete
|
||||
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.exception(f"Error deleting member from organization: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
async def add_member_to_organization(
|
||||
member: OrgMember,
|
||||
organization_id: str,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue