mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +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]
|
||||
|
||||
|
||||
class BudgetDeleteRequest(LiteLLMBase):
|
||||
id: str
|
||||
|
||||
|
||||
class KeyManagementSystem(enum.Enum):
|
||||
GOOGLE_KMS = "google_kms"
|
||||
AZURE_KEY_VAULT = "azure_key_vault"
|
||||
|
|
|
@ -8289,6 +8289,142 @@ async def info_budget(data: BudgetRequest):
|
|||
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 ####
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import Teams from "@/components/teams";
|
|||
import AdminPanel from "@/components/admins";
|
||||
import Settings from "@/components/settings";
|
||||
import GeneralSettings from "@/components/general_settings";
|
||||
import BudgetPanel from "@/components/budgets/budget_panel";
|
||||
import APIRef from "@/components/api_ref";
|
||||
import ChatUI from "@/components/chat_ui";
|
||||
import Sidebar from "../components/leftnav";
|
||||
|
@ -106,14 +107,13 @@ const CreateKeyPage = () => {
|
|||
/>
|
||||
<div className="flex flex-1 overflow-auto">
|
||||
<div className="mt-8">
|
||||
<Sidebar
|
||||
setPage={setPage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={null}
|
||||
/>
|
||||
|
||||
<Sidebar
|
||||
setPage={setPage}
|
||||
userRole={userRole}
|
||||
defaultSelectedKey={null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{page == "api-keys" ? (
|
||||
<UserDashboard
|
||||
userID={userID}
|
||||
|
@ -169,20 +169,22 @@ const CreateKeyPage = () => {
|
|||
showSSOBanner={showSSOBanner}
|
||||
/>
|
||||
) : page == "api_ref" ? (
|
||||
<APIRef/>
|
||||
<APIRef />
|
||||
) : page == "settings" ? (
|
||||
<Settings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
) : page == "general-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : page == "budgets" ? (
|
||||
<BudgetPanel accessToken={accessToken} />
|
||||
) : page == "general-settings" ? (
|
||||
<GeneralSettings
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
modelData={modelData}
|
||||
/>
|
||||
) : (
|
||||
<Usage
|
||||
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;
|
||||
}
|
||||
|
||||
const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) => {
|
||||
const Createuser: React.FC<CreateuserProps> = ({
|
||||
userID,
|
||||
accessToken,
|
||||
teams,
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [apiuser, setApiuser] = useState<string | null>(null);
|
||||
|
@ -73,7 +77,7 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Button2 className="mx-auto" onClick={() => setIsModalVisible(true)}>
|
||||
<Button2 className="mx-auto mb-0" onClick={() => setIsModalVisible(true)}>
|
||||
+ Invite User
|
||||
</Button2>
|
||||
<Modal
|
||||
|
@ -84,8 +88,12 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
|||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
>
|
||||
<Text className="mb-1">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>
|
||||
<Text className="mb-1">
|
||||
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}
|
||||
onFinish={handleCreate}
|
||||
|
@ -97,10 +105,7 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
|||
<TextInput placeholder="" />
|
||||
</Form.Item>
|
||||
<Form.Item label="Team ID" name="team_id">
|
||||
<Select
|
||||
placeholder="Select Team ID"
|
||||
style={{ width: "100%" }}
|
||||
>
|
||||
<Select placeholder="Select Team ID" style={{ width: "100%" }}>
|
||||
{teams ? (
|
||||
teams.map((team: any) => (
|
||||
<Option key={team.team_id} value={team.team_id}>
|
||||
|
@ -131,12 +136,16 @@ const Createuser: React.FC<CreateuserProps> = ({ userID, accessToken, teams }) =
|
|||
footer={null}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Layout, Menu } from "antd";
|
||||
import Link from "next/link";
|
||||
import { List } from "postcss/lib/list";
|
||||
import { Text } from "@tremor/react"
|
||||
import { Text } from "@tremor/react";
|
||||
|
||||
const { Sider } = Layout;
|
||||
|
||||
|
@ -54,88 +54,61 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||
style={{ height: "100%", borderRight: 0 }}
|
||||
>
|
||||
<Menu.Item key="1" onClick={() => setPage("api-keys")}>
|
||||
<Text>
|
||||
API Keys
|
||||
</Text>
|
||||
<Text>API Keys</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="3" onClick={() => setPage("llm-playground")}>
|
||||
<Text>
|
||||
Test Key
|
||||
</Text>
|
||||
<Text>Test Key</Text>
|
||||
</Menu.Item>
|
||||
|
||||
|
||||
|
||||
{
|
||||
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>
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="2" onClick={() => setPage("models")}>
|
||||
<Text>Models</Text>
|
||||
</Menu.Item>
|
||||
|
||||
) : null
|
||||
}
|
||||
) : null}
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="4" onClick={() => setPage("usage")}>
|
||||
<Text>Usage</Text>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="6" onClick={() => setPage("teams")}>
|
||||
<Text>
|
||||
Teams
|
||||
</Text>
|
||||
<Text>Teams</Text>
|
||||
</Menu.Item>
|
||||
) : 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" ? (
|
||||
<Menu.Item key="7" onClick={() => setPage("admin-panel")}>
|
||||
<Text>
|
||||
Admin
|
||||
</Text>
|
||||
<Menu.Item key="5" onClick={() => setPage("users")}>
|
||||
<Text>Users</Text>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
<Menu.Item key="11" onClick={() => setPage("api_ref")}>
|
||||
<Text>
|
||||
API Reference
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
|
||||
{userRole == "Admin" ? (
|
||||
<Menu.Item key="8" onClick={() => setPage("settings")}>
|
||||
<Text>Logging & Alerts</Text>
|
||||
</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>
|
||||
</Sider>
|
||||
</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 (
|
||||
accessToken: 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 (
|
||||
accessToken: String,
|
||||
userID: String,
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
TabPanel,
|
||||
Select,
|
||||
SelectItem,
|
||||
Dialog,
|
||||
Dialog,
|
||||
DialogPanel,
|
||||
Icon,
|
||||
TextInput,
|
||||
|
@ -82,8 +82,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
if (accessToken && token && userRole && userID) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
|
||||
}, [accessToken, token, userRole, userID, currentPage]);
|
||||
|
||||
if (!userData) {
|
||||
|
@ -102,7 +100,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
Showing Page {currentPage+1} of {totalPages}
|
||||
Showing Page {currentPage + 1} of {totalPages}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<button
|
||||
|
@ -129,17 +127,17 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
return (
|
||||
<div style={{ width: "100%" }}>
|
||||
<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">
|
||||
<div className="mb-4 mt-1">
|
||||
<Text>These are Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM</Text>
|
||||
|
||||
</div>
|
||||
<div className="mb-4 mt-1">
|
||||
<Text>
|
||||
These are Users on LiteLLM that created API Keys. Automatically
|
||||
tracked by LiteLLM
|
||||
</Text>
|
||||
</div>
|
||||
<TabGroup>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
|
||||
<Table className="mt-5">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
@ -156,33 +154,46 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
<TableRow key={user.user_id}>
|
||||
<TableCell>{user.user_id}</TableCell>
|
||||
<TableCell>{user.user_email}</TableCell>
|
||||
|
||||
|
||||
<TableCell>
|
||||
{user.models && user.models.length > 0
|
||||
? user.models
|
||||
: "All Models"}
|
||||
</TableCell>
|
||||
<TableCell>{user.spend ? user.spend?.toFixed(2) : 0}</TableCell>
|
||||
<TableCell>
|
||||
{user.spend ? user.spend?.toFixed(2) : 0}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.max_budget ? user.max_budget : "Unlimited"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Grid numItems={2}>
|
||||
{user && user.key_aliases
|
||||
? user.key_aliases.filter((key: any) => key !== null).length > 0
|
||||
? <Badge size={"xs"} color={"indigo"}>{user.key_aliases.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= {() => {
|
||||
{user && user.key_aliases ? (
|
||||
user.key_aliases.filter(
|
||||
(key: any) => key !== null
|
||||
).length > 0 ? (
|
||||
<Badge size={"xs"} color={"indigo"}>
|
||||
{user.key_aliases
|
||||
.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)
|
||||
setSelectedItem(user)
|
||||
}}>View Keys</Icon> */}
|
||||
|
||||
</Grid>
|
||||
|
||||
</TableCell>
|
||||
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
@ -191,9 +202,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
<TabPanel>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex-1 flex justify-between items-center">
|
||||
|
||||
</div>
|
||||
<div className="flex-1 flex justify-between items-center"></div>
|
||||
</div>
|
||||
{/* <Table className="max-h-[70vh] min-h-[500px]">
|
||||
<TableHead>
|
||||
|
@ -242,7 +251,6 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
</DialogPanel>
|
||||
</Dialog> */}
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue