mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 02:34:29 +00:00
496 lines
No EOL
16 KiB
TypeScript
496 lines
No EOL
16 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import NumericalInput from "../shared/numerical_input";
|
|
import {
|
|
Card,
|
|
Title,
|
|
Text,
|
|
Tab,
|
|
TabList,
|
|
TabGroup,
|
|
TabPanel,
|
|
TabPanels,
|
|
Grid,
|
|
Badge,
|
|
Button as TremorButton,
|
|
TableRow,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeaderCell,
|
|
TableBody,
|
|
Table,
|
|
Icon
|
|
} from "@tremor/react";
|
|
import TeamMembersComponent from "./team_member_view";
|
|
import MemberPermissions from "./member_permissions";
|
|
import { teamInfoCall, teamMemberDeleteCall, teamMemberAddCall, teamMemberUpdateCall, Member, teamUpdateCall } from "@/components/networking";
|
|
import { Button, Form, Input, Select, message, Tooltip } from "antd";
|
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
|
import {
|
|
Select as Select2,
|
|
} from "antd";
|
|
import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline";
|
|
import MemberModal from "./edit_membership";
|
|
import UserSearchModal from "@/components/common_components/user_search_modal";
|
|
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
|
import { isAdminRole } from "@/utils/roles";
|
|
|
|
export interface TeamData {
|
|
team_id: string;
|
|
team_info: {
|
|
team_alias: string;
|
|
team_id: string;
|
|
organization_id: string | null;
|
|
admins: string[];
|
|
members: string[];
|
|
members_with_roles: Member[];
|
|
metadata: Record<string, any>;
|
|
tpm_limit: number | null;
|
|
rpm_limit: number | null;
|
|
max_budget: number | null;
|
|
budget_duration: string | null;
|
|
models: string[];
|
|
blocked: boolean;
|
|
spend: number;
|
|
max_parallel_requests: number | null;
|
|
budget_reset_at: string | null;
|
|
model_id: string | null;
|
|
litellm_model_table: {
|
|
model_aliases: Record<string, string>;
|
|
} | null;
|
|
created_at: string;
|
|
};
|
|
keys: any[];
|
|
team_memberships: any[];
|
|
}
|
|
|
|
export interface TeamInfoProps {
|
|
teamId: string;
|
|
onUpdate: (data: any) => void;
|
|
onClose: () => void;
|
|
accessToken: string | null;
|
|
is_team_admin: boolean;
|
|
is_proxy_admin: boolean;
|
|
userModels: string[];
|
|
editTeam: boolean;
|
|
}
|
|
|
|
const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|
teamId,
|
|
onClose,
|
|
accessToken,
|
|
is_team_admin,
|
|
is_proxy_admin,
|
|
userModels,
|
|
editTeam
|
|
}) => {
|
|
const [teamData, setTeamData] = useState<TeamData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
|
const [form] = Form.useForm();
|
|
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
|
|
const [selectedEditMember, setSelectedEditMember] = useState<Member | null>(null);
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
console.log("userModels in team info", userModels);
|
|
|
|
const canEditTeam = is_team_admin || is_proxy_admin;
|
|
|
|
const fetchTeamInfo = async () => {
|
|
try {
|
|
setLoading(true);
|
|
if (!accessToken) return;
|
|
const response = await teamInfoCall(accessToken, teamId);
|
|
setTeamData(response);
|
|
} catch (error) {
|
|
message.error("Failed to load team information");
|
|
console.error("Error fetching team info:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchTeamInfo();
|
|
}, [teamId, accessToken]);
|
|
|
|
const handleMemberCreate = async (values: any) => {
|
|
try {
|
|
if (accessToken == null) {
|
|
return;
|
|
}
|
|
|
|
const member: Member = {
|
|
user_email: values.user_email,
|
|
user_id: values.user_id,
|
|
role: values.role,
|
|
}
|
|
const response = await teamMemberAddCall(accessToken, teamId, member);
|
|
|
|
message.success("Team member added successfully");
|
|
setIsAddMemberModalVisible(false);
|
|
form.resetFields();
|
|
fetchTeamInfo();
|
|
} catch (error) {
|
|
message.error("Failed to add team member");
|
|
console.error("Error adding team member:", error);
|
|
}
|
|
};
|
|
|
|
const handleMemberUpdate = async (values: any) => {
|
|
try {
|
|
if (accessToken == null) {
|
|
return;
|
|
}
|
|
|
|
const member: Member = {
|
|
user_email: values.user_email,
|
|
user_id: values.user_id,
|
|
role: values.role,
|
|
}
|
|
|
|
const response = await teamMemberUpdateCall(accessToken, teamId, member);
|
|
|
|
message.success("Team member updated successfully");
|
|
setIsEditMemberModalVisible(false);
|
|
fetchTeamInfo();
|
|
} catch (error) {
|
|
message.error("Failed to update team member");
|
|
console.error("Error updating team member:", error);
|
|
}
|
|
};
|
|
|
|
const handleMemberDelete = async (member: Member) => {
|
|
try {
|
|
if (accessToken == null) {
|
|
return;
|
|
}
|
|
|
|
const response = await teamMemberDeleteCall(accessToken, teamId, member);
|
|
|
|
message.success("Team member removed successfully");
|
|
fetchTeamInfo();
|
|
} catch (error) {
|
|
message.error("Failed to remove team member");
|
|
console.error("Error removing team member:", error);
|
|
}
|
|
};
|
|
|
|
const handleTeamUpdate = async (values: any) => {
|
|
try {
|
|
if (!accessToken) return;
|
|
|
|
let parsedMetadata = {};
|
|
try {
|
|
parsedMetadata = values.metadata ? JSON.parse(values.metadata) : {};
|
|
} catch (e) {
|
|
message.error("Invalid JSON in metadata field");
|
|
return;
|
|
}
|
|
|
|
const updateData = {
|
|
team_id: teamId,
|
|
team_alias: values.team_alias,
|
|
models: values.models,
|
|
tpm_limit: values.tpm_limit,
|
|
rpm_limit: values.rpm_limit,
|
|
max_budget: values.max_budget,
|
|
budget_duration: values.budget_duration,
|
|
metadata: {
|
|
...parsedMetadata,
|
|
guardrails: values.guardrails || []
|
|
}
|
|
};
|
|
|
|
const response = await teamUpdateCall(accessToken, updateData);
|
|
|
|
message.success("Team settings updated successfully");
|
|
setIsEditing(false);
|
|
fetchTeamInfo();
|
|
} catch (error) {
|
|
message.error("Failed to update team settings");
|
|
console.error("Error updating team:", error);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="p-4">Loading...</div>;
|
|
}
|
|
|
|
if (!teamData?.team_info) {
|
|
return <div className="p-4">Team not found</div>;
|
|
}
|
|
|
|
const { team_info: info } = teamData;
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<div>
|
|
<Button onClick={onClose} className="mb-4">← Back</Button>
|
|
<Title>{info.team_alias}</Title>
|
|
<Text className="text-gray-500 font-mono">{info.team_id}</Text>
|
|
</div>
|
|
</div>
|
|
|
|
<TabGroup defaultIndex={editTeam ? 3 : 0}>
|
|
<TabList className="mb-4">
|
|
{[
|
|
<Tab key="overview">Overview</Tab>,
|
|
...(canEditTeam ? [
|
|
<Tab key="members">Members</Tab>,
|
|
<Tab key="member-permissions">Member Permissions</Tab>,
|
|
<Tab key="settings">Settings</Tab>
|
|
] : [])
|
|
]}
|
|
</TabList>
|
|
|
|
<TabPanels>
|
|
{/* Overview Panel */}
|
|
<TabPanel>
|
|
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
|
<Card>
|
|
<Text>Budget Status</Text>
|
|
<div className="mt-2">
|
|
<Title>${info.spend.toFixed(6)}</Title>
|
|
<Text>of {info.max_budget === null ? "Unlimited" : `$${info.max_budget}`}</Text>
|
|
{info.budget_duration && (
|
|
<Text className="text-gray-500">Reset: {info.budget_duration}</Text>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<Text>Rate Limits</Text>
|
|
<div className="mt-2">
|
|
<Text>TPM: {info.tpm_limit || 'Unlimited'}</Text>
|
|
<Text>RPM: {info.rpm_limit || 'Unlimited'}</Text>
|
|
{info.max_parallel_requests && (
|
|
<Text>Max Parallel Requests: {info.max_parallel_requests}</Text>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<Card>
|
|
<Text>Models</Text>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{info.models.map((model, index) => (
|
|
<Badge key={index} color="red">
|
|
{model}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</Grid>
|
|
</TabPanel>
|
|
|
|
{/* Members Panel */}
|
|
<TabPanel>
|
|
<TeamMembersComponent
|
|
teamData={teamData}
|
|
canEditTeam={canEditTeam}
|
|
handleMemberDelete={handleMemberDelete}
|
|
setSelectedEditMember={setSelectedEditMember}
|
|
setIsEditMemberModalVisible={setIsEditMemberModalVisible}
|
|
setIsAddMemberModalVisible={setIsAddMemberModalVisible}
|
|
/>
|
|
</TabPanel>
|
|
|
|
{/* Member Permissions Panel */}
|
|
{canEditTeam && (
|
|
<TabPanel>
|
|
<MemberPermissions
|
|
teamId={teamId}
|
|
accessToken={accessToken}
|
|
canEditTeam={canEditTeam}
|
|
/>
|
|
</TabPanel>
|
|
)}
|
|
|
|
{/* Settings Panel */}
|
|
<TabPanel>
|
|
<Card>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Title>Team Settings</Title>
|
|
{(canEditTeam && !isEditing) && (
|
|
<TremorButton
|
|
onClick={() => setIsEditing(true)}
|
|
>
|
|
Edit Settings
|
|
</TremorButton>
|
|
)}
|
|
</div>
|
|
|
|
{isEditing ? (
|
|
<Form
|
|
form={form}
|
|
onFinish={handleTeamUpdate}
|
|
initialValues={{
|
|
...info,
|
|
team_alias: info.team_alias,
|
|
models: info.models,
|
|
tpm_limit: info.tpm_limit,
|
|
rpm_limit: info.rpm_limit,
|
|
max_budget: info.max_budget,
|
|
budget_duration: info.budget_duration,
|
|
guardrails: info.metadata?.guardrails || [],
|
|
metadata: info.metadata ? JSON.stringify(info.metadata, null, 2) : "",
|
|
}}
|
|
layout="vertical"
|
|
>
|
|
<Form.Item
|
|
label="Team Name"
|
|
name="team_alias"
|
|
rules={[{ required: true, message: "Please input a team name" }]}
|
|
>
|
|
<Input type=""/>
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Models" name="models">
|
|
<Select
|
|
mode="multiple"
|
|
placeholder="Select models"
|
|
>
|
|
<Select.Option key="all-proxy-models" value="all-proxy-models">
|
|
All Proxy Models
|
|
</Select.Option>
|
|
{userModels.map((model, idx) => (
|
|
<Select.Option key={idx} value={model}>
|
|
{getModelDisplayName(model)}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
|
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Reset Budget" name="budget_duration">
|
|
<Select placeholder="n/a">
|
|
<Select.Option value="24h">daily</Select.Option>
|
|
<Select.Option value="7d">weekly</Select.Option>
|
|
<Select.Option value="30d">monthly</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
|
|
<NumericalInput step={1} style={{ width: "100%" }} />
|
|
</Form.Item>
|
|
|
|
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
|
|
<NumericalInput step={1} style={{ width: "100%" }} />
|
|
</Form.Item>
|
|
|
|
<Form.Item
|
|
label={
|
|
<span>
|
|
Guardrails{' '}
|
|
<Tooltip title="Setup your first guardrail">
|
|
<a
|
|
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
|
</a>
|
|
</Tooltip>
|
|
</span>
|
|
}
|
|
name="guardrails"
|
|
help="Select existing guardrails or enter new ones"
|
|
>
|
|
<Select
|
|
mode="tags"
|
|
placeholder="Select or enter guardrails"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item label="Metadata" name="metadata">
|
|
<Input.TextArea rows={10} />
|
|
</Form.Item>
|
|
|
|
|
|
<div className="flex justify-end gap-2 mt-6">
|
|
<Button onClick={() => setIsEditing(false)}>
|
|
Cancel
|
|
</Button>
|
|
<TremorButton>
|
|
Save Changes
|
|
</TremorButton>
|
|
</div>
|
|
</Form>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Text className="font-medium">Team Name</Text>
|
|
<div>{info.team_alias}</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Team ID</Text>
|
|
<div className="font-mono">{info.team_id}</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Created At</Text>
|
|
<div>{new Date(info.created_at).toLocaleString()}</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Models</Text>
|
|
<div className="flex flex-wrap gap-2 mt-1">
|
|
{info.models.map((model, index) => (
|
|
<Badge key={index} color="red">
|
|
{model}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Rate Limits</Text>
|
|
<div>TPM: {info.tpm_limit || 'Unlimited'}</div>
|
|
<div>RPM: {info.rpm_limit || 'Unlimited'}</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Budget</Text>
|
|
<div>Max: {info.max_budget !== null ? `$${info.max_budget}` : 'No Limit'}</div>
|
|
<div>Reset: {info.budget_duration || 'Never'}</div>
|
|
</div>
|
|
<div>
|
|
<Text className="font-medium">Status</Text>
|
|
<Badge color={info.blocked ? 'red' : 'green'}>
|
|
{info.blocked ? 'Blocked' : 'Active'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</TabGroup>
|
|
|
|
<MemberModal
|
|
visible={isEditMemberModalVisible}
|
|
onCancel={() => setIsEditMemberModalVisible(false)}
|
|
onSubmit={handleMemberUpdate}
|
|
initialData={selectedEditMember}
|
|
mode="edit"
|
|
config={{
|
|
title: "Edit Member",
|
|
showEmail: true,
|
|
showUserId: true,
|
|
roleOptions: [
|
|
{ label: "Admin", value: "admin" },
|
|
{ label: "User", value: "user" }
|
|
]
|
|
}}
|
|
/>
|
|
|
|
<UserSearchModal
|
|
isVisible={isAddMemberModalVisible}
|
|
onCancel={() => setIsAddMemberModalVisible(false)}
|
|
onSubmit={handleMemberCreate}
|
|
accessToken={accessToken}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TeamInfoView; |