forked from phoenix/litellm-mirror
feat(proxy_server.py): allow admin to invite users via invite link
Closes https://github.com/BerriAI/litellm/issues/3863
This commit is contained in:
parent
6b50e656b8
commit
86b66c13a4
5 changed files with 354 additions and 37 deletions
|
@ -1154,3 +1154,28 @@ class WebhookEvent(CallInfo):
|
||||||
class SpecialModelNames(enum.Enum):
|
class SpecialModelNames(enum.Enum):
|
||||||
all_team_models = "all-team-models"
|
all_team_models = "all-team-models"
|
||||||
all_proxy_models = "all-proxy-models"
|
all_proxy_models = "all-proxy-models"
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationNew(LiteLLMBase):
|
||||||
|
user_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationUpdate(LiteLLMBase):
|
||||||
|
invitation_id: str
|
||||||
|
is_accepted: bool
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationDelete(LiteLLMBase):
|
||||||
|
invitation_id: str
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationModel(LiteLLMBase):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
is_accepted: bool
|
||||||
|
accepted_at: Optional[datetime]
|
||||||
|
expires_at: datetime
|
||||||
|
created_at: datetime
|
||||||
|
created_by: str
|
||||||
|
updated_at: datetime
|
||||||
|
updated_by: str
|
||||||
|
|
|
@ -10841,6 +10841,236 @@ async def auth_callback(request: Request):
|
||||||
return RedirectResponse(url=litellm_dashboard_ui)
|
return RedirectResponse(url=litellm_dashboard_ui)
|
||||||
|
|
||||||
|
|
||||||
|
#### INVITATION MANAGEMENT ####
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/invitation/new",
|
||||||
|
tags=["Invite Links"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
response_model=InvitationModel,
|
||||||
|
)
|
||||||
|
async def new_invitation(
|
||||||
|
data: InvitationNew, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Allow admin to create invite links, to onboard new users to Admin UI.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'http://localhost:4000/invitation/new' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-D '{
|
||||||
|
"user_id": "1234" // 👈 id of user in 'LiteLLM_UserTable'
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
global prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_api_key_dict.user_role != "proxy_admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": "{}, your role={}".format(
|
||||||
|
CommonProxyErrors.not_allowed_access.value,
|
||||||
|
user_api_key_dict.user_role,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
current_time = litellm.utils.get_utc_datetime()
|
||||||
|
expires_at = current_time + timedelta(days=7)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await prisma_client.db.litellm_invitationlink.create(
|
||||||
|
data={
|
||||||
|
"user_id": data.user_id,
|
||||||
|
"created_at": current_time,
|
||||||
|
"expires_at": expires_at,
|
||||||
|
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||||
|
"updated_at": current_time,
|
||||||
|
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||||
|
} # type: ignore
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
if "Foreign key constraint failed on the field" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": "User id does not exist in 'LiteLLM_UserTable'. Fix this by creating user via `/user/new`."
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/invitation/info",
|
||||||
|
tags=["Invite Links"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
response_model=InvitationModel,
|
||||||
|
)
|
||||||
|
async def invitation_info(
|
||||||
|
invitation_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Allow admin to create invite links, to onboard new users to Admin UI.
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'http://localhost:4000/invitation/new' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-D '{
|
||||||
|
"user_id": "1234" // 👈 id of user in 'LiteLLM_UserTable'
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
global prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_api_key_dict.user_role != "proxy_admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": "{}, your role={}".format(
|
||||||
|
CommonProxyErrors.not_allowed_access.value,
|
||||||
|
user_api_key_dict.user_role,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await prisma_client.db.litellm_invitationlink.find_unique(
|
||||||
|
where={"id": invitation_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": "Invitation id does not exist in the database."},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/invitation/update",
|
||||||
|
tags=["Invite Links"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
response_model=InvitationModel,
|
||||||
|
)
|
||||||
|
async def invitation_update(
|
||||||
|
data: InvitationUpdate,
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update when invitation is accepted
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'http://localhost:4000/invitation/update' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-D '{
|
||||||
|
"invitation_id": "1234" // 👈 id of invitation in 'LiteLLM_InvitationTable'
|
||||||
|
"is_accepted": True // when invitation is accepted
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
global prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_api_key_dict.user_id is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail={
|
||||||
|
"error": "Unable to identify user id. Received={}".format(
|
||||||
|
user_api_key_dict.user_id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
current_time = litellm.utils.get_utc_datetime()
|
||||||
|
response = await prisma_client.db.litellm_invitationlink.update(
|
||||||
|
where={"id": data.invitation_id},
|
||||||
|
data={
|
||||||
|
"id": data.invitation_id,
|
||||||
|
"is_accepted": data.is_accepted,
|
||||||
|
"accepted_at": current_time,
|
||||||
|
"updated_at": current_time,
|
||||||
|
"updated_by": user_api_key_dict.user_id, # type: ignore
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": "Invitation id does not exist in the database."},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/invitation/delete",
|
||||||
|
tags=["Invite Links"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
response_model=InvitationModel,
|
||||||
|
)
|
||||||
|
async def invitation_delete(
|
||||||
|
data: InvitationDelete,
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete invitation link
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST 'http://localhost:4000/invitation/delete' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-D '{
|
||||||
|
"invitation_id": "1234" // 👈 id of invitation in 'LiteLLM_InvitationTable'
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
global prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_api_key_dict.user_role != "proxy_admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": "{}, your role={}".format(
|
||||||
|
CommonProxyErrors.not_allowed_access.value,
|
||||||
|
user_api_key_dict.user_role,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await prisma_client.db.litellm_invitationlink.delete(
|
||||||
|
where={"id": data.invitation_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={"error": "Invitation id does not exist in the database."},
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
#### CONFIG MANAGEMENT ####
|
#### CONFIG MANAGEMENT ####
|
||||||
@router.post(
|
@router.post(
|
||||||
"/config/update",
|
"/config/update",
|
||||||
|
|
|
@ -102,6 +102,7 @@ model LiteLLM_UserTable {
|
||||||
user_alias String?
|
user_alias String?
|
||||||
team_id String?
|
team_id String?
|
||||||
organization_id String?
|
organization_id String?
|
||||||
|
password String?
|
||||||
teams String[] @default([])
|
teams String[] @default([])
|
||||||
user_role String?
|
user_role String?
|
||||||
max_budget Float?
|
max_budget Float?
|
||||||
|
@ -117,6 +118,9 @@ model LiteLLM_UserTable {
|
||||||
model_spend Json @default("{}")
|
model_spend Json @default("{}")
|
||||||
model_max_budget Json @default("{}")
|
model_max_budget Json @default("{}")
|
||||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||||
|
invitations_created LiteLLM_InvitationLink[] @relation("CreatedBy")
|
||||||
|
invitations_updated LiteLLM_InvitationLink[] @relation("UpdatedBy")
|
||||||
|
invitations_user LiteLLM_InvitationLink[] @relation("UserId")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Tokens for Proxy
|
// Generate Tokens for Proxy
|
||||||
|
@ -222,3 +226,21 @@ model LiteLLM_TeamMembership {
|
||||||
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
|
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
|
||||||
@@id([user_id, team_id])
|
@@id([user_id, team_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model LiteLLM_InvitationLink {
|
||||||
|
// use this table to track invite links sent by admin for people to join the proxy
|
||||||
|
id String @id @default(uuid())
|
||||||
|
user_id String
|
||||||
|
is_accepted Boolean @default(false)
|
||||||
|
accepted_at DateTime? // when link is claimed (user successfully onboards via link)
|
||||||
|
expires_at DateTime // till when is link valid
|
||||||
|
created_at DateTime // when did admin create the link
|
||||||
|
created_by String // who created the link
|
||||||
|
updated_at DateTime // when was invite status updated
|
||||||
|
updated_by String // who updated the status (admin/user who accepted invite)
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
liteLLM_user_table_user LiteLLM_UserTable @relation("UserId", fields: [user_id], references: [user_id])
|
||||||
|
liteLLM_user_table_created LiteLLM_UserTable @relation("CreatedBy", fields: [created_by], references: [user_id])
|
||||||
|
liteLLM_user_table_updated LiteLLM_UserTable @relation("UpdatedBy", fields: [updated_by], references: [user_id])
|
||||||
|
}
|
|
@ -102,6 +102,7 @@ model LiteLLM_UserTable {
|
||||||
user_alias String?
|
user_alias String?
|
||||||
team_id String?
|
team_id String?
|
||||||
organization_id String?
|
organization_id String?
|
||||||
|
password String?
|
||||||
teams String[] @default([])
|
teams String[] @default([])
|
||||||
user_role String?
|
user_role String?
|
||||||
max_budget Float?
|
max_budget Float?
|
||||||
|
@ -117,6 +118,9 @@ model LiteLLM_UserTable {
|
||||||
model_spend Json @default("{}")
|
model_spend Json @default("{}")
|
||||||
model_max_budget Json @default("{}")
|
model_max_budget Json @default("{}")
|
||||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||||
|
invitations_created LiteLLM_InvitationLink[] @relation("CreatedBy")
|
||||||
|
invitations_updated LiteLLM_InvitationLink[] @relation("UpdatedBy")
|
||||||
|
invitations_user LiteLLM_InvitationLink[] @relation("UserId")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Tokens for Proxy
|
// Generate Tokens for Proxy
|
||||||
|
@ -222,3 +226,21 @@ model LiteLLM_TeamMembership {
|
||||||
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
|
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
|
||||||
@@id([user_id, team_id])
|
@@id([user_id, team_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model LiteLLM_InvitationLink {
|
||||||
|
// use this table to track invite links sent by admin for people to join the proxy
|
||||||
|
id String @id @default(uuid())
|
||||||
|
user_id String
|
||||||
|
is_accepted Boolean @default(false)
|
||||||
|
accepted_at DateTime? // when link is claimed (user successfully onboards via link)
|
||||||
|
expires_at DateTime // till when is link valid
|
||||||
|
created_at DateTime // when did admin create the link
|
||||||
|
created_by String // who created the link
|
||||||
|
updated_at DateTime // when was invite status updated
|
||||||
|
updated_by String // who updated the status (admin/user who accepted invite)
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
liteLLM_user_table_user LiteLLM_UserTable @relation("UserId", fields: [user_id], references: [user_id])
|
||||||
|
liteLLM_user_table_created LiteLLM_UserTable @relation("CreatedBy", fields: [created_by], references: [user_id])
|
||||||
|
liteLLM_user_table_updated LiteLLM_UserTable @relation("UpdatedBy", fields: [updated_by], references: [user_id])
|
||||||
|
}
|
|
@ -31,14 +31,13 @@ interface ModelHubProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelInfo {
|
interface ModelInfo {
|
||||||
model_group: string;
|
model_group: string;
|
||||||
mode: string;
|
mode: string;
|
||||||
supports_function_calling: boolean;
|
supports_function_calling: boolean;
|
||||||
supports_vision: boolean;
|
supports_vision: boolean;
|
||||||
max_input_tokens?: number;
|
max_input_tokens?: number;
|
||||||
max_output_tokens?: number;
|
max_output_tokens?: number;
|
||||||
supported_openai_params?: string[];
|
supported_openai_params?: string[];
|
||||||
|
|
||||||
|
|
||||||
// Add other properties if needed
|
// Add other properties if needed
|
||||||
}
|
}
|
||||||
|
@ -116,7 +115,7 @@ const ModelHub: React.FC<ModelHubProps> = ({
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
|
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4 pr-5">
|
||||||
{modelHubData &&
|
{modelHubData &&
|
||||||
modelHubData.map((model: ModelInfo) => (
|
modelHubData.map((model: ModelInfo) => (
|
||||||
<Card key={model.model_group} className="mt-5 mx-8">
|
<Card key={model.model_group} className="mt-5 mx-8">
|
||||||
|
@ -129,14 +128,27 @@ const ModelHub: React.FC<ModelHubProps> = ({
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</pre>
|
</pre>
|
||||||
<div className='my-5'>
|
<div className="my-5">
|
||||||
|
<Text>Mode: {model.mode}</Text>
|
||||||
<Text>Mode: {model.mode}</Text>
|
<Text>
|
||||||
<Text>Supports Function Calling: {model?.supports_function_calling == true ? "Yes" : "No"}</Text>
|
Supports Function Calling:{" "}
|
||||||
<Text>Supports Vision: {model?.supports_vision == true ? "Yes" : "No"}</Text>
|
{model?.supports_function_calling == true ? "Yes" : "No"}
|
||||||
<Text>Max Input Tokens: {model?.max_input_tokens ? model?.max_input_tokens : "N/A"}</Text>
|
</Text>
|
||||||
<Text>Max Output Tokens: {model?.max_output_tokens ? model?.max_output_tokens : "N/A"}</Text>
|
<Text>
|
||||||
</div>
|
Supports Vision:{" "}
|
||||||
|
{model?.supports_vision == true ? "Yes" : "No"}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Max Input Tokens:{" "}
|
||||||
|
{model?.max_input_tokens ? model?.max_input_tokens : "N/A"}
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Max Output Tokens:{" "}
|
||||||
|
{model?.max_output_tokens
|
||||||
|
? model?.max_output_tokens
|
||||||
|
: "N/A"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
<div style={{ marginTop: "auto", textAlign: "right" }}>
|
<div style={{ marginTop: "auto", textAlign: "right" }}>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
|
@ -152,7 +164,11 @@ const ModelHub: React.FC<ModelHubProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={selectedModel && selectedModel.model_group ? selectedModel.model_group : "Unknown Model"}
|
title={
|
||||||
|
selectedModel && selectedModel.model_group
|
||||||
|
? selectedModel.model_group
|
||||||
|
: "Unknown Model"
|
||||||
|
}
|
||||||
width={800}
|
width={800}
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
@ -161,19 +177,21 @@ const ModelHub: React.FC<ModelHubProps> = ({
|
||||||
>
|
>
|
||||||
{selectedModel && (
|
{selectedModel && (
|
||||||
<div>
|
<div>
|
||||||
<p className='mb-4'><strong>Model Information & Usage</strong></p>
|
<p className="mb-4">
|
||||||
|
<strong>Model Information & Usage</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>OpenAI Python SDK</Tab>
|
<Tab>OpenAI Python SDK</Tab>
|
||||||
<Tab>Supported OpenAI Params</Tab>
|
<Tab>Supported OpenAI Params</Tab>
|
||||||
<Tab>LlamaIndex</Tab>
|
<Tab>LlamaIndex</Tab>
|
||||||
<Tab>Langchain Py</Tab>
|
<Tab>Langchain Py</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<SyntaxHighlighter language="python">
|
<SyntaxHighlighter language="python">
|
||||||
{`
|
{`
|
||||||
import openai
|
import openai
|
||||||
client = openai.OpenAI(
|
client = openai.OpenAI(
|
||||||
api_key="your_api_key",
|
api_key="your_api_key",
|
||||||
|
@ -192,13 +210,13 @@ response = client.chat.completions.create(
|
||||||
|
|
||||||
print(response)
|
print(response)
|
||||||
`}
|
`}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<SyntaxHighlighter language="python">
|
<SyntaxHighlighter language="python">
|
||||||
{`${selectedModel.supported_openai_params?.map((param) => `${param}\n`).join('')}`}
|
{`${selectedModel.supported_openai_params?.map((param) => `${param}\n`).join("")}`}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<SyntaxHighlighter language="python">
|
<SyntaxHighlighter language="python">
|
||||||
{`
|
{`
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue