937 lines
33 KiB
Python
937 lines
33 KiB
Python
"""
|
|
Endpoints for /project operations
|
|
|
|
/project/new
|
|
/project/update
|
|
/project/delete
|
|
/project/info
|
|
/project/list
|
|
"""
|
|
|
|
#### PROJECT MANAGEMENT ####
|
|
|
|
import json
|
|
from typing import List, Optional, Union
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
from litellm._uuid import uuid
|
|
from litellm.proxy._types import *
|
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
|
from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field
|
|
from litellm.proxy.management_helpers.utils import (
|
|
management_endpoint_wrapper,
|
|
)
|
|
from litellm.proxy.utils import PrismaClient, handle_exception_on_proxy
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
async def _check_user_permission_for_project(
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
team_id: Optional[str],
|
|
prisma_client: PrismaClient,
|
|
require_admin: bool = False,
|
|
team_object: Optional[LiteLLM_TeamTable] = None,
|
|
) -> bool:
|
|
"""
|
|
Check if user has permission to manage a project.
|
|
|
|
Returns True if user is proxy admin or team admin (when team_id provided).
|
|
If require_admin=True, only proxy admins are allowed.
|
|
|
|
If team_object is provided, it will be used instead of fetching from DB
|
|
(avoids duplicate DB queries when team was already fetched for validation).
|
|
"""
|
|
is_proxy_admin = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
|
|
|
if require_admin:
|
|
return is_proxy_admin
|
|
|
|
if is_proxy_admin:
|
|
return True
|
|
|
|
if not team_id or not user_api_key_dict.user_id:
|
|
return False
|
|
|
|
team = team_object
|
|
if team is None:
|
|
team = await prisma_client.db.litellm_teamtable.find_unique(
|
|
where={"team_id": team_id}
|
|
)
|
|
|
|
if team and team.admins:
|
|
return user_api_key_dict.user_id in team.admins
|
|
|
|
return False
|
|
|
|
|
|
async def _validate_team_exists(
|
|
team_id: str,
|
|
prisma_client: PrismaClient,
|
|
):
|
|
"""Validate that a team exists. Returns the team row."""
|
|
team = await prisma_client.db.litellm_teamtable.find_unique(
|
|
where={"team_id": team_id},
|
|
)
|
|
|
|
if team is None:
|
|
raise ProxyException(
|
|
message=f"Team not found, team_id={team_id}",
|
|
type="not_found",
|
|
code=404,
|
|
param="team_id",
|
|
)
|
|
|
|
return team
|
|
|
|
|
|
def _check_team_project_limits(
|
|
team_object: LiteLLM_TeamTable,
|
|
data: Union[NewProjectRequest, UpdateProjectRequest],
|
|
) -> None:
|
|
"""
|
|
Check that project limits respect its parent Team's limits.
|
|
|
|
Mirrors _check_org_team_limits() from team_endpoints.py.
|
|
|
|
Validates:
|
|
- Project models are a subset of Team models
|
|
- Project max_budget <= Team max_budget
|
|
- Project tpm_limit <= Team tpm_limit
|
|
- Project rpm_limit <= Team rpm_limit
|
|
- Budget values are non-negative
|
|
- soft_budget < max_budget
|
|
"""
|
|
# --- Budget non-negativity checks ---
|
|
if data.max_budget is not None and data.max_budget < 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"max_budget cannot be negative. Received: {data.max_budget}"
|
|
},
|
|
)
|
|
if data.soft_budget is not None and data.soft_budget < 0:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"soft_budget cannot be negative. Received: {data.soft_budget}"
|
|
},
|
|
)
|
|
|
|
# --- soft_budget < max_budget ---
|
|
if data.soft_budget is not None and data.max_budget is not None:
|
|
if data.soft_budget >= data.max_budget:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"soft_budget ({data.soft_budget}) must be strictly lower than max_budget ({data.max_budget})"
|
|
},
|
|
)
|
|
|
|
# --- Validate project models are a subset of team models ---
|
|
project_models = getattr(data, "models", None)
|
|
team_models = team_object.models or []
|
|
if project_models and len(team_models) > 0:
|
|
# If team has 'all-proxy-models', skip validation as it allows all models
|
|
if SpecialModelNames.all_proxy_models.value not in team_models:
|
|
for m in project_models:
|
|
if m not in team_models:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"Model '{m}' not in team's allowed models. Team allowed models={team_models}. Team: {team_object.team_id}"
|
|
},
|
|
)
|
|
|
|
# --- Validate project max_budget <= team max_budget ---
|
|
# Team stores budget fields directly (max_budget, tpm_limit, rpm_limit)
|
|
# unlike Project which uses a separate LiteLLM_BudgetTable relation
|
|
if (
|
|
data.max_budget is not None
|
|
and team_object.max_budget is not None
|
|
and data.max_budget > team_object.max_budget
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"Project max_budget ({data.max_budget}) exceeds team's max_budget ({team_object.max_budget}). Team: {team_object.team_id}"
|
|
},
|
|
)
|
|
|
|
# --- Validate project tpm_limit <= team tpm_limit ---
|
|
if (
|
|
data.tpm_limit is not None
|
|
and team_object.tpm_limit is not None
|
|
and data.tpm_limit > team_object.tpm_limit
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"Project tpm_limit ({data.tpm_limit}) exceeds team's tpm_limit ({team_object.tpm_limit}). Team: {team_object.team_id}"
|
|
},
|
|
)
|
|
|
|
# --- Validate project rpm_limit <= team rpm_limit ---
|
|
if (
|
|
data.rpm_limit is not None
|
|
and team_object.rpm_limit is not None
|
|
and data.rpm_limit > team_object.rpm_limit
|
|
):
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"Project rpm_limit ({data.rpm_limit}) exceeds team's rpm_limit ({team_object.rpm_limit}). Team: {team_object.team_id}"
|
|
},
|
|
)
|
|
|
|
|
|
async def _create_budget_for_project(
|
|
data: NewProjectRequest,
|
|
user_id: Optional[str],
|
|
litellm_proxy_admin_name: str,
|
|
prisma_client: PrismaClient,
|
|
) -> str:
|
|
"""Create a budget for the project and return budget_id."""
|
|
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_id or litellm_proxy_admin_name,
|
|
"updated_by": user_id or litellm_proxy_admin_name,
|
|
}
|
|
)
|
|
|
|
return _budget.budget_id
|
|
|
|
|
|
async def _set_project_object_permission(
|
|
data: NewProjectRequest,
|
|
prisma_client: Optional[PrismaClient],
|
|
) -> Optional[str]:
|
|
"""
|
|
Creates the LiteLLM_ObjectPermissionTable record for the project.
|
|
Returns the object_permission_id if created, otherwise None.
|
|
"""
|
|
if prisma_client is None:
|
|
return None
|
|
|
|
if data.object_permission is not None:
|
|
created_object_permission = (
|
|
await prisma_client.db.litellm_objectpermissiontable.create(
|
|
data=data.object_permission.model_dump(exclude_none=True),
|
|
)
|
|
)
|
|
del data.object_permission
|
|
return created_object_permission.object_permission_id
|
|
return None
|
|
|
|
|
|
def _remove_budget_fields_from_project_data(project_data: dict) -> dict:
|
|
"""
|
|
Remove budget fields from project data.
|
|
Budget fields belong to LiteLLM_BudgetTable, not LiteLLM_ProjectTable.
|
|
Keep budget_id as it's a foreign key.
|
|
|
|
Following the pattern from organization_endpoints.py
|
|
"""
|
|
budget_fields = LiteLLM_BudgetTable.model_fields.keys()
|
|
for field in list(budget_fields):
|
|
if field != "budget_id": # Keep the foreign key
|
|
project_data.pop(field, None)
|
|
return project_data
|
|
|
|
|
|
@router.post(
|
|
"/project/new",
|
|
tags=["project management"],
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=NewProjectResponse,
|
|
)
|
|
@management_endpoint_wrapper
|
|
async def new_project(
|
|
data: NewProjectRequest,
|
|
http_request: Request,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Create a new project. Projects sit between teams and keys in the hierarchy.
|
|
|
|
Only admins or team admins can create projects.
|
|
|
|
# Parameters
|
|
|
|
- project_alias: *Optional[str]* - The name of the project.
|
|
- description: *Optional[str]* - Description of the project's purpose and use case.
|
|
- team_id: *str* - The team id that this project belongs to. Required.
|
|
- models: *List* - The models the project has access to.
|
|
- budget_id: *Optional[str]* - The id for a budget (tpm/rpm/max budget) for the project.
|
|
### IF NO BUDGET ID - CREATE ONE WITH THESE PARAMS ###
|
|
- max_budget: *Optional[float]* - Max budget for project
|
|
- tpm_limit: *Optional[int]* - Max tpm limit for project
|
|
- rpm_limit: *Optional[int]* - Max rpm limit for project
|
|
- max_parallel_requests: *Optional[int]* - Max parallel requests for project
|
|
- soft_budget: *Optional[float]* - 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. Example: {"gpt-4": 100.0, "gpt-3.5-turbo": 50.0}
|
|
- model_rpm_limit: *Optional[dict]* - RPM limits per model. Example: {"gpt-4": 1000, "gpt-3.5-turbo": 5000}
|
|
- model_tpm_limit: *Optional[dict]* - TPM limits per model. Example: {"gpt-4": 50000, "gpt-3.5-turbo": 100000}
|
|
- budget_duration: *Optional[str]* - Frequency of reseting project budget
|
|
- metadata: *Optional[dict]* - Metadata for project, store information for project. Example metadata - {"use_case_id": "SNOW-12345", "responsible_ai_id": "RAI-67890"}
|
|
- tags: *Optional[list]* - Tags for the project. Example: ["production", "api"]
|
|
- blocked: *bool* - Flag indicating if the project is blocked or not - will stop all calls from keys with this project_id.
|
|
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - project-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
|
|
|
Example 1: Create new project **without** a budget_id, with model-specific limits
|
|
|
|
```bash
|
|
curl --location 'http://0.0.0.0:4000/project/new' \\
|
|
--header 'Authorization: Bearer sk-1234' \\
|
|
--header 'Content-Type: application/json' \\
|
|
--data '{
|
|
"project_alias": "flight-search-assistant",
|
|
"description": "AI-powered flight search and booking assistant",
|
|
"team_id": "team-123",
|
|
"models": ["gpt-4", "gpt-3.5-turbo"],
|
|
"max_budget": 100,
|
|
"model_rpm_limit": {
|
|
"gpt-4": 1000,
|
|
"gpt-3.5-turbo": 5000
|
|
},
|
|
"model_tpm_limit": {
|
|
"gpt-4": 50000,
|
|
"gpt-3.5-turbo": 100000
|
|
},
|
|
"metadata": {
|
|
"use_case_id": "SNOW-12345",
|
|
"responsible_ai_id": "RAI-67890"
|
|
}
|
|
}'
|
|
```
|
|
|
|
Example 2: Create new project **with** a budget_id
|
|
|
|
```bash
|
|
curl --location 'http://0.0.0.0:4000/project/new' \\
|
|
--header 'Authorization: Bearer sk-1234' \\
|
|
--header 'Content-Type: application/json' \\
|
|
--data '{
|
|
"project_alias": "hotel-recommendations",
|
|
"description": "Personalized hotel recommendation engine",
|
|
"team_id": "team-123",
|
|
"models": ["claude-3-sonnet"],
|
|
"budget_id": "428eeaa8-f3ac-4e85-a8fb-7dc8d7aa8689",
|
|
"metadata": {
|
|
"use_case_id": "SNOW-54321"
|
|
}
|
|
}'
|
|
```
|
|
"""
|
|
from litellm.proxy.proxy_server import (
|
|
litellm_proxy_admin_name,
|
|
premium_user,
|
|
prisma_client,
|
|
)
|
|
|
|
try:
|
|
if getattr(data, "tags", None) is not None and not premium_user:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": "Only premium users can add tags to projects. "
|
|
+ CommonProxyErrors.not_premium_user.value
|
|
},
|
|
)
|
|
|
|
if not premium_user:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": "Project management is an enterprise feature. "
|
|
+ CommonProxyErrors.not_premium_user.value
|
|
},
|
|
)
|
|
|
|
# ADD METADATA FIELDS
|
|
for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
|
|
if getattr(data, field, None) is not None:
|
|
_set_object_metadata_field(
|
|
object_data=data,
|
|
field_name=field,
|
|
value=getattr(data, field),
|
|
)
|
|
delattr(data, field)
|
|
|
|
if prisma_client is None:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
|
)
|
|
|
|
# Validate team exists and get team object with budget
|
|
team_object = await _validate_team_exists(
|
|
team_id=data.team_id, prisma_client=prisma_client
|
|
)
|
|
|
|
# Validate project limits against team limits
|
|
_check_team_project_limits(
|
|
team_object=LiteLLM_TeamTable(**team_object.model_dump()),
|
|
data=data,
|
|
)
|
|
|
|
# Check if user has permission to create projects for this team
|
|
# only team admins can create projects for their team
|
|
has_permission = await _check_user_permission_for_project(
|
|
user_api_key_dict=user_api_key_dict,
|
|
team_id=data.team_id,
|
|
prisma_client=prisma_client,
|
|
team_object=LiteLLM_TeamTable(**team_object.model_dump()),
|
|
)
|
|
|
|
if not has_permission:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": f"Only admins or team admins can create projects. Your role is {user_api_key_dict.user_role}"
|
|
},
|
|
)
|
|
|
|
# Generate project_id if not provided
|
|
if data.project_id is None:
|
|
data.project_id = str(uuid.uuid4())
|
|
else:
|
|
# Check if project_id already exists
|
|
existing_project = await prisma_client.db.litellm_projecttable.find_unique(
|
|
where={"project_id": data.project_id}
|
|
)
|
|
if existing_project is not None:
|
|
raise ProxyException(
|
|
message=f"Project id = {data.project_id} already exists. Please use a different project id.",
|
|
type="bad_request",
|
|
code=400,
|
|
param="project_id",
|
|
)
|
|
|
|
# Create budget if not provided
|
|
if data.budget_id is None:
|
|
data.budget_id = await _create_budget_for_project(
|
|
data=data,
|
|
user_id=user_api_key_dict.user_id,
|
|
litellm_proxy_admin_name=litellm_proxy_admin_name,
|
|
prisma_client=prisma_client,
|
|
)
|
|
|
|
## Handle Object Permission - MCP, Vector Stores etc.
|
|
object_permission_id = await _set_project_object_permission(
|
|
data=data,
|
|
prisma_client=prisma_client,
|
|
)
|
|
|
|
# Create project row (following organization_endpoints.py pattern)
|
|
project_row = LiteLLM_ProjectTable(
|
|
**data.json(exclude_none=True),
|
|
object_permission_id=object_permission_id,
|
|
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,
|
|
)
|
|
|
|
for field in LiteLLM_ManagementEndpoint_MetadataFields:
|
|
if getattr(data, field, None) is not None:
|
|
_set_object_metadata_field(
|
|
object_data=project_row,
|
|
field_name=field,
|
|
value=getattr(data, field),
|
|
)
|
|
|
|
new_project_row = prisma_client.jsonify_object(
|
|
project_row.json(exclude_none=True)
|
|
)
|
|
|
|
# Remove budget fields (following organization_endpoints.py pattern)
|
|
new_project_row = _remove_budget_fields_from_project_data(new_project_row)
|
|
|
|
verbose_proxy_logger.info(
|
|
f"new_project_row: {json.dumps(new_project_row, indent=2)}"
|
|
)
|
|
response = await prisma_client.db.litellm_projecttable.create(
|
|
data={
|
|
**new_project_row, # type: ignore
|
|
},
|
|
include={"litellm_budget_table": True},
|
|
)
|
|
|
|
return response
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.management_endpoints.project_endpoints.new_project(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
raise handle_exception_on_proxy(e)
|
|
|
|
|
|
@router.post(
|
|
"/project/update",
|
|
tags=["project management"],
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=LiteLLM_ProjectTable,
|
|
)
|
|
@management_endpoint_wrapper
|
|
async def update_project( # noqa: PLR0915
|
|
data: UpdateProjectRequest,
|
|
http_request: Request,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Update a project
|
|
|
|
Parameters:
|
|
- project_id: *str* - The project id to update. Required.
|
|
- project_alias: *Optional[str]* - Updated name for the project
|
|
- description: *Optional[str]* - Updated description for the project
|
|
- team_id: *Optional[str]* - Updated team_id for the project
|
|
- metadata: *Optional[dict]* - Updated metadata for project
|
|
- models: *Optional[list]* - Updated list of models for the project
|
|
- blocked: *Optional[bool]* - Updated blocked status
|
|
- max_budget: *Optional[float]* - Updated max budget
|
|
- tpm_limit: *Optional[int]* - Updated tpm limit
|
|
- rpm_limit: *Optional[int]* - Updated rpm limit
|
|
- model_rpm_limit: *Optional[dict]* - Updated RPM limits per model
|
|
- model_tpm_limit: *Optional[dict]* - Updated TPM limits per model
|
|
- budget_duration: *Optional[str]* - Updated budget duration
|
|
- tags: *Optional[list]* - Updated list of tags for the project
|
|
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - Updated object permission
|
|
|
|
Example:
|
|
```bash
|
|
curl --location 'http://0.0.0.0:4000/project/update' \\
|
|
--header 'Authorization: Bearer sk-1234' \\
|
|
--header 'Content-Type: application/json' \\
|
|
--data '{
|
|
"project_id": "project-123",
|
|
"description": "Updated flight search system with enhanced capabilities",
|
|
"max_budget": 200,
|
|
"model_rpm_limit": {
|
|
"gpt-4": 2000,
|
|
"gpt-3.5-turbo": 10000
|
|
},
|
|
"metadata": {
|
|
"use_case_id": "SNOW-12345",
|
|
"status": "active"
|
|
}
|
|
}'
|
|
```
|
|
"""
|
|
from litellm.proxy.proxy_server import (
|
|
litellm_proxy_admin_name,
|
|
premium_user,
|
|
prisma_client,
|
|
)
|
|
|
|
try:
|
|
if getattr(data, "tags", None) is not None and not premium_user:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": "Only premium users can add tags to projects. "
|
|
+ CommonProxyErrors.not_premium_user.value
|
|
},
|
|
)
|
|
|
|
if not premium_user:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": "Project management is an enterprise feature. "
|
|
+ CommonProxyErrors.not_premium_user.value
|
|
},
|
|
)
|
|
|
|
# ADD METADATA FIELDS
|
|
for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
|
|
if getattr(data, field, None) is not None:
|
|
_set_object_metadata_field(
|
|
object_data=data,
|
|
field_name=field,
|
|
value=getattr(data, field),
|
|
)
|
|
delattr(data, field)
|
|
|
|
if prisma_client is None:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
|
)
|
|
|
|
if data.project_id is None:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"error": "project_id is required"},
|
|
)
|
|
|
|
# Fetch existing project
|
|
existing_project = await prisma_client.db.litellm_projecttable.find_unique(
|
|
where={"project_id": data.project_id}
|
|
)
|
|
|
|
if existing_project is None:
|
|
raise ProxyException(
|
|
message=f"Project not found, project_id={data.project_id}",
|
|
type="not_found",
|
|
code=404,
|
|
param="project_id",
|
|
)
|
|
|
|
# Validate team exists and get team object for limit + permission checks
|
|
team_id_to_check = data.team_id or existing_project.team_id
|
|
team_obj_for_checks = None
|
|
if team_id_to_check is not None:
|
|
team_obj_for_checks = await _validate_team_exists(
|
|
team_id=team_id_to_check, prisma_client=prisma_client
|
|
)
|
|
|
|
# Check if user has permission to update this project
|
|
has_permission = await _check_user_permission_for_project(
|
|
user_api_key_dict=user_api_key_dict,
|
|
team_id=existing_project.team_id,
|
|
prisma_client=prisma_client,
|
|
team_object=LiteLLM_TeamTable(**team_obj_for_checks.model_dump())
|
|
if team_obj_for_checks
|
|
else None,
|
|
)
|
|
|
|
if not has_permission:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={"error": "Only admins or team admins can update projects"},
|
|
)
|
|
|
|
# Validate project limits against team limits
|
|
if team_obj_for_checks is not None:
|
|
_check_team_project_limits(
|
|
team_object=LiteLLM_TeamTable(**team_obj_for_checks.model_dump()),
|
|
data=data,
|
|
)
|
|
|
|
# Prepare update data
|
|
update_data = data.json(exclude_none=True, exclude={"project_id"})
|
|
update_data = prisma_client.jsonify_object(update_data)
|
|
update_data["updated_by"] = (
|
|
user_api_key_dict.user_id or litellm_proxy_admin_name
|
|
)
|
|
|
|
# Handle budget updates
|
|
budget_fields = LiteLLM_BudgetTable.model_fields.keys()
|
|
budget_updates = {k: v for k, v in update_data.items() if k in budget_fields}
|
|
|
|
if budget_updates and existing_project.budget_id:
|
|
# Update existing budget
|
|
await prisma_client.db.litellm_budgettable.update(
|
|
where={"budget_id": existing_project.budget_id},
|
|
data={
|
|
**budget_updates,
|
|
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
|
},
|
|
)
|
|
# Remove budget fields from project update
|
|
for field in budget_updates.keys():
|
|
update_data.pop(field, None)
|
|
|
|
# Handle object permissions
|
|
if "object_permission" in update_data:
|
|
object_permission_data = update_data.pop("object_permission")
|
|
if object_permission_data:
|
|
if existing_project.object_permission_id:
|
|
# Update existing permission
|
|
await prisma_client.db.litellm_objectpermissiontable.update(
|
|
where={
|
|
"object_permission_id": existing_project.object_permission_id
|
|
},
|
|
data=object_permission_data,
|
|
)
|
|
else:
|
|
# Create new permission
|
|
created_permission = (
|
|
await prisma_client.db.litellm_objectpermissiontable.create(
|
|
data=object_permission_data,
|
|
)
|
|
)
|
|
update_data[
|
|
"object_permission_id"
|
|
] = created_permission.object_permission_id
|
|
|
|
# Handle metadata fields
|
|
for field in LiteLLM_ManagementEndpoint_MetadataFields:
|
|
if field in update_data:
|
|
if update_data.get("metadata") is None:
|
|
update_data["metadata"] = {}
|
|
update_data["metadata"][field] = update_data.pop(field)
|
|
|
|
# Remove budget fields (following organization_endpoints.py pattern)
|
|
update_data = _remove_budget_fields_from_project_data(update_data)
|
|
|
|
# Update project
|
|
updated_project = await prisma_client.db.litellm_projecttable.update(
|
|
where={"project_id": data.project_id},
|
|
data=update_data,
|
|
include={"litellm_budget_table": True, "object_permission": True},
|
|
)
|
|
|
|
return updated_project
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.management_endpoints.project_endpoints.update_project(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
raise handle_exception_on_proxy(e)
|
|
|
|
|
|
@router.delete(
|
|
"/project/delete",
|
|
tags=["project management"],
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=List[LiteLLM_ProjectTable],
|
|
)
|
|
@management_endpoint_wrapper
|
|
async def delete_project(
|
|
data: DeleteProjectRequest,
|
|
http_request: Request,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Delete projects
|
|
|
|
Parameters:
|
|
- project_ids: *List[str]* - List of project ids to delete
|
|
|
|
Example:
|
|
```bash
|
|
curl --location --request DELETE 'http://0.0.0.0:4000/project/delete' \\
|
|
--header 'Authorization: Bearer sk-1234' \\
|
|
--header 'Content-Type: application/json' \\
|
|
--data '{
|
|
"project_ids": ["project-123", "project-456"]
|
|
}'
|
|
```
|
|
"""
|
|
from litellm.proxy.proxy_server import premium_user, prisma_client
|
|
|
|
try:
|
|
if not premium_user:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={
|
|
"error": "Project management is an enterprise feature. "
|
|
+ CommonProxyErrors.not_premium_user.value
|
|
},
|
|
)
|
|
|
|
if prisma_client is None:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
|
)
|
|
|
|
# Check if user is admin (only admins can delete projects)
|
|
has_permission = await _check_user_permission_for_project(
|
|
user_api_key_dict=user_api_key_dict,
|
|
team_id=None,
|
|
prisma_client=prisma_client,
|
|
require_admin=True,
|
|
)
|
|
|
|
if not has_permission:
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={"error": "Only admins can delete projects"},
|
|
)
|
|
|
|
deleted_projects = []
|
|
|
|
for project_id in data.project_ids:
|
|
# Check if project exists
|
|
existing_project = await prisma_client.db.litellm_projecttable.find_unique(
|
|
where={"project_id": project_id}
|
|
)
|
|
|
|
if existing_project is None:
|
|
raise ProxyException(
|
|
message=f"Project not found, project_id={project_id}",
|
|
type="not_found",
|
|
code=404,
|
|
param="project_ids",
|
|
)
|
|
|
|
# Check if there are any keys associated with this project
|
|
associated_keys = (
|
|
await prisma_client.db.litellm_verificationtoken.find_many(
|
|
where={"project_id": project_id}
|
|
)
|
|
)
|
|
|
|
if len(associated_keys) > 0:
|
|
raise ProxyException(
|
|
message=f"Cannot delete project {project_id}. {len(associated_keys)} key(s) are associated with it. Please delete or reassign the keys first.",
|
|
type="bad_request",
|
|
code=400,
|
|
param="project_ids",
|
|
)
|
|
|
|
# Delete the project
|
|
deleted_project = await prisma_client.db.litellm_projecttable.delete(
|
|
where={"project_id": project_id}
|
|
)
|
|
|
|
deleted_projects.append(deleted_project)
|
|
|
|
return deleted_projects
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.management_endpoints.project_endpoints.delete_project(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
raise handle_exception_on_proxy(e)
|
|
|
|
|
|
@router.get(
|
|
"/project/info",
|
|
tags=["project management"],
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=LiteLLM_ProjectTable,
|
|
)
|
|
async def project_info(
|
|
project_id: str,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Get information about a specific project
|
|
|
|
Parameters:
|
|
- project_id: *str* - The project id to fetch info for
|
|
|
|
Example:
|
|
```bash
|
|
curl --location 'http://0.0.0.0:4000/project/info?project_id=project-123' \\
|
|
--header 'Authorization: Bearer sk-1234'
|
|
```
|
|
"""
|
|
from litellm.proxy.proxy_server import prisma_client
|
|
|
|
try:
|
|
if prisma_client is None:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
|
)
|
|
|
|
# Fetch project
|
|
project = await prisma_client.db.litellm_projecttable.find_unique(
|
|
where={"project_id": project_id},
|
|
include={"litellm_budget_table": True, "object_permission": True},
|
|
)
|
|
|
|
if project is None:
|
|
raise ProxyException(
|
|
message=f"Project not found, project_id={project_id}",
|
|
type="not_found",
|
|
code=404,
|
|
param="project_id",
|
|
)
|
|
|
|
# Check if user has access to this project (admin or team member)
|
|
is_admin = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
|
is_team_member = False
|
|
|
|
if project.team_id and user_api_key_dict.user_id:
|
|
team = await prisma_client.db.litellm_teamtable.find_unique(
|
|
where={"team_id": project.team_id}
|
|
)
|
|
if team:
|
|
is_team_member = (
|
|
user_api_key_dict.user_id in team.admins
|
|
or user_api_key_dict.user_id in team.members
|
|
)
|
|
|
|
if not (is_admin or is_team_member):
|
|
raise HTTPException(
|
|
status_code=403,
|
|
detail={"error": "You don't have access to this project"},
|
|
)
|
|
|
|
return project
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.management_endpoints.project_endpoints.project_info(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
raise handle_exception_on_proxy(e)
|
|
|
|
|
|
@router.get(
|
|
"/project/list",
|
|
tags=["project management"],
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=List[LiteLLM_ProjectTable],
|
|
)
|
|
async def list_projects(
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
List all projects that the user has access to
|
|
|
|
Example:
|
|
```bash
|
|
curl --location 'http://0.0.0.0:4000/project/list' \\
|
|
--header 'Authorization: Bearer sk-1234'
|
|
```
|
|
"""
|
|
from litellm.proxy.proxy_server import prisma_client
|
|
|
|
try:
|
|
if prisma_client is None:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
|
)
|
|
|
|
# If proxy admin, get all projects
|
|
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN:
|
|
projects = await prisma_client.db.litellm_projecttable.find_many(
|
|
include={"litellm_budget_table": True, "object_permission": True}
|
|
)
|
|
else:
|
|
# Get projects for teams the user belongs to
|
|
user_teams = await prisma_client.db.litellm_teamtable.find_many(
|
|
where={
|
|
"OR": [
|
|
{"members": {"has": user_api_key_dict.user_id}},
|
|
{"admins": {"has": user_api_key_dict.user_id}},
|
|
]
|
|
}
|
|
)
|
|
|
|
team_ids = [team.team_id for team in user_teams]
|
|
|
|
projects = await prisma_client.db.litellm_projecttable.find_many(
|
|
where={"team_id": {"in": team_ids}},
|
|
include={"litellm_budget_table": True, "object_permission": True},
|
|
)
|
|
|
|
return projects
|
|
except Exception as e:
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.management_endpoints.project_endpoints.list_projects(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
raise handle_exception_on_proxy(e)
|