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:
Krish Dholakia 2024-02-25 01:12:39 -08:00 committed by GitHub
commit 61d69b1efa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 958 additions and 57 deletions

View file

@ -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">

View file

@ -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):

View file

@ -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)]
) )

View file

@ -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={

View file

@ -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}

View 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;

View file

@ -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

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
};

View 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;

View file

@ -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>

View file

@ -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>