Merge branch 'main' into litellm_invite_users_via_link

This commit is contained in:
Krish Dholakia 2024-05-27 20:36:43 -07:00 committed by GitHub
commit bcbc250a12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 594 additions and 234 deletions

View file

@ -2,12 +2,6 @@ import Image from '@theme/IdealImage';
# ✨ 📧 Email Notifications
:::info
This is an Enterprise only feature [Get in touch with us for a Free Trial](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat)
:::
Send an Email to your users when:
- A Proxy API Key is created for them
- Their API Key crosses it's Budget
@ -38,6 +32,12 @@ That's it ! start your proxy
## Customizing Email Branding
:::info
Customizing Email Branding is an Enterprise Feature [Get in touch with us for a Free Trial](https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat)
:::
LiteLLM allows you to customize the:
- Logo on the Email
- Email support contact

View file

@ -13,7 +13,7 @@ Requirements:
You can set budgets at 3 levels:
- For the proxy
- For an internal user
- For an end-user
- For a customer (end-user)
- For a key
- For a key (model specific budgets)
@ -173,7 +173,7 @@ curl --location 'http://localhost:4000/chat/completions' \
```
</TabItem>
<TabItem value="per-user-chat" label="For End User">
<TabItem value="per-user-chat" label="For Customers">
Use this to budget `user` passed to `/chat/completions`, **without needing to create a key for every user**
@ -452,7 +452,7 @@ curl --location 'http://0.0.0.0:4000/key/generate' \
```
</TabItem>
<TabItem value="per-end-user" label="For End User">
<TabItem value="per-end-user" label="For customers">
:::info
@ -477,12 +477,12 @@ curl --location 'http://0.0.0.0:4000/budget/new' \
```
#### Step 2. Create `End-User` with Budget
#### Step 2. Create `Customer` with Budget
We use `budget_id="free-tier"` from Step 1 when creating this new end user
We use `budget_id="free-tier"` from Step 1 when creating this new customers
```shell
curl --location 'http://0.0.0.0:4000/end_user/new' \
curl --location 'http://0.0.0.0:4000/customer/new' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
@ -492,7 +492,7 @@ curl --location 'http://0.0.0.0:4000/end_user/new' \
```
#### Step 3. Pass end user id in `/chat/completions` requests
#### Step 3. Pass `user_id` id in `/chat/completions` requests
Pass the `user_id` from Step 2 as `user="palantir"`

View file

@ -41,10 +41,6 @@ class ProviderRegionOutageModel(BaseOutageModel):
# we use this for the email header, please send a test email if you change this. verify it looks good on email
LITELLM_LOGO_URL = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
EMAIL_LOGO_URL = os.getenv(
"SMTP_SENDER_LOGO", "https://litellm-listing.s3.amazonaws.com/litellm_logo.png"
)
EMAIL_SUPPORT_CONTACT = os.getenv("EMAIL_SUPPORT_CONTACT", "support@berri.ai")
class LiteLLMBase(BaseModel):
@ -1147,21 +1143,34 @@ Model Info:
return False
async def _check_if_using_premium_email_feature(
self,
premium_user: bool,
email_logo_url: Optional[str] = None,
email_support_contact: Optional[str] = None,
):
from litellm.proxy.proxy_server import premium_user
from litellm.proxy.proxy_server import CommonProxyErrors
if premium_user is not True:
if email_logo_url is not None or email_support_contact is not None:
raise ValueError(
f"Trying to Customize Email Alerting\n {CommonProxyErrors.not_premium_user.value}"
)
async def send_key_created_email(self, webhook_event: WebhookEvent) -> bool:
from litellm.proxy.utils import send_email
if self.alerting is None or "email" not in self.alerting:
# do nothing if user does not want email alerts
return False
from litellm.proxy.proxy_server import premium_user, prisma_client
# make sure this is a premium user
from litellm.proxy.proxy_server import premium_user
from litellm.proxy.proxy_server import CommonProxyErrors, prisma_client
if premium_user != True:
raise Exception(
f"Trying to use Email Alerting on key creation\n {CommonProxyErrors.not_premium_user.value}"
)
email_logo_url = os.getenv("SMTP_SENDER_LOGO", None)
email_support_contact = os.getenv("EMAIL_SUPPORT_CONTACT", None)
await self._check_if_using_premium_email_feature(
premium_user, email_logo_url, email_support_contact
)
event_name = webhook_event.event_message
recipient_email = webhook_event.user_email
@ -1188,7 +1197,7 @@ Model Info:
"Trying to send email alert to no recipient", extra=webhook_event.dict()
)
email_html_content = f"""
<img src="{EMAIL_LOGO_URL}" alt="LiteLLM Logo" width="150" height="50" />
<img src="{email_logo_url}" alt="LiteLLM Logo" width="150" height="50" />
<p> Hi {recipient_email}, <br/>
@ -1223,7 +1232,7 @@ Model Info:
</pre>
If you have any questions, please send an email to {EMAIL_SUPPORT_CONTACT} <br /> <br />
If you have any questions, please send an email to {email_support_contact} <br /> <br />
Best, <br />
The LiteLLM team <br />
@ -1254,6 +1263,14 @@ Model Info:
"""
from litellm.proxy.utils import send_email
from litellm.proxy.proxy_server import premium_user, prisma_client
email_logo_url = os.getenv("SMTP_SENDER_LOGO", None)
email_support_contact = os.getenv("EMAIL_SUPPORT_CONTACT", None)
await self._check_if_using_premium_email_feature(
premium_user, email_logo_url, email_support_contact
)
event_name = webhook_event.event_message
recipient_email = webhook_event.user_email
user_name = webhook_event.user_id
@ -1266,7 +1283,7 @@ Model Info:
if webhook_event.event == "budget_crossed":
email_html_content = f"""
<img src="{EMAIL_LOGO_URL}" alt="LiteLLM Logo" width="150" height="50" />
<img src="{email_logo_url}" alt="LiteLLM Logo" width="150" height="50" />
<p> Hi {user_name}, <br/>
@ -1274,7 +1291,7 @@ Model Info:
API requests will be rejected until either (a) you increase your monthly budget or (b) your monthly usage resets at the beginning of the next calendar month. <br /> <br />
If you have any questions, please send an email to {EMAIL_SUPPORT_CONTACT} <br /> <br />
If you have any questions, please send an email to {email_support_contact} <br /> <br />
Best, <br />
The LiteLLM team <br />

View file

@ -519,7 +519,11 @@ class UpdateUserRequest(GenerateRequestBase):
return values
class NewEndUserRequest(LiteLLMBase):
class NewCustomerRequest(LiteLLMBase):
"""
Create a new customer, allocate a budget to them
"""
user_id: str
alias: Optional[str] = None # human-friendly alias
blocked: bool = False # allow/disallow requests for this end-user
@ -540,6 +544,33 @@ class NewEndUserRequest(LiteLLMBase):
return values
class UpdateCustomerRequest(LiteLLMBase):
"""
Update a Customer, use this to update customer budgets etc
"""
user_id: str
alias: Optional[str] = None # human-friendly alias
blocked: bool = False # allow/disallow requests for this end-user
max_budget: Optional[float] = None
budget_id: Optional[str] = None # give either a budget_id or max_budget
allowed_model_region: Optional[Literal["eu"]] = (
None # require all user requests to use models in this specific region
)
default_model: Optional[str] = (
None # if no equivalent model in allowed region - default all requests to this model
)
class DeleteCustomerRequest(LiteLLMBase):
"""
Delete multiple Customers
"""
user_ids: List[str]
class Member(LiteLLMBase):
role: Literal["admin", "user"]
user_id: Optional[str] = None
@ -928,6 +959,10 @@ class ConfigGeneralSettings(LiteLLMBase):
allowed_routes: Optional[List] = Field(
None, description="Proxy API Endpoints you want users to be able to access"
)
enable_public_model_hub: bool = Field(
default=False,
description="Public model hub for users to see what models they have access to, supported openai params, etc.",
)
class ConfigYAML(LiteLLMBase):
@ -1179,3 +1214,7 @@ class InvitationModel(LiteLLMBase):
created_by: str
updated_at: datetime
updated_by: str
class ConfigFieldInfo(LiteLLMBase):
field_name: str
field_value: Any

View file

@ -7137,13 +7137,15 @@ async def global_predict_spend_logs(request: Request):
#### INTERNAL USER MANAGEMENT ####
@router.post(
"/user/new",
tags=["user management"],
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
response_model=NewUserResponse,
)
async def new_user(data: NewUserRequest):
"""
Use this to create a new user with a budget. This creates a new user and generates a new api key for the new user. The new api key is returned.
Use this to create a new INTERNAL user with a budget.
Internal Users can access LiteLLM Admin UI to make keys, request access to models.
This creates a new user and generates a new api key for the new user. The new api key is returned.
Returns user id, budget + new key.
@ -7214,7 +7216,9 @@ async def new_user(data: NewUserRequest):
@router.post(
"/user/auth", tags=["user management"], dependencies=[Depends(user_api_key_auth)]
"/user/auth",
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def user_auth(request: Request):
"""
@ -7280,7 +7284,9 @@ async def user_auth(request: Request):
@router.get(
"/user/info", tags=["user management"], dependencies=[Depends(user_api_key_auth)]
"/user/info",
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def user_info(
user_id: Optional[str] = fastapi.Query(
@ -7452,7 +7458,9 @@ async def user_info(
@router.post(
"/user/update", tags=["user management"], dependencies=[Depends(user_api_key_auth)]
"/user/update",
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def user_update(data: UpdateUserRequest):
"""
@ -7546,7 +7554,7 @@ async def user_update(data: UpdateUserRequest):
@router.post(
"/user/request_model",
tags=["user management"],
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def user_request_model(request: Request):
@ -7599,7 +7607,7 @@ async def user_request_model(request: Request):
@router.get(
"/user/get_requests",
tags=["user management"],
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def user_get_requests():
@ -7641,7 +7649,7 @@ async def user_get_requests():
@router.get(
"/user/get_users",
tags=["user management"],
tags=["Internal User management"],
dependencies=[Depends(user_api_key_auth)],
)
async def get_users(
@ -7678,7 +7686,13 @@ async def get_users(
@router.post(
"/end_user/block",
tags=["End User Management"],
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
include_in_schema=False,
)
@router.post(
"/customer/block",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
async def block_user(data: BlockUsers):
@ -7721,9 +7735,15 @@ async def block_user(data: BlockUsers):
@router.post(
"/end_user/unblock",
tags=["End User Management"],
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
@router.post(
"/customer/unblock",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
include_in_schema=False,
)
async def unblock_user(data: BlockUsers):
"""
[BETA] Unblock calls with this user id
@ -7768,35 +7788,36 @@ async def unblock_user(data: BlockUsers):
@router.post(
"/end_user/new",
tags=["End User Management"],
tags=["Customer Management"],
include_in_schema=False,
dependencies=[Depends(user_api_key_auth)],
)
@router.post(
"/customer/new",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
async def new_end_user(
data: NewEndUserRequest,
data: NewCustomerRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
[TODO] Needs to be implemented.
Allow creating a new end-user
Allow creating a new Customer
NOTE: This used to be called `/end_user/new`, we will still be maintaining compatibility for /end_user/XXX for these endpoints
- Allow specifying allowed regions
- Allow specifying default model
Example curl:
```
curl --location 'http://0.0.0.0:4000/end_user/new' \
curl --location 'http://0.0.0.0:4000/customer/new' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"end_user_id" : "ishaan-jaff-3", <- specific customer
"allowed_region": "eu" <- set region for models
+
"user_id" : "ishaan-jaff-3",
"allowed_region": "eu",
"budget_id": "free_tier",
"default_model": "azure/gpt-3.5-turbo-eu" <- all calls from this user, use this model?
}'
# return end-user object
@ -7819,56 +7840,88 @@ async def new_end_user(
status_code=500,
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
try:
## VALIDATION ##
if data.default_model is not None:
if llm_router is None:
raise HTTPException(
status_code=422, detail={"error": CommonProxyErrors.no_llm_router.value}
)
elif data.default_model not in llm_router.get_model_names():
raise HTTPException(
status_code=422,
detail={
"error": "Default Model not on proxy. Configure via `/model/new` or config.yaml. Default_model={}, proxy_model_names={}".format(
data.default_model, set(llm_router.get_model_names())
)
},
## VALIDATION ##
if data.default_model is not None:
if llm_router is None:
raise HTTPException(
status_code=422,
detail={"error": CommonProxyErrors.no_llm_router.value},
)
elif data.default_model not in llm_router.get_model_names():
raise HTTPException(
status_code=422,
detail={
"error": "Default Model not on proxy. Configure via `/model/new` or config.yaml. Default_model={}, proxy_model_names={}".format(
data.default_model, set(llm_router.get_model_names())
)
},
)
new_end_user_obj: Dict = {}
## CREATE BUDGET ## if set
if data.max_budget is not None:
budget_record = await prisma_client.db.litellm_budgettable.create(
data={
"max_budget": data.max_budget,
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name, # type: ignore
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
}
)
new_end_user_obj: Dict = {}
new_end_user_obj["budget_id"] = budget_record.budget_id
elif data.budget_id is not None:
new_end_user_obj["budget_id"] = data.budget_id
## CREATE BUDGET ## if set
if data.max_budget is not None:
budget_record = await prisma_client.db.litellm_budgettable.create(
data={
"max_budget": data.max_budget,
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name, # type: ignore
"updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
}
_user_data = data.dict(exclude_none=True)
for k, v in _user_data.items():
if k != "max_budget" and k != "budget_id":
new_end_user_obj[k] = v
## WRITE TO DB ##
end_user_record = await prisma_client.db.litellm_endusertable.create(
data=new_end_user_obj # type: ignore
)
new_end_user_obj["budget_id"] = budget_record.budget_id
elif data.budget_id is not None:
new_end_user_obj["budget_id"] = data.budget_id
return end_user_record
except Exception as e:
if "Unique constraint failed on the fields: (`user_id`)" in str(e):
raise ProxyException(
message=f"Customer already exists, passed user_id={data.user_id}. Please pass a new user_id.",
type="bad_request",
code=400,
param="user_id",
)
_user_data = data.dict(exclude_none=True)
for k, v in _user_data.items():
if k != "max_budget" and k != "budget_id":
new_end_user_obj[k] = v
## WRITE TO DB ##
end_user_record = await prisma_client.db.litellm_endusertable.create(
data=new_end_user_obj # type: ignore
)
return end_user_record
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", f"Internal Server Error({str(e)})"),
type="internal_error",
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
elif isinstance(e, ProxyException):
raise e
raise ProxyException(
message="Internal Server Error, " + str(e),
type="internal_error",
param=getattr(e, "param", "None"),
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@router.get(
"/customer/info",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
@router.get(
"/end_user/info",
tags=["End User Management"],
tags=["Customer Management"],
include_in_schema=False,
dependencies=[Depends(user_api_key_auth)],
)
async def end_user_info(
@ -7892,26 +7945,174 @@ async def end_user_info(
@router.post(
"/end_user/update",
tags=["End User Management"],
"/customer/update",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
async def update_end_user():
@router.post(
"/end_user/update",
tags=["Customer Management"],
include_in_schema=False,
dependencies=[Depends(user_api_key_auth)],
)
async def update_end_user(
data: UpdateCustomerRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
[TODO] Needs to be implemented.
Example curl
```
curl --location 'http://0.0.0.0:4000/customer/update' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"user_id": "test-litellm-user-4",
"budget_id": "paid_tier"
}'
See below for all params
```
"""
global prisma_client
try:
data_json: dict = data.json()
# get the row from db
if prisma_client is None:
raise Exception("Not connected to DB!")
# get non default values for key
non_default_values = {}
for k, v in data_json.items():
if v is not None and v not in (
[],
{},
0,
): # models default to [], spend defaults to 0, we should not reset these values
non_default_values[k] = v
## ADD USER, IF NEW ##
verbose_proxy_logger.debug("/customer/update: Received data = %s", data)
if data.user_id is not None and len(data.user_id) > 0:
non_default_values["user_id"] = data.user_id # type: ignore
verbose_proxy_logger.debug("In update customer, user_id condition block.")
response = await prisma_client.db.litellm_endusertable.update(
where={"user_id": data.user_id}, data=non_default_values # type: ignore
)
if response is None:
raise ValueError(
f"Failed updating customer data. User ID does not exist passed user_id={data.user_id}"
)
verbose_proxy_logger.debug(
f"received response from updating prisma client. response={response}"
)
return response
else:
raise ValueError(f"user_id is required, passed user_id = {data.user_id}")
# update based on remaining passed in values
except Exception as e:
traceback.print_exc()
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", f"Internal Server Error({str(e)})"),
type="internal_error",
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
elif isinstance(e, ProxyException):
raise e
raise ProxyException(
message="Internal Server Error, " + str(e),
type="internal_error",
param=getattr(e, "param", "None"),
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
pass
@router.post(
"/end_user/delete",
tags=["End User Management"],
"/customer/delete",
tags=["Customer Management"],
dependencies=[Depends(user_api_key_auth)],
)
async def delete_end_user():
@router.post(
"/end_user/delete",
tags=["Customer Management"],
include_in_schema=False,
dependencies=[Depends(user_api_key_auth)],
)
async def delete_end_user(
data: DeleteCustomerRequest,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
[TODO] Needs to be implemented.
Example curl
```
curl --location 'http://0.0.0.0:4000/customer/delete' \
--header 'Authorization: Bearer sk-1234' \
--header 'Content-Type: application/json' \
--data '{
"user_ids" :["ishaan-jaff-5"]
}'
See below for all params
```
"""
global prisma_client
try:
if prisma_client is None:
raise Exception("Not connected to DB!")
verbose_proxy_logger.debug("/customer/delete: Received data = %s", data)
if (
data.user_ids is not None
and isinstance(data.user_ids, list)
and len(data.user_ids) > 0
):
response = await prisma_client.db.litellm_endusertable.delete_many(
where={"user_id": {"in": data.user_ids}}
)
if response is None:
raise ValueError(
f"Failed deleting customer data. User ID does not exist passed user_id={data.user_ids}"
)
if response != len(data.user_ids):
raise ValueError(
f"Failed deleting all customer data. User ID does not exist passed user_id={data.user_ids}. Deleted {response} customers, passed {len(data.user_ids)} customers"
)
verbose_proxy_logger.debug(
f"received response from updating prisma client. response={response}"
)
return {
"deleted_customers": response,
"message": "Successfully deleted customers with ids: "
+ str(data.user_ids),
}
else:
raise ValueError(f"user_id is required, passed user_id = {data.user_ids}")
# update based on remaining passed in values
except Exception as e:
traceback.print_exc()
if isinstance(e, HTTPException):
raise ProxyException(
message=getattr(e, "detail", f"Internal Server Error({str(e)})"),
type="internal_error",
param=getattr(e, "param", "None"),
code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR),
)
elif isinstance(e, ProxyException):
raise e
raise ProxyException(
message="Internal Server Error, " + str(e),
type="internal_error",
param=getattr(e, "param", "None"),
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
pass
@ -11297,6 +11498,7 @@ async def update_config_general_settings(
"/config/field/info",
tags=["config.yaml"],
dependencies=[Depends(user_api_key_auth)],
response_model=ConfigFieldInfo,
)
async def get_config_general_settings(
field_name: str,
@ -11343,10 +11545,9 @@ async def get_config_general_settings(
general_settings = dict(db_general_settings.param_value)
if field_name in general_settings:
return {
"field_name": field_name,
"field_value": general_settings[field_name],
}
return ConfigFieldInfo(
field_name=field_name, field_value=general_settings[field_name]
)
else:
raise HTTPException(
status_code=400,

View file

@ -1850,20 +1850,13 @@ async def send_email(receiver_email, subject, html):
from litellm.proxy.proxy_server import premium_user
from litellm.proxy.proxy_server import CommonProxyErrors
# Check if user is premium - This is an Enterprise only Feature
if premium_user != True:
raise Exception(
f"Trying to use Email Alerting\n {CommonProxyErrors.not_premium_user.value}"
)
# Done Checking
smtp_host = os.getenv("SMTP_HOST")
smtp_port = os.getenv("SMTP_PORT", 587) # default to port 587
smtp_port = int(os.getenv("SMTP_PORT", "587")) # default to port 587
smtp_username = os.getenv("SMTP_USERNAME")
smtp_password = os.getenv("SMTP_PASSWORD")
sender_email = os.getenv("SMTP_SENDER_EMAIL", None)
if sender_email is None:
raise Exception("Trying to use SMTP, but SMTP_SENDER_EMAIL is not set")
raise ValueError("Trying to use SMTP, but SMTP_SENDER_EMAIL is not set")
## EMAIL SETUP ##
email_message = MIMEMultipart()

View file

@ -0,0 +1,25 @@
"use client";
import React, { Suspense, useEffect, useState } from "react";
import { useSearchParams } from "next/navigation";
import { modelHubCall } from "@/components/networking";
import ModelHub from "@/components/model_hub";
export default function PublicModelHub() {
const searchParams = useSearchParams();
const key = searchParams.get("key");
const [accessToken, setAccessToken] = useState<string | null>(null);
useEffect(() => {
if (!key) {
return;
}
setAccessToken(key);
}, [key]);
/**
* populate navbar
*
*/
return (
<ModelHub accessToken={accessToken} publicPage={true} premiumUser={false} />
);
}

View file

@ -18,6 +18,30 @@ import Usage from "../components/usage";
import { jwtDecode } from "jwt-decode";
import { Typography } from "antd";
export function formatUserRole(userRole: string) {
if (!userRole) {
return "Undefined Role";
}
console.log(`Received user role: ${userRole.toLowerCase()}`);
console.log(`Received user role length: ${userRole.toLowerCase().length}`);
switch (userRole.toLowerCase()) {
case "app_owner":
return "App Owner";
case "demo_app_owner":
return "App Owner";
case "app_admin":
return "Admin";
case "proxy_admin":
return "Admin";
case "proxy_admin_viewer":
return "Admin Viewer";
case "app_user":
return "App User";
default:
return "Unknown Role";
}
}
const CreateKeyPage = () => {
const { Title, Paragraph } = Typography;
const [userRole, setUserRole] = useState("");
@ -78,30 +102,6 @@ const CreateKeyPage = () => {
}
}, [token]);
function formatUserRole(userRole: string) {
if (!userRole) {
return "Undefined Role";
}
console.log(`Received user role: ${userRole.toLowerCase()}`);
console.log(`Received user role length: ${userRole.toLowerCase().length}`);
switch (userRole.toLowerCase()) {
case "app_owner":
return "App Owner";
case "demo_app_owner":
return "App Owner";
case "app_admin":
return "Admin";
case "proxy_admin":
return "Admin";
case "proxy_admin_viewer":
return "Admin Viewer";
case "app_user":
return "App User";
default:
return "Unknown Role";
}
}
return (
<Suspense fallback={<div>Loading...</div>}>
<div className="flex flex-col min-h-screen">
@ -194,17 +194,13 @@ const CreateKeyPage = () => {
accessToken={accessToken}
modelData={modelData}
/>
) : page == "model-hub" ? (
<ModelHub
userID={userID}
userRole={userRole}
token={token}
) : page == "model-hub" ? (
<ModelHub
accessToken={accessToken}
keys={keys}
publicPage={false}
premiumUser={premiumUser}
/>
)
: (
) : (
<Usage
userID={userID}
userRole={userRole}

View file

@ -1,7 +1,8 @@
import React, { useEffect, useState } from "react";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { modelHubCall } from "./networking";
import { getConfigFieldSetting, updateConfigFieldSetting } from "./networking";
import {
Card,
Text,
@ -15,18 +16,14 @@ import {
TabPanel,
TabPanels,
} from "@tremor/react";
import { RightOutlined, CopyOutlined } from "@ant-design/icons";
import { Modal, Tooltip } from "antd";
import { Modal, Tooltip, message } from "antd";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
interface ModelHubProps {
userID: string | null;
userRole: string | null;
token: string | null;
accessToken: string | null;
keys: any; // Replace with the appropriate type for 'keys' prop
publicPage: boolean;
premiumUser: boolean;
}
@ -38,46 +35,51 @@ interface ModelInfo {
max_input_tokens?: number;
max_output_tokens?: number;
supported_openai_params?: string[];
// Add other properties if needed
}
const ModelHub: React.FC<ModelHubProps> = ({
userID,
userRole,
token,
accessToken,
keys,
publicPage,
premiumUser,
}) => {
const [publicPageAllowed, setPublicPageAllowed] = useState<boolean>(false);
const [modelHubData, setModelHubData] = useState<ModelInfo[] | null>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isPublicPageModalVisible, setIsPublicPageModalVisible] =
useState(false);
const [selectedModel, setSelectedModel] = useState<null | ModelInfo>(null);
const router = useRouter();
useEffect(() => {
if (!accessToken || !token || !userRole || !userID) {
if (!accessToken) {
return;
}
const fetchData = async () => {
try {
const _modelHubData = await modelHubCall(accessToken, userID, userRole);
const _modelHubData = await modelHubCall(accessToken);
console.log("ModelHubData:", _modelHubData);
setModelHubData(_modelHubData.data);
getConfigFieldSetting(accessToken, "enable_public_model_hub")
.then((data) => {
console.log(`data: ${JSON.stringify(data)}`);
if (data.field_value == true) {
setPublicPageAllowed(true);
}
})
.catch((error) => {
// do nothing
});
} catch (error) {
console.error("There was an error fetching the model data", error);
}
};
fetchData();
}, [accessToken, token, userRole, userID]);
}, [accessToken, publicPage]);
const showModal = (model: ModelInfo) => {
setSelectedModel(model);
@ -85,15 +87,29 @@ const ModelHub: React.FC<ModelHubProps> = ({
setIsModalVisible(true);
};
const goToPublicModelPage = () => {
router.replace(`/model_hub?key=${accessToken}`);
};
const handleMakePublicPage = async () => {
if (!accessToken) {
return;
}
updateConfigFieldSetting(accessToken, "enable_public_model_hub", true).then(
(data) => {
setIsPublicPageModalVisible(true);
}
);
};
const handleOk = () => {
setIsModalVisible(false);
setIsPublicPageModalVisible(false);
setSelectedModel(null);
};
const handleCancel = () => {
setIsModalVisible(false);
setIsPublicPageModalVisible(false);
setSelectedModel(null);
};
@ -103,66 +119,112 @@ const ModelHub: React.FC<ModelHubProps> = ({
return (
<div>
<div className="w-full m-2 mt-2 p-8">
<div className="relative w-full"></div>
{(publicPage && publicPageAllowed) || publicPage == false ? (
<div className="w-full m-2 mt-2 p-8">
<div className="relative w-full"></div>
<div className="flex items-center">
<Title className="ml-8 text-center ">Model Hub</Title>
<Button className="ml-4">
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
Make Public
</a>
</Button>
</div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4 pr-5">
{modelHubData &&
modelHubData.map((model: ModelInfo) => (
<Card key={model.model_group} className="mt-5 mx-8">
<pre className="flex justify-between">
<Title>{model.model_group}</Title>
<Tooltip title={model.model_group}>
<CopyOutlined
onClick={() => copyToClipboard(model.model_group)}
style={{ cursor: "pointer", marginRight: "10px" }}
/>
</Tooltip>
</pre>
<div className="my-5">
<Text>Mode: {model.mode}</Text>
<Text>
Supports Function Calling:{" "}
{model?.supports_function_calling == true ? "Yes" : "No"}
</Text>
<Text>
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" }}>
<a
href="#"
onClick={() => showModal(model)}
style={{ color: "#1890ff", fontSize: "smaller" }}
>
View more <RightOutlined />
<div
className={`flex ${publicPage ? "justify-between" : "items-center"}`}
>
<Title className="ml-8 text-center ">Model Hub</Title>
{publicPage == false ? (
premiumUser ? (
<Button className="ml-4" onClick={() => handleMakePublicPage()}>
Make Public
</Button>
) : (
<Button className="ml-4">
<a href="https://forms.gle/W3U4PZpJGFHWtHyA9" target="_blank">
Make Public
</a>
</div>
</Card>
))}
</div>
</div>
</Button>
)
) : (
<div className="flex justify-between items-center">
<p>Filter by key:</p>
<Text className="bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center">{`/ui/model_hub?key=<YOUR_KEY>`}</Text>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{modelHubData &&
modelHubData.map((model: ModelInfo) => (
<Card key={model.model_group} className="mt-5 mx-8">
<pre className="flex justify-between">
<Title>{model.model_group}</Title>
<Tooltip title={model.model_group}>
<CopyOutlined
onClick={() => copyToClipboard(model.model_group)}
style={{ cursor: "pointer", marginRight: "10px" }}
/>
</Tooltip>
</pre>
<div className="my-5">
<Text>Mode: {model.mode}</Text>
<Text>
Supports Function Calling:{" "}
{model?.supports_function_calling == true ? "Yes" : "No"}
</Text>
<Text>
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" }}>
<a
href="#"
onClick={() => showModal(model)}
style={{ color: "#1890ff", fontSize: "smaller" }}
>
View more <RightOutlined />
</a>
</div>
</Card>
))}
</div>
</div>
) : (
<Card className="mx-auto max-w-xl mt-10">
<Text className="text-xl text-center mb-2 text-black">
Public Model Hub not enabled.
</Text>
<p className="text-base text-center text-slate-800">
Ask your proxy admin to enable this on their Admin UI.
</p>
</Card>
)}
<Modal
title={"Public Model Hub"}
width={600}
visible={isPublicPageModalVisible}
footer={null}
onOk={handleOk}
onCancel={handleCancel}
>
<div className="pt-5 pb-5">
<div className="flex justify-between mb-4">
<Text className="text-base mr-2">Shareable Link:</Text>
<Text className="max-w-sm ml-2 bg-gray-200 pr-2 pl-2 pt-1 pb-1 text-center rounded">{`<proxy_base_url>/ui/model_hub?key=<YOUR_API_KEY>`}</Text>
</div>
<div className="flex justify-end">
<Button onClick={goToPublicModelPage}>See Page</Button>
</div>
</div>
</Modal>
<Modal
title={
selectedModel && selectedModel.model_group

View file

@ -579,17 +579,14 @@ export const modelInfoCall = async (
}
};
export const modelHubCall = async (
accessToken: String,
userID: String,
userRole: String
) => {
export const modelHubCall = async (accessToken: String) => {
/**
* Get all models on proxy
*/
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/model_group/info` : `/model_group/info`;
let url = proxyBaseUrl
? `${proxyBaseUrl}/model_group/info`
: `/model_group/info`;
//message.info("Requesting model data");
const response = await fetch(url, {
@ -617,8 +614,6 @@ export const modelHubCall = async (
}
};
export const modelMetricsCall = async (
accessToken: String,
userID: String,
@ -1870,6 +1865,38 @@ export const getGeneralSettingsCall = async (accessToken: String) => {
}
};
export const getConfigFieldSetting = async (
accessToken: String,
fieldName: string
) => {
try {
let url = proxyBaseUrl
? `${proxyBaseUrl}/config/field/info?field_name=${fieldName}`
: `/config/field/info?field_name=${fieldName}`;
//message.info("Requesting model data");
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.text();
throw new Error("Network response was not ok");
}
const data = await response.json();
return data;
// Handle success - you might want to update some state or UI based on the created key
} catch (error) {
console.error("Failed to set callbacks:", error);
throw error;
}
};
export const updateConfigFieldSetting = async (
accessToken: String,
fieldName: string,