mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 19:24:27 +00:00
Merge pull request #3787 from BerriAI/litellm_set_budgets_ui_2
feat(proxy_server.py): enable admin to set tpm/rpm limits for end-users via UI
This commit is contained in:
commit
3400596dd2
10 changed files with 931 additions and 122 deletions
|
@ -706,6 +706,10 @@ class BudgetRequest(LiteLLMBase):
|
||||||
budgets: List[str]
|
budgets: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
class BudgetDeleteRequest(LiteLLMBase):
|
||||||
|
id: str
|
||||||
|
|
||||||
|
|
||||||
class KeyManagementSystem(enum.Enum):
|
class KeyManagementSystem(enum.Enum):
|
||||||
GOOGLE_KMS = "google_kms"
|
GOOGLE_KMS = "google_kms"
|
||||||
AZURE_KEY_VAULT = "azure_key_vault"
|
AZURE_KEY_VAULT = "azure_key_vault"
|
||||||
|
|
|
@ -8289,6 +8289,142 @@ async def info_budget(data: BudgetRequest):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/budget/settings",
|
||||||
|
tags=["budget management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
async def budget_settings(
|
||||||
|
budget_id: str,
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get list of configurable params + current value for a budget item + description of each field
|
||||||
|
|
||||||
|
Used on Admin UI.
|
||||||
|
"""
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
## get budget item from db
|
||||||
|
db_budget_row = await prisma_client.db.litellm_budgettable.find_first(
|
||||||
|
where={"budget_id": budget_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
if db_budget_row is not None:
|
||||||
|
db_budget_row_dict = db_budget_row.model_dump(exclude_none=True)
|
||||||
|
else:
|
||||||
|
db_budget_row_dict = {}
|
||||||
|
|
||||||
|
allowed_args = {
|
||||||
|
"max_parallel_requests": {"type": "Integer"},
|
||||||
|
"tpm_limit": {"type": "Integer"},
|
||||||
|
"rpm_limit": {"type": "Integer"},
|
||||||
|
"budget_duration": {"type": "String"},
|
||||||
|
"max_budget": {"type": "Float"},
|
||||||
|
"soft_budget": {"type": "Float"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return_val = []
|
||||||
|
|
||||||
|
for field_name, field_info in BudgetNew.model_fields.items():
|
||||||
|
if field_name in allowed_args:
|
||||||
|
|
||||||
|
_stored_in_db = True
|
||||||
|
|
||||||
|
_response_obj = ConfigList(
|
||||||
|
field_name=field_name,
|
||||||
|
field_type=allowed_args[field_name]["type"],
|
||||||
|
field_description=field_info.description or "",
|
||||||
|
field_value=db_budget_row_dict.get(field_name, None),
|
||||||
|
stored_in_db=_stored_in_db,
|
||||||
|
)
|
||||||
|
return_val.append(_response_obj)
|
||||||
|
|
||||||
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/budget/list",
|
||||||
|
tags=["budget management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
async def list_budget(
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""List all the created budgets in proxy db. Used on Admin UI."""
|
||||||
|
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_budgettable.find_many()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/budget/delete",
|
||||||
|
tags=["budget management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
async def delete_budget(
|
||||||
|
data: BudgetDeleteRequest,
|
||||||
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||||
|
):
|
||||||
|
"""Delete budget"""
|
||||||
|
global prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
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_budgettable.delete(
|
||||||
|
where={"budget_id": data.id}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
#### MODEL MANAGEMENT ####
|
#### MODEL MANAGEMENT ####
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Teams from "@/components/teams";
|
||||||
import AdminPanel from "@/components/admins";
|
import AdminPanel from "@/components/admins";
|
||||||
import Settings from "@/components/settings";
|
import Settings from "@/components/settings";
|
||||||
import GeneralSettings from "@/components/general_settings";
|
import GeneralSettings from "@/components/general_settings";
|
||||||
|
import BudgetPanel from "@/components/budgets/budget_panel";
|
||||||
import APIRef from "@/components/api_ref";
|
import APIRef from "@/components/api_ref";
|
||||||
import ChatUI from "@/components/chat_ui";
|
import ChatUI from "@/components/chat_ui";
|
||||||
import Sidebar from "../components/leftnav";
|
import Sidebar from "../components/leftnav";
|
||||||
|
@ -106,14 +107,13 @@ const CreateKeyPage = () => {
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 overflow-auto">
|
<div className="flex flex-1 overflow-auto">
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
setPage={setPage}
|
setPage={setPage}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
defaultSelectedKey={null}
|
defaultSelectedKey={null}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{page == "api-keys" ? (
|
{page == "api-keys" ? (
|
||||||
<UserDashboard
|
<UserDashboard
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
@ -169,20 +169,22 @@ const CreateKeyPage = () => {
|
||||||
showSSOBanner={showSSOBanner}
|
showSSOBanner={showSSOBanner}
|
||||||
/>
|
/>
|
||||||
) : page == "api_ref" ? (
|
) : page == "api_ref" ? (
|
||||||
<APIRef/>
|
<APIRef />
|
||||||
) : page == "settings" ? (
|
) : page == "settings" ? (
|
||||||
<Settings
|
<Settings
|
||||||
userID={userID}
|
userID={userID}
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
/>
|
/>
|
||||||
) : page == "general-settings" ? (
|
) : page == "budgets" ? (
|
||||||
<GeneralSettings
|
<BudgetPanel accessToken={accessToken} />
|
||||||
userID={userID}
|
) : page == "general-settings" ? (
|
||||||
userRole={userRole}
|
<GeneralSettings
|
||||||
accessToken={accessToken}
|
userID={userID}
|
||||||
modelData={modelData}
|
userRole={userRole}
|
||||||
/>
|
accessToken={accessToken}
|
||||||
|
modelData={modelData}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Usage
|
<Usage
|
||||||
userID={userID}
|
userID={userID}
|
||||||
|
|
140
ui/litellm-dashboard/src/components/budgets/budget_modal.tsx
Normal file
140
ui/litellm-dashboard/src/components/budgets/budget_modal.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
TextInput,
|
||||||
|
Grid,
|
||||||
|
Col,
|
||||||
|
Accordion,
|
||||||
|
AccordionHeader,
|
||||||
|
AccordionBody,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import {
|
||||||
|
Button as Button2,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
} from "antd";
|
||||||
|
import { budgetCreateCall } from "../networking";
|
||||||
|
|
||||||
|
interface BudgetModalProps {
|
||||||
|
isModalVisible: boolean;
|
||||||
|
accessToken: string | null;
|
||||||
|
setIsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setBudgetList: React.Dispatch<React.SetStateAction<any[]>>;
|
||||||
|
}
|
||||||
|
const BudgetModal: React.FC<BudgetModalProps> = ({
|
||||||
|
isModalVisible,
|
||||||
|
accessToken,
|
||||||
|
setIsModalVisible,
|
||||||
|
setBudgetList,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const handleOk = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (formValues: Record<string, any>) => {
|
||||||
|
if (accessToken == null || accessToken == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
message.info("Making API Call");
|
||||||
|
// setIsModalVisible(true);
|
||||||
|
const response = await budgetCreateCall(accessToken, formValues);
|
||||||
|
console.log("key create Response:", response);
|
||||||
|
setBudgetList((prevData) =>
|
||||||
|
prevData ? [...prevData, response] : [response]
|
||||||
|
); // Check if prevData is null
|
||||||
|
message.success("API Key Created");
|
||||||
|
form.resetFields();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating the key:", error);
|
||||||
|
message.error(`Error creating the key: ${error}`, 20);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Create Budget"
|
||||||
|
visible={isModalVisible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleCreate}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
label="Budget ID"
|
||||||
|
name="budget_id"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please input a human-friendly name for the budget",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
help="A human-friendly name for the budget"
|
||||||
|
>
|
||||||
|
<TextInput placeholder="" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Max Tokens per minute"
|
||||||
|
name="tpm_limit"
|
||||||
|
help="Default is model limit."
|
||||||
|
>
|
||||||
|
<InputNumber step={1} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Max Requests per minute"
|
||||||
|
name="rpm_limit"
|
||||||
|
help="Default is model limit."
|
||||||
|
>
|
||||||
|
<InputNumber step={1} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Accordion className="mt-20 mb-8">
|
||||||
|
<AccordionHeader>
|
||||||
|
<b>Optional Settings</b>
|
||||||
|
</AccordionHeader>
|
||||||
|
<AccordionBody>
|
||||||
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||||
|
<InputNumber step={0.01} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
className="mt-8"
|
||||||
|
label="Reset Budget"
|
||||||
|
name="budget_duration"
|
||||||
|
>
|
||||||
|
<Select defaultValue={null} placeholder="n/a">
|
||||||
|
<Select.Option value="24h">daily</Select.Option>
|
||||||
|
<Select.Option value="30d">monthly</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</AccordionBody>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button2 htmlType="submit">Create Budget</Button2>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetModal;
|
201
ui/litellm-dashboard/src/components/budgets/budget_panel.tsx
Normal file
201
ui/litellm-dashboard/src/components/budgets/budget_panel.tsx
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/**
|
||||||
|
* The parent pane, showing list of budgets
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import BudgetSettings from "./budget_settings";
|
||||||
|
import BudgetModal from "./budget_modal";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableFoot,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableRow,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
Tab,
|
||||||
|
TabGroup,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Grid,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
PencilAltIcon,
|
||||||
|
PencilIcon,
|
||||||
|
StatusOnlineIcon,
|
||||||
|
TrashIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
QuestionMarkCircleIcon,
|
||||||
|
} from "@heroicons/react/outline";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { getBudgetList, budgetDeleteCall } from "../networking";
|
||||||
|
import { message } from "antd";
|
||||||
|
interface BudgetSettingsPageProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface budgetItem {
|
||||||
|
budget_id: string;
|
||||||
|
max_budget: string | null;
|
||||||
|
rpm_limit: number | null;
|
||||||
|
tpm_limit: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetPanel: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [budgetList, setBudgetList] = useState<budgetItem[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getBudgetList(accessToken).then((data) => {
|
||||||
|
setBudgetList(data);
|
||||||
|
});
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
|
const handleDeleteCall = async (budget_id: string, index: number) => {
|
||||||
|
if (accessToken == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.info("Request made");
|
||||||
|
|
||||||
|
await budgetDeleteCall(accessToken, budget_id);
|
||||||
|
|
||||||
|
const newBudgetList = [...budgetList];
|
||||||
|
newBudgetList.splice(index, 1);
|
||||||
|
setBudgetList(newBudgetList);
|
||||||
|
|
||||||
|
message.success("Budget Deleted.");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full mx-auto flex-auto overflow-y-auto m-8 p-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
className="mb-2"
|
||||||
|
onClick={() => setIsModalVisible(true)}
|
||||||
|
>
|
||||||
|
+ Create Budget
|
||||||
|
</Button>
|
||||||
|
<BudgetModal
|
||||||
|
accessToken={accessToken}
|
||||||
|
isModalVisible={isModalVisible}
|
||||||
|
setIsModalVisible={setIsModalVisible}
|
||||||
|
setBudgetList={setBudgetList}
|
||||||
|
/>
|
||||||
|
<Card>
|
||||||
|
<Text>Create a budget to assign to customers.</Text>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Budget ID</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Max Budget</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM</TableHeaderCell>
|
||||||
|
<TableHeaderCell>RPM</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{budgetList.map((value: budgetItem, index: number) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{value.budget_id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{value.max_budget ? value.max_budget : "n/a"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{value.tpm_limit ? value.tpm_limit : "n/a"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{value.rpm_limit ? value.rpm_limit : "n/a"}
|
||||||
|
</TableCell>
|
||||||
|
<Icon
|
||||||
|
icon={TrashIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteCall(value.budget_id, index)}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
<div className="mt-5">
|
||||||
|
<Text className="text-base">How to use budget id</Text>
|
||||||
|
<TabGroup>
|
||||||
|
<TabList>
|
||||||
|
<Tab>Assign Budget to Customer</Tab>
|
||||||
|
<Tab>Test it (Curl)</Tab>
|
||||||
|
|
||||||
|
<Tab>Test it (OpenAI SDK)</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<SyntaxHighlighter language="bash">
|
||||||
|
{`
|
||||||
|
curl -X POST --location '<your_proxy_base_url>/end_user/new' \
|
||||||
|
|
||||||
|
-H 'Authorization: Bearer <your-master-key>' \
|
||||||
|
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
|
||||||
|
-d '{"user_id": "my-customer-id', "budget_id": "<BUDGET_ID>"}' # 👈 KEY CHANGE
|
||||||
|
|
||||||
|
`}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<SyntaxHighlighter language="bash">
|
||||||
|
{`
|
||||||
|
curl -X POST --location '<your_proxy_base_url>/chat/completions' \
|
||||||
|
|
||||||
|
-H 'Authorization: Bearer <your-master-key>' \
|
||||||
|
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
|
||||||
|
-d '{
|
||||||
|
"model": "gpt-3.5-turbo',
|
||||||
|
"messages":[{"role": "user", "content": "Hey, how's it going?"}],
|
||||||
|
"user": "my-customer-id"
|
||||||
|
}' # 👈 KEY CHANGE
|
||||||
|
|
||||||
|
`}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<SyntaxHighlighter language="python">
|
||||||
|
{`from openai import OpenAI
|
||||||
|
client = OpenAI(
|
||||||
|
base_url="<your_proxy_base_url",
|
||||||
|
api_key="<your_proxy_key>"
|
||||||
|
)
|
||||||
|
|
||||||
|
completion = client.chat.completions.create(
|
||||||
|
model="gpt-3.5-turbo",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "You are a helpful assistant."},
|
||||||
|
{"role": "user", "content": "Hello!"}
|
||||||
|
],
|
||||||
|
user="my-customer-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(completion.choices[0].message)`}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetPanel;
|
195
ui/litellm-dashboard/src/components/budgets/budget_settings.tsx
Normal file
195
ui/litellm-dashboard/src/components/budgets/budget_settings.tsx
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Badge,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableCell,
|
||||||
|
TableBody,
|
||||||
|
Metric,
|
||||||
|
Text,
|
||||||
|
Grid,
|
||||||
|
Button,
|
||||||
|
TextInput,
|
||||||
|
Select as Select2,
|
||||||
|
SelectItem,
|
||||||
|
Col,
|
||||||
|
Accordion,
|
||||||
|
AccordionBody,
|
||||||
|
AccordionHeader,
|
||||||
|
AccordionList,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import {
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
TabGroup,
|
||||||
|
TabList,
|
||||||
|
Tab,
|
||||||
|
Icon,
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { getBudgetSettings } from "../networking";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Button as Button2,
|
||||||
|
message,
|
||||||
|
InputNumber,
|
||||||
|
} from "antd";
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
PencilAltIcon,
|
||||||
|
PencilIcon,
|
||||||
|
StatusOnlineIcon,
|
||||||
|
TrashIcon,
|
||||||
|
RefreshIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
QuestionMarkCircleIcon,
|
||||||
|
} from "@heroicons/react/outline";
|
||||||
|
import StaticGenerationSearchParamsBailoutProvider from "next/dist/client/components/static-generation-searchparams-bailout-provider";
|
||||||
|
import AddFallbacks from "../add_fallbacks";
|
||||||
|
import openai from "openai";
|
||||||
|
import Paragraph from "antd/es/skeleton/Paragraph";
|
||||||
|
|
||||||
|
interface BudgetSettingsPageProps {
|
||||||
|
accessToken: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface budgetSettingsItem {
|
||||||
|
field_name: string;
|
||||||
|
field_type: string;
|
||||||
|
field_value: any;
|
||||||
|
field_description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
|
||||||
|
const [budgetSettings, setBudgetSettings] = useState<budgetSettingsItem[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getBudgetSettings(accessToken).then((data) => {
|
||||||
|
console.log("budget settings", data);
|
||||||
|
let budget_settings = data.budget_settings;
|
||||||
|
setBudgetSettings(budget_settings);
|
||||||
|
});
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
|
const handleInputChange = (fieldName: string, newValue: any) => {
|
||||||
|
// Update the value in the state
|
||||||
|
const updatedSettings = budgetSettings.map((setting) =>
|
||||||
|
setting.field_name === fieldName
|
||||||
|
? { ...setting, field_value: newValue }
|
||||||
|
: setting
|
||||||
|
);
|
||||||
|
setBudgetSettings(updatedSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateField = (fieldName: string, idx: number) => {
|
||||||
|
if (!accessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fieldValue = budgetSettings[idx].field_value;
|
||||||
|
|
||||||
|
if (fieldValue == null || fieldValue == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const updatedSettings = budgetSettings.map((setting) =>
|
||||||
|
setting.field_name === fieldName
|
||||||
|
? { ...setting, stored_in_db: true }
|
||||||
|
: setting
|
||||||
|
);
|
||||||
|
setBudgetSettings(updatedSettings);
|
||||||
|
} catch (error) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetField = (fieldName: string, idx: number) => {
|
||||||
|
if (!accessToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedSettings = budgetSettings.map((setting) =>
|
||||||
|
setting.field_name === fieldName
|
||||||
|
? { ...setting, stored_in_db: null, field_value: null }
|
||||||
|
: setting
|
||||||
|
);
|
||||||
|
setBudgetSettings(updatedSettings);
|
||||||
|
} catch (error) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full mx-4">
|
||||||
|
<Card>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Setting</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Value</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{budgetSettings.map((value, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>
|
||||||
|
<Text>{value.field_name}</Text>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "#808080",
|
||||||
|
fontStyle: "italic",
|
||||||
|
}}
|
||||||
|
className="mt-1"
|
||||||
|
>
|
||||||
|
{value.field_description}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{value.field_type == "Integer" ? (
|
||||||
|
<InputNumber
|
||||||
|
step={1}
|
||||||
|
value={value.field_value}
|
||||||
|
onChange={(newValue) =>
|
||||||
|
handleInputChange(value.field_name, newValue)
|
||||||
|
} // Handle value change
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleUpdateField(value.field_name, index)}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<Icon
|
||||||
|
icon={TrashIcon}
|
||||||
|
color="red"
|
||||||
|
onClick={() => handleResetField(value.field_name, index)}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Icon>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BudgetSettings;
|
|
@ -10,7 +10,11 @@ interface CreateuserProps {
|
||||||
teams: any[] | null;
|
teams: any[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) => {
|
const Createuser: React.FC<CreateuserProps> = ({
|
||||||
|
userID,
|
||||||
|
accessToken,
|
||||||
|
teams,
|
||||||
|
}) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [apiuser, setApiuser] = useState<string | null>(null);
|
const [apiuser, setApiuser] = useState<string | null>(null);
|
||||||
|
@ -73,7 +77,7 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button2 className="mx-auto" onClick={() => setIsModalVisible(true)}>
|
<Button2 className="mx-auto mb-0" onClick={() => setIsModalVisible(true)}>
|
||||||
+ Invite User
|
+ Invite User
|
||||||
</Button2>
|
</Button2>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -84,8 +88,12 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
||||||
onOk={handleOk}
|
onOk={handleOk}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
>
|
>
|
||||||
<Text className="mb-1">Invite a user to login to the Admin UI and create Keys</Text>
|
<Text className="mb-1">
|
||||||
<Text className="mb-6"><b>Note: SSO Setup Required for this</b></Text>
|
Invite a user to login to the Admin UI and create Keys
|
||||||
|
</Text>
|
||||||
|
<Text className="mb-6">
|
||||||
|
<b>Note: SSO Setup Required for this</b>
|
||||||
|
</Text>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
onFinish={handleCreate}
|
onFinish={handleCreate}
|
||||||
|
@ -97,10 +105,7 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
||||||
<TextInput placeholder="" />
|
<TextInput placeholder="" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="Team ID" name="team_id">
|
<Form.Item label="Team ID" name="team_id">
|
||||||
<Select
|
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
|
||||||
placeholder="Select Team ID"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{teams ? (
|
{teams ? (
|
||||||
teams.map((team: any) => (
|
teams.map((team: any) => (
|
||||||
<Option key={team.team_id} value={team.team_id}>
|
<Option key={team.team_id} value={team.team_id}>
|
||||||
|
@ -131,12 +136,16 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
User has been created to access your proxy. Please Ask them to Log In.
|
User has been created to access your proxy. Please Ask them to Log
|
||||||
|
In.
|
||||||
</p>
|
</p>
|
||||||
<br></br>
|
<br></br>
|
||||||
|
|
||||||
<p><b>Note: This Feature is only supported through SSO on the Admin UI</b></p>
|
<p>
|
||||||
|
<b>
|
||||||
|
Note: This Feature is only supported through SSO on the Admin UI
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
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";
|
import { List } from "postcss/lib/list";
|
||||||
import { Text } from "@tremor/react"
|
import { Text } from "@tremor/react";
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
@ -54,88 +54,61 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||||
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")}>
|
||||||
<Text>
|
<Text>API Keys</Text>
|
||||||
API Keys
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item key="3" onClick={() => setPage("llm-playground")}>
|
<Menu.Item key="3" onClick={() => setPage("llm-playground")}>
|
||||||
<Text>
|
<Text>Test Key</Text>
|
||||||
Test Key
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
|
{userRole == "Admin" ? (
|
||||||
|
<Menu.Item key="2" onClick={() => setPage("models")}>
|
||||||
{
|
<Text>Models</Text>
|
||||||
userRole == "Admin" ? (
|
|
||||||
<Menu.Item key="2" onClick={() => setPage("models")}>
|
|
||||||
<Text>
|
|
||||||
Models
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
userRole == "Admin" ? (
|
|
||||||
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
|
||||||
<Text>
|
|
||||||
Usage
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
) : null
|
{userRole == "Admin" ? (
|
||||||
}
|
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
||||||
|
<Text>Usage</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{userRole == "Admin" ? (
|
{userRole == "Admin" ? (
|
||||||
<Menu.Item key="6" onClick={() => setPage("teams")}>
|
<Menu.Item key="6" onClick={() => setPage("teams")}>
|
||||||
<Text>
|
<Text>Teams</Text>
|
||||||
Teams
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{userRole == "Admin" ? (
|
|
||||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
|
||||||
<Text>
|
|
||||||
Users
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{
|
|
||||||
userRole == "Admin" ? (
|
|
||||||
<Menu.Item key="8" onClick={() => setPage("settings")}>
|
|
||||||
<Text>
|
|
||||||
Logging & Alerts
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
userRole == "Admin" ? (
|
|
||||||
<Menu.Item key="9" onClick={() => setPage("general-settings")}>
|
|
||||||
<Text>
|
|
||||||
Router Settings
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{userRole == "Admin" ? (
|
{userRole == "Admin" ? (
|
||||||
<Menu.Item key="7" onClick={() => setPage("admin-panel")}>
|
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||||
<Text>
|
<Text>Users</Text>
|
||||||
Admin
|
|
||||||
</Text>
|
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
) : null}
|
) : null}
|
||||||
<Menu.Item key="11" onClick={() => setPage("api_ref")}>
|
|
||||||
<Text>
|
{userRole == "Admin" ? (
|
||||||
API Reference
|
<Menu.Item key="8" onClick={() => setPage("settings")}>
|
||||||
</Text>
|
<Text>Logging & Alerts</Text>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userRole == "Admin" ? (
|
||||||
|
<Menu.Item key="9" onClick={() => setPage("budgets")}>
|
||||||
|
<Text>Rate Limits</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userRole == "Admin" ? (
|
||||||
|
<Menu.Item key="10" onClick={() => setPage("general-settings")}>
|
||||||
|
<Text>Router Settings</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{userRole == "Admin" ? (
|
||||||
|
<Menu.Item key="11" onClick={() => setPage("admin-panel")}>
|
||||||
|
<Text>Admin</Text>
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
<Menu.Item key="12" onClick={() => setPage("api_ref")}>
|
||||||
|
<Text>API Reference</Text>
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -97,6 +97,82 @@ export const modelDeleteCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const budgetDeleteCall = async (
|
||||||
|
accessToken: string | null,
|
||||||
|
budget_id: string
|
||||||
|
) => {
|
||||||
|
console.log(`budget_id in budget delete call: ${budget_id}`);
|
||||||
|
|
||||||
|
if (accessToken == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/budget/delete`
|
||||||
|
: `/budget/delete`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: budget_id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
message.error("Failed to create key: " + errorData, 10);
|
||||||
|
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;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const budgetCreateCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
formValues: Record<string, any> // Assuming formValues is an object
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("Form Values in budgetCreateCall:", formValues); // Log the form values before making the API call
|
||||||
|
|
||||||
|
console.log("Form Values after check:", formValues);
|
||||||
|
const url = proxyBaseUrl ? `${proxyBaseUrl}/budget/new` : `/budget/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, 10);
|
||||||
|
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 const keyCreateCall = async (
|
export const keyCreateCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
userID: string,
|
userID: string,
|
||||||
|
@ -1421,6 +1497,71 @@ export const serviceHealthCheck = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBudgetList = async (accessToken: String) => {
|
||||||
|
/**
|
||||||
|
* Get all configurable params for setting a budget
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/budget/list` : `/budget/list`;
|
||||||
|
|
||||||
|
//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();
|
||||||
|
message.error(errorData, 10);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
//message.info("Received model 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 get callbacks:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const getBudgetSettings = async (accessToken: String) => {
|
||||||
|
/**
|
||||||
|
* Get all configurable params for setting a budget
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/budget/settings`
|
||||||
|
: `/budget/settings`;
|
||||||
|
|
||||||
|
//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();
|
||||||
|
message.error(errorData, 10);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
//message.info("Received model 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 get callbacks:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getCallbacksCall = async (
|
export const getCallbacksCall = async (
|
||||||
accessToken: String,
|
accessToken: String,
|
||||||
userID: String,
|
userID: String,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
TabPanel,
|
TabPanel,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogPanel,
|
DialogPanel,
|
||||||
Icon,
|
Icon,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
@ -82,8 +82,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
if (accessToken && token && userRole && userID) {
|
if (accessToken && token && userRole && userID) {
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}, [accessToken, token, userRole, userID, currentPage]);
|
}, [accessToken, token, userRole, userID, currentPage]);
|
||||||
|
|
||||||
if (!userData) {
|
if (!userData) {
|
||||||
|
@ -102,7 +100,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
Showing Page {currentPage+1} of {totalPages}
|
Showing Page {currentPage + 1} of {totalPages}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
|
@ -129,17 +127,17 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Grid className="gap-2 p-2 h-[80vh] w-full mt-8">
|
<Grid className="gap-2 p-2 h-[80vh] w-full mt-8">
|
||||||
<CreateUser userID={userID} accessToken={accessToken} teams={teams}/>
|
<CreateUser userID={userID} accessToken={accessToken} teams={teams} />
|
||||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
|
||||||
<div className="mb-4 mt-1">
|
<div className="mb-4 mt-1">
|
||||||
<Text>These are Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM</Text>
|
<Text>
|
||||||
|
These are Users on LiteLLM that created API Keys. Automatically
|
||||||
</div>
|
tracked by LiteLLM
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
|
||||||
<Table className="mt-5">
|
<Table className="mt-5">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
@ -156,33 +154,46 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
<TableRow key={user.user_id}>
|
<TableRow key={user.user_id}>
|
||||||
<TableCell>{user.user_id}</TableCell>
|
<TableCell>{user.user_id}</TableCell>
|
||||||
<TableCell>{user.user_email}</TableCell>
|
<TableCell>{user.user_email}</TableCell>
|
||||||
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{user.models && user.models.length > 0
|
{user.models && user.models.length > 0
|
||||||
? user.models
|
? user.models
|
||||||
: "All Models"}
|
: "All Models"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{user.spend ? user.spend?.toFixed(2) : 0}</TableCell>
|
<TableCell>
|
||||||
|
{user.spend ? user.spend?.toFixed(2) : 0}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{user.max_budget ? user.max_budget : "Unlimited"}
|
{user.max_budget ? user.max_budget : "Unlimited"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Grid numItems={2}>
|
<Grid numItems={2}>
|
||||||
{user && user.key_aliases
|
{user && user.key_aliases ? (
|
||||||
? user.key_aliases.filter((key: any) => key !== null).length > 0
|
user.key_aliases.filter(
|
||||||
? <Badge size={"xs"} color={"indigo"}>{user.key_aliases.filter((key: any) => key !== null).join(', ') }</Badge>
|
(key: any) => key !== null
|
||||||
: <Badge size={"xs"} color={"gray"}>No Keys</Badge>
|
).length > 0 ? (
|
||||||
: <Badge size={"xs"} color={"gray"}>No Keys</Badge>}
|
<Badge size={"xs"} color={"indigo"}>
|
||||||
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
|
{user.key_aliases
|
||||||
{/* <Icon icon={InformationCircleIcon} onClick= {() => {
|
.filter((key: any) => key !== null)
|
||||||
|
.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge size={"xs"} color={"gray"}>
|
||||||
|
No Keys
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Badge size={"xs"} color={"gray"}>
|
||||||
|
No Keys
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{/* <Text>{user.key_aliases.filter(key => key !== null).length} Keys</Text> */}
|
||||||
|
{/* <Icon icon={InformationCircleIcon} onClick= {() => {
|
||||||
setOpenDialogId(user.user_id)
|
setOpenDialogId(user.user_id)
|
||||||
setSelectedItem(user)
|
setSelectedItem(user)
|
||||||
}}>View Keys</Icon> */}
|
}}>View Keys</Icon> */}
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
@ -191,9 +202,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex-1"></div>
|
<div className="flex-1"></div>
|
||||||
<div className="flex-1 flex justify-between items-center">
|
<div className="flex-1 flex justify-between items-center"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* <Table className="max-h-[70vh] min-h-[500px]">
|
{/* <Table className="max-h-[70vh] min-h-[500px]">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
@ -242,7 +251,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
||||||
</DialogPanel>
|
</DialogPanel>
|
||||||
</Dialog> */}
|
</Dialog> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue