|
""" |
|
Endpoints for /organization operations |
|
|
|
/organization/new |
|
/organization/update |
|
/organization/delete |
|
/organization/member_add |
|
/organization/info |
|
/organization/list |
|
""" |
|
|
|
|
|
|
|
import uuid |
|
from typing import List, Optional, Tuple |
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, status |
|
|
|
from litellm.proxy._types import * |
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth |
|
from litellm.proxy.management_helpers.utils import ( |
|
get_new_internal_user_defaults, |
|
management_endpoint_wrapper, |
|
) |
|
from litellm.proxy.utils import PrismaClient |
|
|
|
router = APIRouter() |
|
|
|
|
|
@router.post( |
|
"/organization/new", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
response_model=NewOrganizationResponse, |
|
) |
|
async def new_organization( |
|
data: NewOrganizationRequest, |
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), |
|
): |
|
""" |
|
Allow orgs to own teams |
|
|
|
Set org level budgets + model access. |
|
|
|
Only admins can create orgs. |
|
|
|
# Parameters |
|
|
|
- organization_alias: *str* - The name of the organization. |
|
- models: *List* - The models the organization has access to. |
|
- budget_id: *Optional[str]* - The id for a budget (tpm/rpm/max budget) for the organization. |
|
### IF NO BUDGET ID - CREATE ONE WITH THESE PARAMS ### |
|
- max_budget: *Optional[float]* - Max budget for org |
|
- tpm_limit: *Optional[int]* - Max tpm limit for org |
|
- rpm_limit: *Optional[int]* - Max rpm limit for org |
|
- max_parallel_requests: *Optional[int]* - [Not Implemented Yet] Max parallel requests for org |
|
- soft_budget: *Optional[float]* - [Not Implemented Yet] Get a slack alert when this soft budget is reached. Don't block requests. |
|
- model_max_budget: *Optional[dict]* - Max budget for a specific model |
|
- budget_duration: *Optional[str]* - Frequency of reseting org budget |
|
- metadata: *Optional[dict]* - Metadata for team, store information for team. Example metadata - {"extra_info": "some info"} |
|
- blocked: *bool* - Flag indicating if the org is blocked or not - will stop all calls from keys with this org_id. |
|
- tags: *Optional[List[str]]* - Tags for [tracking spend](https://litellm.vercel.app/docs/proxy/enterprise#tracking-spend-for-custom-tags) and/or doing [tag-based routing](https://litellm.vercel.app/docs/proxy/tag_routing). |
|
- organization_id: *Optional[str]* - The organization id of the team. Default is None. Create via `/organization/new`. |
|
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias) |
|
|
|
|
|
Case 1: Create new org **without** a budget_id |
|
|
|
```bash |
|
curl --location 'http://0.0.0.0:4000/organization/new' \ |
|
|
|
--header 'Authorization: Bearer sk-1234' \ |
|
|
|
--header 'Content-Type: application/json' \ |
|
|
|
--data '{ |
|
"organization_alias": "my-secret-org", |
|
"models": ["model1", "model2"], |
|
"max_budget": 100 |
|
}' |
|
|
|
|
|
``` |
|
|
|
Case 2: Create new org **with** a budget_id |
|
|
|
```bash |
|
curl --location 'http://0.0.0.0:4000/organization/new' \ |
|
|
|
--header 'Authorization: Bearer sk-1234' \ |
|
|
|
--header 'Content-Type: application/json' \ |
|
|
|
--data '{ |
|
"organization_alias": "my-secret-org", |
|
"models": ["model1", "model2"], |
|
"budget_id": "428eeaa8-f3ac-4e85-a8fb-7dc8d7aa8689" |
|
}' |
|
``` |
|
""" |
|
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client |
|
|
|
if prisma_client is None: |
|
raise HTTPException(status_code=500, detail={"error": "No db connected"}) |
|
|
|
if ( |
|
user_api_key_dict.user_role is None |
|
or user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN |
|
): |
|
raise HTTPException( |
|
status_code=401, |
|
detail={ |
|
"error": f"Only admins can create orgs. Your role is = {user_api_key_dict.user_role}" |
|
}, |
|
) |
|
|
|
if data.budget_id is None: |
|
""" |
|
Every organization needs a budget attached. |
|
|
|
If none provided, create one based on provided values |
|
""" |
|
budget_params = LiteLLM_BudgetTable.model_fields.keys() |
|
|
|
|
|
_json_data = data.json(exclude_none=True) |
|
_budget_data = {k: v for k, v in _json_data.items() if k in budget_params} |
|
budget_row = LiteLLM_BudgetTable(**_budget_data) |
|
|
|
new_budget = prisma_client.jsonify_object(budget_row.json(exclude_none=True)) |
|
|
|
_budget = await prisma_client.db.litellm_budgettable.create( |
|
data={ |
|
**new_budget, |
|
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name, |
|
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name, |
|
} |
|
) |
|
|
|
data.budget_id = _budget.budget_id |
|
|
|
""" |
|
Ensure only models that user has access to, are given to org |
|
""" |
|
if len(user_api_key_dict.models) == 0: |
|
pass |
|
else: |
|
if len(data.models) == 0: |
|
raise HTTPException( |
|
status_code=400, |
|
detail={ |
|
"error": "User not allowed to give access to all models. Select models you want org to have access to." |
|
}, |
|
) |
|
for m in data.models: |
|
if m not in user_api_key_dict.models: |
|
raise HTTPException( |
|
status_code=400, |
|
detail={ |
|
"error": f"User not allowed to give access to model={m}. Models you have access to = {user_api_key_dict.models}" |
|
}, |
|
) |
|
organization_row = LiteLLM_OrganizationTable( |
|
**data.json(exclude_none=True), |
|
created_by=user_api_key_dict.user_id or litellm_proxy_admin_name, |
|
updated_by=user_api_key_dict.user_id or litellm_proxy_admin_name, |
|
) |
|
new_organization_row = prisma_client.jsonify_object( |
|
organization_row.json(exclude_none=True) |
|
) |
|
response = await prisma_client.db.litellm_organizationtable.create( |
|
data={ |
|
**new_organization_row, |
|
} |
|
) |
|
|
|
return response |
|
|
|
|
|
@router.post( |
|
"/organization/update", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
) |
|
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") |
|
|
|
|
|
@router.post( |
|
"/organization/delete", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
) |
|
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") |
|
|
|
|
|
@router.get( |
|
"/organization/list", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
) |
|
async def list_organization( |
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), |
|
): |
|
""" |
|
``` |
|
curl --location --request GET 'http://0.0.0.0:4000/organization/list' \ |
|
--header 'Authorization: Bearer sk-1234' |
|
``` |
|
""" |
|
from litellm.proxy.proxy_server import prisma_client |
|
|
|
if prisma_client is None: |
|
raise HTTPException(status_code=500, detail={"error": "No db connected"}) |
|
|
|
if ( |
|
user_api_key_dict.user_role is None |
|
or user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN |
|
): |
|
raise HTTPException( |
|
status_code=401, |
|
detail={ |
|
"error": f"Only admins can list orgs. Your role is = {user_api_key_dict.user_role}" |
|
}, |
|
) |
|
if prisma_client is None: |
|
raise HTTPException( |
|
status_code=400, |
|
detail={"error": CommonProxyErrors.db_not_connected_error.value}, |
|
) |
|
response = await prisma_client.db.litellm_organizationtable.find_many( |
|
include={"members": True} |
|
) |
|
|
|
return response |
|
|
|
|
|
@router.post( |
|
"/organization/info", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
) |
|
async def info_organization(data: OrganizationRequest): |
|
""" |
|
Get the org specific information |
|
""" |
|
from litellm.proxy.proxy_server import prisma_client |
|
|
|
if prisma_client is None: |
|
raise HTTPException(status_code=500, detail={"error": "No db connected"}) |
|
|
|
if len(data.organizations) == 0: |
|
raise HTTPException( |
|
status_code=400, |
|
detail={ |
|
"error": f"Specify list of organization id's to query. Passed in={data.organizations}" |
|
}, |
|
) |
|
response = await prisma_client.db.litellm_organizationtable.find_many( |
|
where={"organization_id": {"in": data.organizations}}, |
|
include={"litellm_budget_table": True}, |
|
) |
|
|
|
return response |
|
|
|
|
|
@router.post( |
|
"/organization/member_add", |
|
tags=["organization management"], |
|
dependencies=[Depends(user_api_key_auth)], |
|
response_model=OrganizationAddMemberResponse, |
|
) |
|
@management_endpoint_wrapper |
|
async def organization_member_add( |
|
data: OrganizationMemberAddRequest, |
|
http_request: Request, |
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), |
|
) -> OrganizationAddMemberResponse: |
|
""" |
|
[BETA] |
|
|
|
Add new members (either via user_email or user_id) to an organization |
|
|
|
If user doesn't exist, new user row will also be added to User Table |
|
|
|
Only proxy_admin or org_admin of organization, allowed to access this endpoint. |
|
|
|
# Parameters: |
|
|
|
- organization_id: str (required) |
|
- member: Union[List[Member], Member] (required) |
|
- role: Literal[LitellmUserRoles] (required) |
|
- user_id: Optional[str] |
|
- user_email: Optional[str] |
|
|
|
Note: Either user_id or user_email must be provided for each member. |
|
|
|
Example: |
|
``` |
|
curl -X POST 'http://0.0.0.0:4000/organization/member_add' \ |
|
-H 'Authorization: Bearer sk-1234' \ |
|
-H 'Content-Type: application/json' \ |
|
-d '{ |
|
"organization_id": "45e3e396-ee08-4a61-a88e-16b3ce7e0849", |
|
"member": { |
|
"role": "internal_user", |
|
"user_id": "[email protected]" |
|
}, |
|
"max_budget_in_organization": 100.0 |
|
}' |
|
``` |
|
|
|
The following is executed in this function: |
|
|
|
1. Check if organization exists |
|
2. Creates a new Internal User if the user_id or user_email is not found in LiteLLM_UserTable |
|
3. Add Internal User to the `LiteLLM_OrganizationMembership` table |
|
""" |
|
try: |
|
from litellm.proxy.proxy_server import prisma_client |
|
|
|
if prisma_client is None: |
|
raise HTTPException(status_code=500, detail={"error": "No db connected"}) |
|
|
|
|
|
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=404, |
|
detail={ |
|
"error": f"Organization not found for organization_id={getattr(data, 'organization_id', None)}" |
|
}, |
|
) |
|
|
|
members: List[OrgMember] |
|
if isinstance(data.member, List): |
|
members = data.member |
|
else: |
|
members = [data.member] |
|
|
|
updated_users: List[LiteLLM_UserTable] = [] |
|
updated_organization_memberships: List[LiteLLM_OrganizationMembershipTable] = [] |
|
|
|
for member in members: |
|
updated_user, updated_organization_membership = ( |
|
await add_member_to_organization( |
|
member=member, |
|
organization_id=data.organization_id, |
|
prisma_client=prisma_client, |
|
) |
|
) |
|
|
|
updated_users.append(updated_user) |
|
updated_organization_memberships.append(updated_organization_membership) |
|
|
|
return OrganizationAddMemberResponse( |
|
organization_id=data.organization_id, |
|
updated_users=updated_users, |
|
updated_organization_memberships=updated_organization_memberships, |
|
) |
|
except Exception as e: |
|
if isinstance(e, HTTPException): |
|
raise ProxyException( |
|
message=getattr(e, "detail", f"Authentication Error({str(e)})"), |
|
type=ProxyErrorTypes.auth_error, |
|
param=getattr(e, "param", "None"), |
|
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), |
|
) |
|
elif isinstance(e, ProxyException): |
|
raise e |
|
raise ProxyException( |
|
message="Authentication Error, " + str(e), |
|
type=ProxyErrorTypes.auth_error, |
|
param=getattr(e, "param", "None"), |
|
code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
|
) |
|
|
|
|
|
async def add_member_to_organization( |
|
member: OrgMember, |
|
organization_id: str, |
|
prisma_client: PrismaClient, |
|
) -> Tuple[LiteLLM_UserTable, LiteLLM_OrganizationMembershipTable]: |
|
""" |
|
Add a member to an organization |
|
|
|
- Checks if member.user_id or member.user_email is in LiteLLM_UserTable |
|
- If not found, create a new user in LiteLLM_UserTable |
|
- Add user to organization in LiteLLM_OrganizationMembership |
|
""" |
|
|
|
try: |
|
user_object: Optional[LiteLLM_UserTable] = None |
|
existing_user_id_row = None |
|
existing_user_email_row = None |
|
|
|
if member.user_id is not None: |
|
existing_user_id_row = await prisma_client.db.litellm_usertable.find_unique( |
|
where={"user_id": member.user_id} |
|
) |
|
|
|
if member.user_email is not None: |
|
existing_user_email_row = ( |
|
await prisma_client.db.litellm_usertable.find_unique( |
|
where={"user_email": member.user_email} |
|
) |
|
) |
|
|
|
|
|
if existing_user_id_row is None and existing_user_email_row is None: |
|
|
|
user_id: str = member.user_id or str(uuid.uuid4()) |
|
new_user_defaults = get_new_internal_user_defaults( |
|
user_id=user_id, |
|
user_email=member.user_email, |
|
) |
|
|
|
_returned_user = await prisma_client.insert_data(data=new_user_defaults, table_name="user") |
|
if _returned_user is not None: |
|
user_object = LiteLLM_UserTable(**_returned_user.model_dump()) |
|
elif existing_user_email_row is not None and len(existing_user_email_row) > 1: |
|
raise HTTPException( |
|
status_code=400, |
|
detail={ |
|
"error": "Multiple users with this email found in db. Please use 'user_id' instead." |
|
}, |
|
) |
|
elif existing_user_email_row is not None: |
|
user_object = LiteLLM_UserTable(**existing_user_email_row.model_dump()) |
|
elif existing_user_id_row is not None: |
|
user_object = LiteLLM_UserTable(**existing_user_id_row.model_dump()) |
|
else: |
|
raise HTTPException( |
|
status_code=404, |
|
detail={ |
|
"error": f"User not found for user_id={member.user_id} and user_email={member.user_email}" |
|
}, |
|
) |
|
|
|
if user_object is None: |
|
raise ValueError( |
|
f"User does not exist in LiteLLM_UserTable. user_id={member.user_id} and user_email={member.user_email}" |
|
) |
|
|
|
|
|
_organization_membership = ( |
|
await prisma_client.db.litellm_organizationmembership.create( |
|
data={ |
|
"organization_id": organization_id, |
|
"user_id": user_object.user_id, |
|
"user_role": member.role, |
|
} |
|
) |
|
) |
|
organization_membership = LiteLLM_OrganizationMembershipTable( |
|
**_organization_membership.model_dump() |
|
) |
|
return user_object, organization_membership |
|
|
|
except Exception as e: |
|
raise ValueError(f"Error adding member to organization: {e}") |
|
|