forked from phoenix/litellm-mirror
Merge pull request #2183 from BerriAI/litellm_team_rate_limits
fix(proxy_server.py): allow user to set team tpm/rpm limits/budget/models
This commit is contained in:
commit
61d69b1efa
14 changed files with 958 additions and 57 deletions
|
@ -119,6 +119,51 @@ curl --location 'http://0.0.0.0:8000/key/generate' \
|
||||||
--data '{"models": ["azure-models"], "user_id": "krrish3@berri.ai"}'
|
--data '{"models": ["azure-models"], "user_id": "krrish3@berri.ai"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="per-team" label="For Team">
|
||||||
|
You can:
|
||||||
|
- Add budgets to Teams
|
||||||
|
|
||||||
|
|
||||||
|
#### **Add budgets to users**
|
||||||
|
```shell
|
||||||
|
curl --location 'http://localhost:8000/team/new' \
|
||||||
|
--header 'Authorization: Bearer <your-master-key>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"team_alias": "my-new-team_4",
|
||||||
|
"members_with_roles": [{"role": "admin", "user_id": "5c4a0aa3-a1e1-43dc-bd87-3c2da8382a3a"}],
|
||||||
|
"rpm_limit": 99
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
[**See Swagger**](https://litellm-api.up.railway.app/#/team%20management/new_team_team_new_post)
|
||||||
|
|
||||||
|
**Sample Response**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
{
|
||||||
|
"team_alias": "my-new-team_4",
|
||||||
|
"team_id": "13e83b19-f851-43fe-8e93-f96e21033100",
|
||||||
|
"admins": [],
|
||||||
|
"members": [],
|
||||||
|
"members_with_roles": [
|
||||||
|
{
|
||||||
|
"role": "admin",
|
||||||
|
"user_id": "5c4a0aa3-a1e1-43dc-bd87-3c2da8382a3a"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {},
|
||||||
|
"tpm_limit": null,
|
||||||
|
"rpm_limit": 99,
|
||||||
|
"max_budget": null,
|
||||||
|
"models": [],
|
||||||
|
"spend": 0.0,
|
||||||
|
"max_parallel_requests": null,
|
||||||
|
"budget_duration": null,
|
||||||
|
"budget_reset_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="per-user-chat" label="For 'user' passed to /chat/completions">
|
<TabItem value="per-user-chat" label="For 'user' passed to /chat/completions">
|
||||||
|
|
||||||
|
|
|
@ -230,7 +230,14 @@ class UpdateUserRequest(GenerateRequestBase):
|
||||||
|
|
||||||
class Member(LiteLLMBase):
|
class Member(LiteLLMBase):
|
||||||
role: Literal["admin", "user"]
|
role: Literal["admin", "user"]
|
||||||
user_id: str
|
user_id: Optional[str] = None
|
||||||
|
user_email: Optional[str] = None
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def check_user_info(cls, values):
|
||||||
|
if values.get("user_id") is None and values.get("user_email") is None:
|
||||||
|
raise ValueError("Either user id or user email must be provided")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
class NewTeamRequest(LiteLLMBase):
|
class NewTeamRequest(LiteLLMBase):
|
||||||
|
@ -240,6 +247,15 @@ class NewTeamRequest(LiteLLMBase):
|
||||||
members: list = []
|
members: list = []
|
||||||
members_with_roles: List[Member] = []
|
members_with_roles: List[Member] = []
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
|
tpm_limit: Optional[int] = None
|
||||||
|
rpm_limit: Optional[int] = None
|
||||||
|
max_budget: Optional[float] = None
|
||||||
|
models: list = []
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberAddRequest(LiteLLMBase):
|
||||||
|
team_id: str
|
||||||
|
member: Optional[Member] = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateTeamRequest(LiteLLMBase):
|
class UpdateTeamRequest(LiteLLMBase):
|
||||||
|
@ -256,23 +272,29 @@ class DeleteTeamRequest(LiteLLMBase):
|
||||||
|
|
||||||
|
|
||||||
class LiteLLM_TeamTable(NewTeamRequest):
|
class LiteLLM_TeamTable(NewTeamRequest):
|
||||||
max_budget: Optional[float] = None
|
|
||||||
spend: Optional[float] = None
|
spend: Optional[float] = None
|
||||||
models: list = []
|
|
||||||
max_parallel_requests: Optional[int] = None
|
max_parallel_requests: Optional[int] = None
|
||||||
tpm_limit: Optional[int] = None
|
|
||||||
rpm_limit: Optional[int] = None
|
|
||||||
budget_duration: Optional[str] = None
|
budget_duration: Optional[str] = None
|
||||||
budget_reset_at: Optional[datetime] = None
|
budget_reset_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def set_model_info(cls, values):
|
||||||
|
dict_fields = [
|
||||||
|
"metadata",
|
||||||
|
"aliases",
|
||||||
|
"config",
|
||||||
|
"permissions",
|
||||||
|
"model_max_budget",
|
||||||
|
]
|
||||||
|
for field in dict_fields:
|
||||||
|
value = values.get(field)
|
||||||
|
if value is not None and isinstance(value, str):
|
||||||
|
try:
|
||||||
|
values[field] = json.loads(value)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValueError(f"Field {field} should be a valid dictionary")
|
||||||
|
|
||||||
class NewTeamResponse(LiteLLMBase):
|
return values
|
||||||
team_id: str
|
|
||||||
admins: list
|
|
||||||
members: list
|
|
||||||
metadata: dict
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class TeamRequest(LiteLLMBase):
|
class TeamRequest(LiteLLMBase):
|
||||||
|
|
|
@ -4063,6 +4063,7 @@ async def user_info(
|
||||||
default=False,
|
default=False,
|
||||||
description="set to true to View all users. When using view_all, don't pass user_id",
|
description="set to true to View all users. When using view_all, don't pass user_id",
|
||||||
),
|
),
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Use this to get user information. (user row + all user key info)
|
Use this to get user information. (user row + all user key info)
|
||||||
|
@ -4108,6 +4109,22 @@ async def user_info(
|
||||||
team_id_list=user_info.teams, table_name="team", query_type="find_all"
|
team_id_list=user_info.teams, table_name="team", query_type="find_all"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if teams_2 is not None and isinstance(teams_2, list):
|
||||||
|
for team in teams_2:
|
||||||
|
if team.team_id not in team_id_list:
|
||||||
|
team_list.append(team)
|
||||||
|
team_id_list.append(team.team_id)
|
||||||
|
elif user_api_key_dict.user_id is not None:
|
||||||
|
caller_user_info = await prisma_client.get_data(
|
||||||
|
user_id=user_api_key_dict.user_id
|
||||||
|
)
|
||||||
|
# *NEW* get all teams in user 'teams' field
|
||||||
|
teams_2 = await prisma_client.get_data(
|
||||||
|
team_id_list=caller_user_info.teams,
|
||||||
|
table_name="team",
|
||||||
|
query_type="find_all",
|
||||||
|
)
|
||||||
|
|
||||||
if teams_2 is not None and isinstance(teams_2, list):
|
if teams_2 is not None and isinstance(teams_2, list):
|
||||||
for team in teams_2:
|
for team in teams_2:
|
||||||
if team.team_id not in team_id_list:
|
if team.team_id not in team_id_list:
|
||||||
|
@ -4137,12 +4154,14 @@ async def user_info(
|
||||||
# if using pydantic v1
|
# if using pydantic v1
|
||||||
key = key.dict()
|
key = key.dict()
|
||||||
key.pop("token", None)
|
key.pop("token", None)
|
||||||
return {
|
|
||||||
|
response_data = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"user_info": user_info,
|
"user_info": user_info,
|
||||||
"keys": keys,
|
"keys": keys,
|
||||||
"teams": team_list,
|
"teams": team_list,
|
||||||
}
|
}
|
||||||
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
|
@ -4401,7 +4420,7 @@ async def unblock_user(data: BlockUsers):
|
||||||
"/team/new",
|
"/team/new",
|
||||||
tags=["team management"],
|
tags=["team management"],
|
||||||
dependencies=[Depends(user_api_key_auth)],
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
response_model=NewTeamResponse,
|
response_model=LiteLLM_TeamTable,
|
||||||
)
|
)
|
||||||
async def new_team(
|
async def new_team(
|
||||||
data: NewTeamRequest,
|
data: NewTeamRequest,
|
||||||
|
@ -4447,13 +4466,66 @@ async def new_team(
|
||||||
if data.team_id is None:
|
if data.team_id is None:
|
||||||
data.team_id = str(uuid.uuid4())
|
data.team_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.tpm_limit is not None
|
||||||
|
and user_api_key_dict.tpm_limit is not None
|
||||||
|
and data.tpm_limit > user_api_key_dict.tpm_limit
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": f"tpm limit higher than user max. User tpm limit={user_api_key_dict.tpm_limit}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.rpm_limit is not None
|
||||||
|
and user_api_key_dict.rpm_limit is not None
|
||||||
|
and data.rpm_limit > user_api_key_dict.rpm_limit
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": f"rpm limit higher than user max. User rpm limit={user_api_key_dict.rpm_limit}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.max_budget is not None
|
||||||
|
and user_api_key_dict.max_budget is not None
|
||||||
|
and data.max_budget > user_api_key_dict.max_budget
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": f"max budget higher than user max. User max budget={user_api_key_dict.max_budget}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.models is not None:
|
||||||
|
for m in data.models:
|
||||||
|
if m not in user_api_key_dict.models:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": f"Model not in allowed user models. User allowed models={user_api_key_dict.models}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_api_key_dict.user_id is not None:
|
||||||
|
creating_user_in_list = False
|
||||||
|
for member in data.members_with_roles:
|
||||||
|
if member.user_id == user_api_key_dict.user_id:
|
||||||
|
creating_user_in_list = True
|
||||||
|
|
||||||
|
if creating_user_in_list == False:
|
||||||
|
data.members_with_roles.append(
|
||||||
|
Member(role="admin", user_id=user_api_key_dict.user_id)
|
||||||
|
)
|
||||||
|
|
||||||
complete_team_data = LiteLLM_TeamTable(
|
complete_team_data = LiteLLM_TeamTable(
|
||||||
**data.json(),
|
**data.json(),
|
||||||
max_budget=user_api_key_dict.max_budget,
|
|
||||||
models=user_api_key_dict.models,
|
|
||||||
max_parallel_requests=user_api_key_dict.max_parallel_requests,
|
max_parallel_requests=user_api_key_dict.max_parallel_requests,
|
||||||
tpm_limit=user_api_key_dict.tpm_limit,
|
|
||||||
rpm_limit=user_api_key_dict.rpm_limit,
|
|
||||||
budget_duration=user_api_key_dict.budget_duration,
|
budget_duration=user_api_key_dict.budget_duration,
|
||||||
budget_reset_at=user_api_key_dict.budget_reset_at,
|
budget_reset_at=user_api_key_dict.budget_reset_at,
|
||||||
)
|
)
|
||||||
|
@ -4468,13 +4540,13 @@ async def new_team(
|
||||||
await prisma_client.update_data(
|
await prisma_client.update_data(
|
||||||
user_id=user.user_id,
|
user_id=user.user_id,
|
||||||
data={"user_id": user.user_id, "teams": [team_row.team_id]},
|
data={"user_id": user.user_id, "teams": [team_row.team_id]},
|
||||||
update_key_values={
|
update_key_values_custom_query={
|
||||||
"teams": {
|
"teams": {
|
||||||
"push ": [team_row.team_id],
|
"push ": [team_row.team_id],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return team_row
|
return team_row.model_dump()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
@ -4485,6 +4557,9 @@ async def update_team(
|
||||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
[BETA]
|
||||||
|
[DEPRECATED] - use the `/team/member_add` and `/team/member_remove` endpoints instead
|
||||||
|
|
||||||
You can now add / delete users from a team via /team/update
|
You can now add / delete users from a team via /team/update
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -4524,6 +4599,7 @@ async def update_team(
|
||||||
existing_user_id_list = []
|
existing_user_id_list = []
|
||||||
## Get new users
|
## Get new users
|
||||||
for user in existing_team_row.members_with_roles:
|
for user in existing_team_row.members_with_roles:
|
||||||
|
if user["user_id"] is not None:
|
||||||
existing_user_id_list.append(user["user_id"])
|
existing_user_id_list.append(user["user_id"])
|
||||||
|
|
||||||
## Update new user rows with team id (info used by /user/info to show all teams, user is a part of)
|
## Update new user rows with team id (info used by /user/info to show all teams, user is a part of)
|
||||||
|
@ -4532,26 +4608,32 @@ async def update_team(
|
||||||
if user.user_id not in existing_user_id_list:
|
if user.user_id not in existing_user_id_list:
|
||||||
await prisma_client.update_data(
|
await prisma_client.update_data(
|
||||||
user_id=user.user_id,
|
user_id=user.user_id,
|
||||||
data={"user_id": user.user_id, "teams": [team_row["team_id"]]},
|
data={
|
||||||
update_key_values={
|
"user_id": user.user_id,
|
||||||
|
"teams": [team_row["team_id"]],
|
||||||
|
"models": team_row["data"].models,
|
||||||
|
},
|
||||||
|
update_key_values_custom_query={
|
||||||
"teams": {
|
"teams": {
|
||||||
"push": [team_row["team_id"]],
|
"push": [team_row["team_id"]],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
table_name="user",
|
||||||
)
|
)
|
||||||
|
|
||||||
## REMOVE DELETED USERS ##
|
## REMOVE DELETED USERS ##
|
||||||
### Get list of deleted users (old list - new list)
|
### Get list of deleted users (old list - new list)
|
||||||
deleted_user_id_list = []
|
deleted_user_id_list = []
|
||||||
existing_user_id_list = []
|
new_user_id_list = []
|
||||||
## Get old user list
|
## Get old user list
|
||||||
for user in existing_team_row.members_with_roles:
|
|
||||||
existing_user_id_list.append(user["user_id"])
|
|
||||||
## Get diff
|
|
||||||
if data.members_with_roles is not None:
|
if data.members_with_roles is not None:
|
||||||
for user in data.members_with_roles:
|
for user in data.members_with_roles:
|
||||||
if user.user_id not in existing_user_id_list:
|
new_user_id_list.append(user.user_id)
|
||||||
deleted_user_id_list.append(user.user_id)
|
## Get diff
|
||||||
|
if existing_team_row.members_with_roles is not None:
|
||||||
|
for user in existing_team_row.members_with_roles:
|
||||||
|
if user["user_id"] not in new_user_id_list:
|
||||||
|
deleted_user_id_list.append(user["user_id"])
|
||||||
|
|
||||||
## SET UPDATED LIST
|
## SET UPDATED LIST
|
||||||
if len(deleted_user_id_list) > 0:
|
if len(deleted_user_id_list) > 0:
|
||||||
|
@ -4570,6 +4652,99 @@ async def update_team(
|
||||||
return team_row
|
return team_row
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/team/member_add",
|
||||||
|
tags=["team management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
async def team_member_add(
|
||||||
|
data: TeamMemberAddRequest,
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
[BETA]
|
||||||
|
|
||||||
|
Add new members (either via user_email or user_id) to a team
|
||||||
|
|
||||||
|
If user doesn't exist, new user row will also be added to User Table
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'http://0.0.0.0:8000/team/update' \
|
||||||
|
|
||||||
|
-H 'Authorization: Bearer sk-1234' \
|
||||||
|
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
|
||||||
|
-D '{
|
||||||
|
"team_id": "45e3e396-ee08-4a61-a88e-16b3ce7e0849",
|
||||||
|
"member": {"role": "user", "user_id": "krrish247652@berri.ai"}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||||
|
|
||||||
|
if data.team_id is None:
|
||||||
|
raise HTTPException(status_code=400, detail={"error": "No team id passed in"})
|
||||||
|
|
||||||
|
if data.member is None:
|
||||||
|
raise HTTPException(status_code=400, detail={"error": "No member passed in"})
|
||||||
|
|
||||||
|
existing_team_row = await prisma_client.get_data( # type: ignore
|
||||||
|
team_id=data.team_id, table_name="team", query_type="find_unique"
|
||||||
|
)
|
||||||
|
|
||||||
|
new_member = data.member
|
||||||
|
|
||||||
|
existing_team_row.members_with_roles.append(new_member)
|
||||||
|
|
||||||
|
complete_team_data = LiteLLM_TeamTable(
|
||||||
|
**existing_team_row.model_dump(),
|
||||||
|
)
|
||||||
|
|
||||||
|
team_row = await prisma_client.update_data(
|
||||||
|
update_key_values=complete_team_data.json(exclude_none=True),
|
||||||
|
data=complete_team_data.json(exclude_none=True),
|
||||||
|
table_name="team",
|
||||||
|
team_id=data.team_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
## ADD USER, IF NEW ##
|
||||||
|
user_data = { # type: ignore
|
||||||
|
"teams": [team_row["team_id"]],
|
||||||
|
"models": team_row["data"].models,
|
||||||
|
}
|
||||||
|
if new_member.user_id is not None:
|
||||||
|
user_data["user_id"] = new_member.user_id # type: ignore
|
||||||
|
await prisma_client.update_data(
|
||||||
|
user_id=new_member.user_id,
|
||||||
|
data=user_data,
|
||||||
|
update_key_values_custom_query={
|
||||||
|
"teams": {
|
||||||
|
"push": [team_row["team_id"]],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
table_name="user",
|
||||||
|
)
|
||||||
|
elif new_member.user_email is not None:
|
||||||
|
user_data["user_id"] = str(uuid.uuid4())
|
||||||
|
user_data["user_email"] = new_member.user_email
|
||||||
|
## user email is not unique acc. to prisma schema -> future improvement
|
||||||
|
### for now: check if it exists in db, if not - insert it
|
||||||
|
existing_user_row = await prisma_client.get_data(
|
||||||
|
key_val={"user_email": new_member.user_email},
|
||||||
|
table_name="user",
|
||||||
|
query_type="find_all",
|
||||||
|
)
|
||||||
|
if existing_user_row is None or (
|
||||||
|
isinstance(existing_user_row, list) and len(existing_user_row) == 0
|
||||||
|
):
|
||||||
|
|
||||||
|
await prisma_client.insert_data(data=user_data, table_name="user")
|
||||||
|
|
||||||
|
return team_row
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/team/delete", tags=["team management"], dependencies=[Depends(user_api_key_auth)]
|
"/team/delete", tags=["team management"], dependencies=[Depends(user_api_key_auth)]
|
||||||
)
|
)
|
||||||
|
|
|
@ -635,11 +635,15 @@ class PrismaClient:
|
||||||
table_name is not None and table_name == "user"
|
table_name is not None and table_name == "user"
|
||||||
):
|
):
|
||||||
if query_type == "find_unique":
|
if query_type == "find_unique":
|
||||||
|
if key_val is None:
|
||||||
|
key_val = {"user_id": user_id}
|
||||||
response = await self.db.litellm_usertable.find_unique( # type: ignore
|
response = await self.db.litellm_usertable.find_unique( # type: ignore
|
||||||
where={
|
where=key_val # type: ignore
|
||||||
"user_id": user_id, # type: ignore
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
elif query_type == "find_all" and key_val is not None:
|
||||||
|
response = await self.db.litellm_usertable.find_many(
|
||||||
|
where=key_val # type: ignore
|
||||||
|
) # type: ignore
|
||||||
elif query_type == "find_all" and reset_at is not None:
|
elif query_type == "find_all" and reset_at is not None:
|
||||||
response = await self.db.litellm_usertable.find_many(
|
response = await self.db.litellm_usertable.find_many(
|
||||||
where={ # type:ignore
|
where={ # type:ignore
|
||||||
|
@ -869,12 +873,15 @@ class PrismaClient:
|
||||||
query_type: Literal["update", "update_many"] = "update",
|
query_type: Literal["update", "update_many"] = "update",
|
||||||
table_name: Optional[Literal["user", "key", "config", "spend", "team"]] = None,
|
table_name: Optional[Literal["user", "key", "config", "spend", "team"]] = None,
|
||||||
update_key_values: Optional[dict] = None,
|
update_key_values: Optional[dict] = None,
|
||||||
|
update_key_values_custom_query: Optional[dict] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update existing data
|
Update existing data
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
db_data = self.jsonify_object(data=data)
|
db_data = self.jsonify_object(data=data)
|
||||||
|
if update_key_values is not None:
|
||||||
|
update_key_values = self.jsonify_object(data=update_key_values)
|
||||||
if token is not None:
|
if token is not None:
|
||||||
print_verbose(f"token: {token}")
|
print_verbose(f"token: {token}")
|
||||||
# check if plain text or hash
|
# check if plain text or hash
|
||||||
|
@ -902,6 +909,9 @@ class PrismaClient:
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
user_id = db_data["user_id"]
|
user_id = db_data["user_id"]
|
||||||
if update_key_values is None:
|
if update_key_values is None:
|
||||||
|
if update_key_values_custom_query is not None:
|
||||||
|
update_key_values = update_key_values_custom_query
|
||||||
|
else:
|
||||||
update_key_values = db_data
|
update_key_values = db_data
|
||||||
update_user_row = await self.db.litellm_usertable.upsert(
|
update_user_row = await self.db.litellm_usertable.upsert(
|
||||||
where={"user_id": user_id}, # type: ignore
|
where={"user_id": user_id}, # type: ignore
|
||||||
|
@ -944,7 +954,6 @@ class PrismaClient:
|
||||||
update_key_values["members_with_roles"] = json.dumps(
|
update_key_values["members_with_roles"] = json.dumps(
|
||||||
update_key_values["members_with_roles"]
|
update_key_values["members_with_roles"]
|
||||||
)
|
)
|
||||||
|
|
||||||
update_team_row = await self.db.litellm_teamtable.upsert(
|
update_team_row = await self.db.litellm_teamtable.upsert(
|
||||||
where={"team_id": team_id}, # type: ignore
|
where={"team_id": team_id}, # type: ignore
|
||||||
data={
|
data={
|
||||||
|
|
|
@ -5,14 +5,16 @@ import Navbar from "../components/navbar";
|
||||||
import UserDashboard from "../components/user_dashboard";
|
import UserDashboard from "../components/user_dashboard";
|
||||||
import ModelDashboard from "@/components/model_dashboard";
|
import ModelDashboard from "@/components/model_dashboard";
|
||||||
import ViewUserDashboard from "@/components/view_users";
|
import ViewUserDashboard from "@/components/view_users";
|
||||||
|
import Teams from "@/components/teams";
|
||||||
import ChatUI from "@/components/chat_ui";
|
import ChatUI from "@/components/chat_ui";
|
||||||
import Sidebar from "../components/leftnav";
|
import Sidebar from "../components/leftnav";
|
||||||
import Usage from "../components/usage";
|
import Usage from "../components/usage";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
|
||||||
const CreateKeyPage = () => {
|
const CreateKeyPage = () => {
|
||||||
const [userRole, setUserRole] = useState('');
|
const [userRole, setUserRole] = useState("");
|
||||||
const [userEmail, setUserEmail] = useState<null | string>(null);
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
|
const [teams, setTeams] = useState<null | any[]>(null);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const userID = searchParams.get("userID");
|
const userID = searchParams.get("userID");
|
||||||
|
@ -74,14 +76,20 @@ const CreateKeyPage = () => {
|
||||||
<div className="flex flex-col min-h-screen">
|
<div className="flex flex-col min-h-screen">
|
||||||
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
<div className="flex flex-1 overflow-auto">
|
<div className="flex flex-1 overflow-auto">
|
||||||
<Sidebar setPage={setPage} userRole={userRole}/>
|
<Sidebar
|
||||||
|
setPage={setPage}
|
||||||
|
userRole={userRole}
|
||||||
|
defaultSelectedKey={null}
|
||||||
|
/>
|
||||||
{page == "api-keys" ? (
|
{page == "api-keys" ? (
|
||||||
<UserDashboard
|
<UserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
|
teams={teams}
|
||||||
setUserRole={setUserRole}
|
setUserRole={setUserRole}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
setUserEmail={setUserEmail}
|
setUserEmail={setUserEmail}
|
||||||
|
setTeams={setTeams}
|
||||||
/>
|
/>
|
||||||
) : page == "models" ? (
|
) : page == "models" ? (
|
||||||
<ModelDashboard
|
<ModelDashboard
|
||||||
|
@ -97,16 +105,21 @@ const CreateKeyPage = () => {
|
||||||
token={token}
|
token={token}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
)
|
) : page == "users" ? (
|
||||||
: page == "users" ? (
|
|
||||||
<ViewUserDashboard
|
<ViewUserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
token={token}
|
token={token}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
)
|
) : page == "teams" ? (
|
||||||
: (
|
<Teams
|
||||||
|
teams={teams}
|
||||||
|
setTeams={setTeams}
|
||||||
|
searchParams={searchParams}
|
||||||
|
accessToken={accessToken}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Usage
|
<Usage
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
|
|
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
149
ui/litellm-dashboard/src/app/team/page.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"use client";
|
||||||
|
import React, { Suspense, useEffect, useState } from "react";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import Navbar from "../../components/navbar";
|
||||||
|
import Sidebar from "../../components/leftnav";
|
||||||
|
import { jwtDecode } from "jwt-decode";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Card,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { CogIcon } from "@heroicons/react/outline";
|
||||||
|
|
||||||
|
const TeamSettingsPage = () => {
|
||||||
|
const [userRole, setUserRole] = useState("");
|
||||||
|
const [userEmail, setUserEmail] = useState<null | string>(null);
|
||||||
|
const [teams, setTeams] = useState<null | string[]>(null);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const userID = searchParams.get("userID");
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
const [page, setPage] = useState("team");
|
||||||
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
const decoded = jwtDecode(token) as { [key: string]: any };
|
||||||
|
if (decoded) {
|
||||||
|
// cast decoded to dictionary
|
||||||
|
console.log("Decoded token:", decoded);
|
||||||
|
|
||||||
|
console.log("Decoded key:", decoded.key);
|
||||||
|
// set accessToken
|
||||||
|
setAccessToken(decoded.key);
|
||||||
|
|
||||||
|
// check if userRole is defined
|
||||||
|
if (decoded.user_role) {
|
||||||
|
const formattedUserRole = formatUserRole(decoded.user_role);
|
||||||
|
console.log("Decoded user_role:", formattedUserRole);
|
||||||
|
setUserRole(formattedUserRole);
|
||||||
|
} else {
|
||||||
|
console.log("User role not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded.user_email) {
|
||||||
|
setUserEmail(decoded.user_email);
|
||||||
|
} else {
|
||||||
|
console.log(`User Email is not set ${decoded}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
function formatUserRole(userRole: string) {
|
||||||
|
if (!userRole) {
|
||||||
|
return "Undefined Role";
|
||||||
|
}
|
||||||
|
console.log(`Received user role: ${userRole}`);
|
||||||
|
switch (userRole.toLowerCase()) {
|
||||||
|
case "app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "demo_app_owner":
|
||||||
|
return "App Owner";
|
||||||
|
case "app_admin":
|
||||||
|
return "Admin";
|
||||||
|
case "app_user":
|
||||||
|
return "App User";
|
||||||
|
default:
|
||||||
|
return "Unknown Role";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div>Loading...</div>}>
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar userID={userID} userRole={userRole} userEmail={userEmail} />
|
||||||
|
<div className="flex flex-1 overflow-auto">
|
||||||
|
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Settings</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Wilhelm Tell</TableCell>
|
||||||
|
<TableCell className="text-right">1</TableCell>
|
||||||
|
<TableCell>Uri, Schwyz, Unterwalden</TableCell>
|
||||||
|
<TableCell>National Hero</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>The Witcher</TableCell>
|
||||||
|
<TableCell className="text-right">129</TableCell>
|
||||||
|
<TableCell>Kaedwen</TableCell>
|
||||||
|
<TableCell>Legend</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Mizutsune</TableCell>
|
||||||
|
<TableCell className="text-right">82</TableCell>
|
||||||
|
<TableCell>Japan</TableCell>
|
||||||
|
<TableCell>N/A</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Link
|
||||||
|
href={`/team?userID=${searchParams.get(
|
||||||
|
"userID"
|
||||||
|
)}&token=${searchParams.get("token")}`}
|
||||||
|
>
|
||||||
|
<Button className="mx-auto">+ Create New Team</Button>
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default TeamSettingsPage;
|
|
@ -18,6 +18,7 @@ const { Option } = Select;
|
||||||
|
|
||||||
interface CreateKeyProps {
|
interface CreateKeyProps {
|
||||||
userID: string;
|
userID: string;
|
||||||
|
teamID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
data: any[] | null;
|
data: any[] | null;
|
||||||
|
@ -27,6 +28,7 @@ interface CreateKeyProps {
|
||||||
|
|
||||||
const CreateKey: React.FC<CreateKeyProps> = ({
|
const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
userID,
|
userID,
|
||||||
|
teamID,
|
||||||
userRole,
|
userRole,
|
||||||
accessToken,
|
accessToken,
|
||||||
data,
|
data,
|
||||||
|
@ -36,7 +38,6 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [apiKey, setApiKey] = useState(null);
|
const [apiKey, setApiKey] = useState(null);
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
@ -89,7 +90,10 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Team ID" name="team_id">
|
<Form.Item label="Team ID" name="team_id">
|
||||||
<Input placeholder="ai_team" />
|
<Input
|
||||||
|
placeholder="ai_team"
|
||||||
|
defaultValue={teamID ? teamID : ""}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Models" name="models">
|
<Form.Item label="Models" name="models">
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { Typography } from "antd";
|
||||||
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
|
|
||||||
|
interface DashboardTeamProps {
|
||||||
|
teams: Object[] | null;
|
||||||
|
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
||||||
|
teams,
|
||||||
|
setSelectedTeam,
|
||||||
|
}) => {
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-10">
|
||||||
|
<Title level={4}>Default Team</Title>
|
||||||
|
<Paragraph>
|
||||||
|
If you belong to multiple teams, this setting controls which team is
|
||||||
|
used by default when creating new API Keys.
|
||||||
|
</Paragraph>
|
||||||
|
{teams && teams.length > 0 ? (
|
||||||
|
<Select defaultValue="0">
|
||||||
|
{teams.map((team: any, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(index)}
|
||||||
|
onClick={() => setSelectedTeam(team)}
|
||||||
|
>
|
||||||
|
{team["team_alias"]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Paragraph>
|
||||||
|
No team created. <b>Defaulting to personal account.</b>
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardTeam;
|
|
@ -1,5 +1,6 @@
|
||||||
import { Layout, Menu } from "antd";
|
import { Layout, Menu } from "antd";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { List } from "postcss/lib/list";
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
@ -7,15 +8,20 @@ const { Sider } = Layout;
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
setPage: React.Dispatch<React.SetStateAction<string>>;
|
setPage: React.Dispatch<React.SetStateAction<string>>;
|
||||||
userRole: string;
|
userRole: string;
|
||||||
|
defaultSelectedKey: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
|
setPage,
|
||||||
|
userRole,
|
||||||
|
defaultSelectedKey,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
<Layout style={{ minHeight: "100vh", maxWidth: "120px" }}>
|
||||||
<Sider width={120}>
|
<Sider width={120}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
defaultSelectedKeys={["1"]}
|
defaultSelectedKeys={defaultSelectedKey ? defaultSelectedKey : ["1"]}
|
||||||
style={{ height: "100%", borderRight: 0 }}
|
style={{ height: "100%", borderRight: 0 }}
|
||||||
>
|
>
|
||||||
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
||||||
|
@ -30,13 +36,16 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, userRole }) => {
|
||||||
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
||||||
Usage
|
Usage
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{
|
{userRole == "Admin" ? (
|
||||||
userRole == "Admin" ?
|
|
||||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||||
Users
|
Users
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
: null
|
) : null}
|
||||||
}
|
{userRole == "Admin" ? (
|
||||||
|
<Menu.Item key="6" onClick={() => setPage("teams")}>
|
||||||
|
Teams
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -29,14 +29,19 @@ const Navbar: React.FC<NavbarProps> = ({ userID, userRole, userEmail }) => {
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
const isLocal = process.env.NODE_ENV === "development";
|
||||||
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
const imageUrl = isLocal ? "http://localhost:4000/get_image" : "/get_image";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
|
<nav className="left-0 right-0 top-0 flex justify-between items-center h-12 mb-4">
|
||||||
<div className="text-left mx-4 my-2 absolute top-0 left-0">
|
<div className="text-left my-2 absolute top-0 left-0">
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-gray-800 text-2xl px-4 py-1 rounded text-center">
|
<button className="text-gray-800 text-2xl py-1 rounded text-center">
|
||||||
<img src={imageUrl} width={200} height={200} alt="LiteLLM Brand" className="mr-2" />
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
alt="LiteLLM Brand"
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -196,6 +196,7 @@ export const userInfoCall = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
console.log("API Response:", data);
|
||||||
message.info("Received user data");
|
message.info("Received user data");
|
||||||
return data;
|
return data;
|
||||||
// Handle success - you might want to update some state or UI based on the created key
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
@ -466,3 +467,85 @@ export const userGetRequesedtModelsCall = async (accessToken: String) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const teamCreateCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
formValues: Record<string, any> // Assuming formValues is an object
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("Form Values in teamCreateCall:", formValues); // Log the form values before making the API call
|
||||||
|
|
||||||
|
const url = proxyBaseUrl ? `${proxyBaseUrl}/team/new` : `/team/new`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...formValues, // Include formValues in the request body
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
message.error("Failed to create key: " + errorData);
|
||||||
|
console.error("Error response from the server:", errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("API Response:", data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Member {
|
||||||
|
role: string;
|
||||||
|
user_id: string | null;
|
||||||
|
user_email: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const teamMemberAddCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
teamId: string,
|
||||||
|
formValues: Member // Assuming formValues is an object
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("Form Values in teamMemberAddCall:", formValues); // Log the form values before making the API call
|
||||||
|
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/team/member_add`
|
||||||
|
: `/team/member_add`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
team_id: teamId,
|
||||||
|
member: formValues, // Include formValues in the request body
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
message.error("Failed to create key: " + errorData);
|
||||||
|
console.error("Error response from the server:", errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("API Response:", data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
328
ui/litellm-dashboard/src/components/teams.tsx
Normal file
328
ui/litellm-dashboard/src/components/teams.tsx
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Typography } from "antd";
|
||||||
|
import {
|
||||||
|
Button as Button2,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select as Select2,
|
||||||
|
InputNumber,
|
||||||
|
message,
|
||||||
|
} from "antd";
|
||||||
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Card,
|
||||||
|
Icon,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Text,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { CogIcon } from "@heroicons/react/outline";
|
||||||
|
interface TeamProps {
|
||||||
|
teams: any[] | null;
|
||||||
|
searchParams: any;
|
||||||
|
accessToken: string | null;
|
||||||
|
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||||
|
}
|
||||||
|
import { teamCreateCall, teamMemberAddCall, Member } from "./networking";
|
||||||
|
|
||||||
|
const Team: React.FC<TeamProps> = ({
|
||||||
|
teams,
|
||||||
|
searchParams,
|
||||||
|
accessToken,
|
||||||
|
setTeams,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [memberForm] = Form.useForm();
|
||||||
|
const { Title, Paragraph } = Typography;
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState<null | any>(
|
||||||
|
teams ? teams[0] : null
|
||||||
|
);
|
||||||
|
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||||
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||||
|
const handleOk = () => {
|
||||||
|
setIsTeamModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberOk = () => {
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
memberForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsTeamModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberCancel = () => {
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
memberForm.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (formValues: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
if (accessToken != null) {
|
||||||
|
message.info("Making API Call");
|
||||||
|
const response: any = await teamCreateCall(accessToken, formValues);
|
||||||
|
if (teams !== null) {
|
||||||
|
setTeams([...teams, response]);
|
||||||
|
} else {
|
||||||
|
setTeams([response]);
|
||||||
|
}
|
||||||
|
console.log(`response for team create call: ${response}`);
|
||||||
|
setIsTeamModalVisible(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating the key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
if (accessToken != null && teams != null) {
|
||||||
|
message.info("Making API Call");
|
||||||
|
const user_role: Member = {
|
||||||
|
role: "user",
|
||||||
|
user_email: formValues.user_email,
|
||||||
|
user_id: null,
|
||||||
|
};
|
||||||
|
const response: any = await teamMemberAddCall(
|
||||||
|
accessToken,
|
||||||
|
selectedTeam["team_id"],
|
||||||
|
user_role
|
||||||
|
);
|
||||||
|
console.log(`response for team create call: ${response["data"]}`);
|
||||||
|
// Checking if the team exists in the list and updating or adding accordingly
|
||||||
|
const foundIndex = teams.findIndex((team) => {
|
||||||
|
console.log(
|
||||||
|
`team.team_id=${team.team_id}; response.data.team_id=${response.data.team_id}`
|
||||||
|
);
|
||||||
|
return team.team_id === response.data.team_id;
|
||||||
|
});
|
||||||
|
console.log(`foundIndex: ${foundIndex}`);
|
||||||
|
if (foundIndex !== -1) {
|
||||||
|
// If the team is found, update it
|
||||||
|
const updatedTeams = [...teams]; // Copy the current state
|
||||||
|
updatedTeams[foundIndex] = response.data; // Update the specific team
|
||||||
|
setTeams(updatedTeams); // Set the new state
|
||||||
|
setSelectedTeam(response.data);
|
||||||
|
}
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating the key:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
console.log(`received teams ${teams}`);
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Grid numItems={1} className="gap-2 p-2 h-[75vh] w-full">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Title level={4}>All Teams</Title>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{teams && teams.length > 0
|
||||||
|
? teams.map((team: any) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{team["team_alias"]}</TableCell>
|
||||||
|
<TableCell>{team["spend"]}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{team["max_budget"] ? team["max_budget"] : "No limit"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>
|
||||||
|
TPM Limit:{" "}
|
||||||
|
{team.tpm_limit ? team.tpm_limit : "Unlimited"}{" "}
|
||||||
|
<br></br> RPM Limit:{" "}
|
||||||
|
{team.rpm_limit ? team.rpm_limit : "Unlimited"}
|
||||||
|
</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Button
|
||||||
|
className="mx-auto"
|
||||||
|
onClick={() => setIsTeamModalVisible(true)}
|
||||||
|
>
|
||||||
|
+ Create New Team
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Create Team"
|
||||||
|
visible={isTeamModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleCreate}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Form.Item label="Team Name" name="team_alias">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Models" name="models">
|
||||||
|
<Select2
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Select models"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{/* {userModels.map((model) => (
|
||||||
|
<Option key={model} value={model}>
|
||||||
|
{model}
|
||||||
|
</Option>
|
||||||
|
))} */}
|
||||||
|
</Select2>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||||
|
<InputNumber step={0.01} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Tokens per minute Limit (TPM)"
|
||||||
|
name="tpm_limit"
|
||||||
|
>
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Requests per minute Limit (RPM)"
|
||||||
|
name="rpm_limit"
|
||||||
|
>
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button2 htmlType="submit">Create Team</Button2>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Title level={4}>Team Members</Title>
|
||||||
|
<Paragraph>
|
||||||
|
If you belong to multiple teams, this setting controls which teams'
|
||||||
|
members you see.
|
||||||
|
</Paragraph>
|
||||||
|
{teams && teams.length > 0 ? (
|
||||||
|
<Select defaultValue="0">
|
||||||
|
{teams.map((team: any, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={String(index)}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedTeam(team);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{team["team_alias"]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Paragraph>
|
||||||
|
No team created. <b>Defaulting to personal account.</b>
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Member Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Role</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Action</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{selectedTeam
|
||||||
|
? selectedTeam["members_with_roles"].map((member: any) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
{member["user_email"]
|
||||||
|
? member["user_email"]
|
||||||
|
: member["user_id"]
|
||||||
|
? member["user_id"]
|
||||||
|
: null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{member["role"]}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Icon icon={CogIcon} size="sm" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Button
|
||||||
|
className="mx-auto mb-5"
|
||||||
|
onClick={() => setIsAddMemberModalVisible(true)}
|
||||||
|
>
|
||||||
|
+ Add member
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
title="Add member"
|
||||||
|
visible={isAddMemberModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleMemberOk}
|
||||||
|
onCancel={handleMemberCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleMemberCreate}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Form.Item label="Email" name="user_email">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button2 htmlType="submit">Add member</Button2>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Team;
|
|
@ -5,6 +5,7 @@ import { Grid, Col, Card, Text } from "@tremor/react";
|
||||||
import CreateKey from "./create_key_button";
|
import CreateKey from "./create_key_button";
|
||||||
import ViewKeyTable from "./view_key_table";
|
import ViewKeyTable from "./view_key_table";
|
||||||
import ViewUserSpend from "./view_user_spend";
|
import ViewUserSpend from "./view_user_spend";
|
||||||
|
import DashboardTeam from "./dashboard_default_team";
|
||||||
import EnterProxyUrl from "./enter_proxy_url";
|
import EnterProxyUrl from "./enter_proxy_url";
|
||||||
import { message } from "antd";
|
import { message } from "antd";
|
||||||
import Navbar from "./navbar";
|
import Navbar from "./navbar";
|
||||||
|
@ -24,16 +25,20 @@ interface UserDashboardProps {
|
||||||
userID: string | null;
|
userID: string | null;
|
||||||
userRole: string | null;
|
userRole: string | null;
|
||||||
userEmail: string | null;
|
userEmail: string | null;
|
||||||
|
teams: any[] | null;
|
||||||
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
setUserRole: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
setUserEmail: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
|
setTeams: React.Dispatch<React.SetStateAction<Object[] | null>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserDashboard: React.FC<UserDashboardProps> = ({
|
const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
userID,
|
userID,
|
||||||
userRole,
|
userRole,
|
||||||
|
teams,
|
||||||
setUserRole,
|
setUserRole,
|
||||||
userEmail,
|
userEmail,
|
||||||
setUserEmail,
|
setUserEmail,
|
||||||
|
setTeams,
|
||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
const [data, setData] = useState<null | any[]>(null); // Keep the initialization of state here
|
||||||
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
const [userSpendData, setUserSpendData] = useState<UserSpendData | null>(
|
||||||
|
@ -48,6 +53,9 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
const token = searchParams.get("token");
|
const token = searchParams.get("token");
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||||
const [userModels, setUserModels] = useState<string[]>([]);
|
const [userModels, setUserModels] = useState<string[]>([]);
|
||||||
|
const [selectedTeam, setSelectedTeam] = useState<any | null>(
|
||||||
|
teams ? teams[0] : null
|
||||||
|
);
|
||||||
// check if window is not undefined
|
// check if window is not undefined
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.addEventListener("beforeunload", function () {
|
window.addEventListener("beforeunload", function () {
|
||||||
|
@ -111,8 +119,14 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await userInfoCall(accessToken, userID, userRole);
|
const response = await userInfoCall(accessToken, userID, userRole);
|
||||||
|
console.log(
|
||||||
|
`received teams in user dashboard: ${Object.keys(
|
||||||
|
response
|
||||||
|
)}; team values: ${Object.entries(response.teams)}`
|
||||||
|
);
|
||||||
setUserSpendData(response["user_info"]);
|
setUserSpendData(response["user_info"]);
|
||||||
setData(response["keys"]); // Assuming this is the correct path to your data
|
setData(response["keys"]); // Assuming this is the correct path to your data
|
||||||
|
setTeams(response["teams"]);
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
"userData" + userID,
|
"userData" + userID,
|
||||||
JSON.stringify(response["keys"])
|
JSON.stringify(response["keys"])
|
||||||
|
@ -185,12 +199,14 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
/>
|
/>
|
||||||
<CreateKey
|
<CreateKey
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
teamID={selectedTeam ? selectedTeam["team_id"] : null}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
userModels={userModels}
|
userModels={userModels}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
data={data}
|
data={data}
|
||||||
setData={setData}
|
setData={setData}
|
||||||
/>
|
/>
|
||||||
|
<DashboardTeam teams={teams} setSelectedTeam={setSelectedTeam} />
|
||||||
</Col>
|
</Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -43,7 +43,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
|
||||||
// Set the key to delete and open the confirmation modal
|
// Set the key to delete and open the confirmation modal
|
||||||
setKeyToDelete(token);
|
setKeyToDelete(token);
|
||||||
localStorage.removeItem("userData" + userID)
|
localStorage.removeItem("userData" + userID);
|
||||||
setIsDeleteModalOpen(true);
|
setIsDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
console.log("RERENDER TRIGGERED");
|
console.log("RERENDER TRIGGERED");
|
||||||
return (
|
return (
|
||||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh] mb-4">
|
||||||
<Title>API Keys</Title>
|
|
||||||
<Table className="mt-5">
|
<Table className="mt-5">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue