mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +00:00
Org Flow Improvements (#8549)
* refactor(organization.tsx): initial commit with orgs tab refactor make it similar to 'Teams' tab - simplifies org management actions * fix(page.tsx): pass user orgs to component * fix(organization_view.tsx): fix to pull info from org info endpoint * feat(organization_endpoints.py): return org members when calling /org/info * fix(organization_view.tsx): show org members on info page * feat(organization_view.tsx): allow adding user to org via user email Resolves https://github.com/BerriAI/litellm/issues/8330 * fix(organization_endpoints.py): raise better error when duplicate user_email found in db * fix(organization_view.tsx): cleanup user_email for now not in initial org info - will need to prefetch * fix(page.tsx): fix getting user models on page load allows passing down the user models to org * fix(organizations.tsx): fix creating org on ui * fix(proxy/_types.py): include org created at and updated at cleans up ui * fix(navbar.tsx): cleanup * fix(organizations.tsx): fix tpm/rpm limits on org * fix(organizations.tsx): fix linting error * fix(organizations.tsx): fix linting \ * (Feat) - Add `/bedrock/meta.llama3-3-70b-instruct-v1:0` tool calling support + cost tracking + base llm unit test for tool calling (#8545) * Add support for bedrock meta.llama3-3-70b-instruct-v1:0 tool calling (#8512) * fix(converse_transformation.py): fixing bedrock meta.llama3-3-70b tool calling * test(test_bedrock_completion.py): adding llama3.3 tool compatibility check * add TestBedrockTestSuite * add bedrock llama 3.3 to base llm class * us.meta.llama3-3-70b-instruct-v1:0 * test_basic_tool_calling * TestAzureOpenAIO1 * test_basic_tool_calling * test_basic_tool_calling --------- Co-authored-by: miraclebakelaser <65143272+miraclebakelaser@users.noreply.github.com> * fix(general_settings.tsx): filter out empty dictionaries post fallback delete (#8550) Fixes https://github.com/BerriAI/litellm/issues/8331 * bump: version 1.61.3 → 1.61.4 * (perf) Fix memory leak on `/completions` route (#8551) * initial mem util test * fix _cached_get_model_info_helper * test memory usage * fix tests * fix mem usage --------- Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com> Co-authored-by: miraclebakelaser <65143272+miraclebakelaser@users.noreply.github.com>
This commit is contained in:
parent
510e8cd754
commit
b516cf21cb
10 changed files with 940 additions and 301 deletions
File diff suppressed because one or more lines are too long
|
@ -1487,6 +1487,7 @@ class LiteLLM_OrganizationTable(LiteLLMPydanticObjectBase):
|
||||||
organization_id: Optional[str] = None
|
organization_id: Optional[str] = None
|
||||||
organization_alias: Optional[str] = None
|
organization_alias: Optional[str] = None
|
||||||
budget_id: str
|
budget_id: str
|
||||||
|
spend: float = 0.0
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
models: List[str]
|
models: List[str]
|
||||||
created_by: str
|
created_by: str
|
||||||
|
@ -1494,8 +1495,13 @@ class LiteLLM_OrganizationTable(LiteLLMPydanticObjectBase):
|
||||||
|
|
||||||
|
|
||||||
class LiteLLM_OrganizationTableWithMembers(LiteLLM_OrganizationTable):
|
class LiteLLM_OrganizationTableWithMembers(LiteLLM_OrganizationTable):
|
||||||
members: List[LiteLLM_OrganizationMembershipTable]
|
"""Returned by the /organization/info endpoint and /organization/list endpoint"""
|
||||||
teams: List[LiteLLM_TeamTable]
|
|
||||||
|
members: List[LiteLLM_OrganizationMembershipTable] = []
|
||||||
|
teams: List[LiteLLM_TeamTable] = []
|
||||||
|
litellm_budget_table: Optional[LiteLLM_BudgetTable] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
|
||||||
class NewOrganizationResponse(LiteLLM_OrganizationTable):
|
class NewOrganizationResponse(LiteLLM_OrganizationTable):
|
||||||
|
|
|
@ -16,6 +16,7 @@ from typing import List, Optional, Tuple
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
||||||
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.proxy._types import *
|
from litellm.proxy._types import *
|
||||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||||
from litellm.proxy.management_helpers.utils import (
|
from litellm.proxy.management_helpers.utils import (
|
||||||
|
@ -250,14 +251,46 @@ async def list_organization(
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/organization/info",
|
||||||
|
tags=["organization management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
response_model=LiteLLM_OrganizationTableWithMembers,
|
||||||
|
)
|
||||||
|
async def info_organization(organization_id: str):
|
||||||
|
"""
|
||||||
|
Get the org specific information
|
||||||
|
"""
|
||||||
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
|
if prisma_client is None:
|
||||||
|
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||||
|
|
||||||
|
response: Optional[LiteLLM_OrganizationTableWithMembers] = (
|
||||||
|
await prisma_client.db.litellm_organizationtable.find_unique(
|
||||||
|
where={"organization_id": organization_id},
|
||||||
|
include={"litellm_budget_table": True, "members": True, "teams": True},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
raise HTTPException(status_code=404, detail={"error": "Organization not found"})
|
||||||
|
|
||||||
|
response_pydantic_obj = LiteLLM_OrganizationTableWithMembers(
|
||||||
|
**response.model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
return response_pydantic_obj
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/organization/info",
|
"/organization/info",
|
||||||
tags=["organization management"],
|
tags=["organization management"],
|
||||||
dependencies=[Depends(user_api_key_auth)],
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
)
|
)
|
||||||
async def info_organization(data: OrganizationRequest):
|
async def deprecated_info_organization(data: OrganizationRequest):
|
||||||
"""
|
"""
|
||||||
Get the org specific information
|
DEPRECATED: Use GET /organization/info instead
|
||||||
"""
|
"""
|
||||||
from litellm.proxy.proxy_server import prisma_client
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
|
@ -378,6 +411,7 @@ async def organization_member_add(
|
||||||
updated_organization_memberships=updated_organization_memberships,
|
updated_organization_memberships=updated_organization_memberships,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
verbose_proxy_logger.exception(f"Error adding member to organization: {e}")
|
||||||
if isinstance(e, HTTPException):
|
if isinstance(e, HTTPException):
|
||||||
raise ProxyException(
|
raise ProxyException(
|
||||||
message=getattr(e, "detail", f"Authentication Error({str(e)})"),
|
message=getattr(e, "detail", f"Authentication Error({str(e)})"),
|
||||||
|
@ -418,12 +452,17 @@ async def add_member_to_organization(
|
||||||
where={"user_id": member.user_id}
|
where={"user_id": member.user_id}
|
||||||
)
|
)
|
||||||
|
|
||||||
if member.user_email is not None:
|
if existing_user_id_row is None and member.user_email is not None:
|
||||||
existing_user_email_row = (
|
try:
|
||||||
await prisma_client.db.litellm_usertable.find_unique(
|
existing_user_email_row = (
|
||||||
where={"user_email": member.user_email}
|
await prisma_client.db.litellm_usertable.find_unique(
|
||||||
|
where={"user_email": member.user_email}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(
|
||||||
|
f"Potential NON-Existent or Duplicate user email in DB: Error finding a unique instance of user_email={member.user_email} in LiteLLM_UserTable.: {e}"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
## If user does not exist, create a new user
|
## If user does not exist, create a new user
|
||||||
if existing_user_id_row is None and existing_user_email_row is None:
|
if existing_user_id_row is None and existing_user_email_row is None:
|
||||||
|
@ -477,4 +516,9 @@ async def add_member_to_organization(
|
||||||
return user_object, organization_membership
|
return user_object, organization_membership
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Error adding member to organization: {e}")
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
raise ValueError(
|
||||||
|
f"Error adding member={member} to organization={organization_id}: {e}"
|
||||||
|
)
|
||||||
|
|
|
@ -27,6 +27,8 @@ import CacheDashboard from "@/components/cache_dashboard";
|
||||||
import { setGlobalLitellmHeaderName } from "@/components/networking";
|
import { setGlobalLitellmHeaderName } from "@/components/networking";
|
||||||
import { Organization } from "@/components/networking";
|
import { Organization } from "@/components/networking";
|
||||||
import GuardrailsPanel from "@/components/guardrails";
|
import GuardrailsPanel from "@/components/guardrails";
|
||||||
|
import { fetchUserModels } from "@/components/create_key_button";
|
||||||
|
|
||||||
function getCookie(name: string) {
|
function getCookie(name: string) {
|
||||||
const cookieValue = document.cookie
|
const cookieValue = document.cookie
|
||||||
.split("; ")
|
.split("; ")
|
||||||
|
@ -81,6 +83,7 @@ export default function CreateKeyPage() {
|
||||||
const [keys, setKeys] = useState<null | any[]>(null);
|
const [keys, setKeys] = useState<null | any[]>(null);
|
||||||
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
|
const [currentOrg, setCurrentOrg] = useState<Organization>(defaultOrg);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
||||||
|
const [userModels, setUserModels] = useState<string[]>([]);
|
||||||
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
const [proxySettings, setProxySettings] = useState<ProxySettings>({
|
||||||
PROXY_BASE_URL: "",
|
PROXY_BASE_URL: "",
|
||||||
PROXY_LOGOUT_URL: "",
|
PROXY_LOGOUT_URL: "",
|
||||||
|
@ -171,6 +174,12 @@ export default function CreateKeyPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accessToken && userID && userRole) {
|
||||||
|
fetchUserModels(userID, userRole, accessToken, setUserModels);
|
||||||
|
}
|
||||||
|
}, [accessToken, userID, userRole]);
|
||||||
|
|
||||||
const handleOrgChange = (org: Organization) => {
|
const handleOrgChange = (org: Organization) => {
|
||||||
setCurrentOrg(org);
|
setCurrentOrg(org);
|
||||||
|
@ -283,11 +292,10 @@ export default function CreateKeyPage() {
|
||||||
/>
|
/>
|
||||||
) : page == "organizations" ? (
|
) : page == "organizations" ? (
|
||||||
<Organizations
|
<Organizations
|
||||||
teams={teams}
|
organizations={organizations}
|
||||||
setTeams={setTeams}
|
setOrganizations={setOrganizations}
|
||||||
searchParams={searchParams}
|
userModels={userModels}
|
||||||
accessToken={accessToken}
|
accessToken={accessToken}
|
||||||
userID={userID}
|
|
||||||
userRole={userRole}
|
userRole={userRole}
|
||||||
premiumUser={premiumUser}
|
premiumUser={premiumUser}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -35,6 +35,7 @@ interface DeleteModalProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataTableProps {
|
interface DataTableProps {
|
||||||
|
@ -43,6 +44,7 @@ interface DataTableProps {
|
||||||
actions?: Action[];
|
actions?: Action[];
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
deleteModal?: DeleteModalProps;
|
deleteModal?: DeleteModalProps;
|
||||||
|
onItemClick?: (item: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataTable: React.FC<DataTableProps> = ({
|
const DataTable: React.FC<DataTableProps> = ({
|
||||||
|
@ -50,7 +52,8 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||||
columns,
|
columns,
|
||||||
actions,
|
actions,
|
||||||
emptyMessage = "No data available",
|
emptyMessage = "No data available",
|
||||||
deleteModal
|
deleteModal,
|
||||||
|
onItemClick
|
||||||
}) => {
|
}) => {
|
||||||
const renderCell = (column: Column, row: any) => {
|
const renderCell = (column: Column, row: any) => {
|
||||||
const value = row[column.accessor];
|
const value = row[column.accessor];
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { Modal, Form, Button, Select } from 'antd';
|
import { Modal, Form, Button, Select, Tooltip } from 'antd';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { userFilterUICall } from "@/components/networking";
|
import { userFilterUICall } from "@/components/networking";
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
interface User {
|
interface User {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
user_email: string;
|
user_email: string;
|
||||||
|
@ -15,10 +15,17 @@ interface UserOption {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Role {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
user_email: string;
|
user_email: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
role: 'admin' | 'user';
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UserSearchModalProps {
|
interface UserSearchModalProps {
|
||||||
|
@ -26,13 +33,22 @@ interface UserSearchModalProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: FormValues) => void;
|
onSubmit: (values: FormValues) => void;
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
title?: string;
|
||||||
|
roles?: Role[];
|
||||||
|
defaultRole?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
accessToken
|
accessToken,
|
||||||
|
title = "Add Team Member",
|
||||||
|
roles = [
|
||||||
|
{ label: "admin", value: "admin", description: "Admin role. Can create team keys, add members, and manage settings." },
|
||||||
|
{ label: "user", value: "user", description: "User role. Can view team info, but not manage it." }
|
||||||
|
],
|
||||||
|
defaultRole = "user"
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
const [userOptions, setUserOptions] = useState<UserOption[]>([]);
|
||||||
|
@ -97,7 +113,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Add Team Member"
|
title={title}
|
||||||
open={isVisible}
|
open={isVisible}
|
||||||
onCancel={handleClose}
|
onCancel={handleClose}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
@ -110,7 +126,7 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||||
wrapperCol={{ span: 16 }}
|
wrapperCol={{ span: 16 }}
|
||||||
labelAlign="left"
|
labelAlign="left"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
role: "user",
|
role: defaultRole,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -156,9 +172,15 @@ const UserSearchModal: React.FC<UserSearchModalProps> = ({
|
||||||
name="role"
|
name="role"
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<Select defaultValue="user">
|
<Select defaultValue={defaultRole}>
|
||||||
<Select.Option value="admin">admin</Select.Option>
|
{roles.map(role => (
|
||||||
<Select.Option value="user">user</Select.Option>
|
<Select.Option key={role.value} value={role.value}>
|
||||||
|
<Tooltip title={role.description}>
|
||||||
|
<span className="font-medium">{role.label}</span>
|
||||||
|
<span className="ml-2 text-gray-500 text-sm">- {role.description}</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
|
@ -94,6 +94,29 @@ export const getTeamModels = (team: Team | null, allAvailableModels: string[]):
|
||||||
return unfurlWildcardModelsInList(tempModelsToPick, allAvailableModels);
|
return unfurlWildcardModelsInList(tempModelsToPick, allAvailableModels);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchUserModels = async (userID: string, userRole: string, accessToken: string, setUserModels: (models: string[]) => void) => {
|
||||||
|
try {
|
||||||
|
if (userID === null || userRole === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessToken !== null) {
|
||||||
|
const model_available = await modelAvailableCall(
|
||||||
|
accessToken,
|
||||||
|
userID,
|
||||||
|
userRole
|
||||||
|
);
|
||||||
|
let available_model_names = model_available["data"].map(
|
||||||
|
(element: { id: string }) => element.id
|
||||||
|
);
|
||||||
|
console.log("available_model_names:", available_model_names);
|
||||||
|
setUserModels(available_model_names);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user models:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const CreateKey: React.FC<CreateKeyProps> = ({
|
const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
userID,
|
userID,
|
||||||
team,
|
team,
|
||||||
|
@ -126,30 +149,9 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserModels = async () => {
|
if (userID && userRole && accessToken) {
|
||||||
try {
|
fetchUserModels(userID, userRole, accessToken, setUserModels);
|
||||||
if (userID === null || userRole === null) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessToken !== null) {
|
|
||||||
const model_available = await modelAvailableCall(
|
|
||||||
accessToken,
|
|
||||||
userID,
|
|
||||||
userRole
|
|
||||||
);
|
|
||||||
let available_model_names = model_available["data"].map(
|
|
||||||
(element: { id: string }) => element.id
|
|
||||||
);
|
|
||||||
console.log("available_model_names:", available_model_names);
|
|
||||||
setUserModels(available_model_names);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUserModels();
|
|
||||||
}, [accessToken, userID, userRole]);
|
}, [accessToken, userID, userRole]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface Model {
|
||||||
model_info: Object | null;
|
model_info: Object | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Organization {
|
export interface Organization {
|
||||||
organization_id: string | null;
|
organization_id: string | null;
|
||||||
organization_alias: string;
|
organization_alias: string;
|
||||||
|
@ -834,6 +835,40 @@ export const organizationListCall = async (accessToken: String) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const organizationInfoCall = async (
|
||||||
|
accessToken: String,
|
||||||
|
organizationID: String
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/organization/info` : `/organization/info`;
|
||||||
|
if (organizationID) {
|
||||||
|
url = `${url}?organization_id=${organizationID}`;
|
||||||
|
}
|
||||||
|
console.log("in teamInfoCall");
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(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 organizationCreateCall = async (
|
export const organizationCreateCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
formValues: Record<string, any> // Assuming formValues is an object
|
formValues: Record<string, any> // Assuming formValues is an object
|
||||||
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Title,
|
||||||
|
Text,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabGroup,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Grid,
|
||||||
|
Badge,
|
||||||
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
Button as TremorButton,
|
||||||
|
Icon
|
||||||
|
} from "@tremor/react";
|
||||||
|
import { Button, Form, Input, Select, message, InputNumber, Tooltip } from "antd";
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
|
||||||
|
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||||
|
import { Member, Organization, organizationInfoCall, organizationMemberAddCall } from "../networking";
|
||||||
|
import UserSearchModal from "../common_components/user_search_modal";
|
||||||
|
|
||||||
|
|
||||||
|
interface OrganizationInfoProps {
|
||||||
|
organizationId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
accessToken: string | null;
|
||||||
|
is_org_admin: boolean;
|
||||||
|
is_proxy_admin: boolean;
|
||||||
|
userModels: string[];
|
||||||
|
editOrg: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
|
||||||
|
organizationId,
|
||||||
|
onClose,
|
||||||
|
accessToken,
|
||||||
|
is_org_admin,
|
||||||
|
is_proxy_admin,
|
||||||
|
userModels,
|
||||||
|
editOrg
|
||||||
|
}) => {
|
||||||
|
const [orgData, setOrgData] = useState<Organization | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const canEditOrg = is_org_admin || is_proxy_admin;
|
||||||
|
|
||||||
|
const fetchOrgInfo = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
if (!accessToken) return;
|
||||||
|
const response = await organizationInfoCall(accessToken, organizationId);
|
||||||
|
setOrgData(response);
|
||||||
|
} catch (error) {
|
||||||
|
message.error("Failed to load organization information");
|
||||||
|
console.error("Error fetching organization info:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchOrgInfo();
|
||||||
|
}, [organizationId, accessToken]);
|
||||||
|
|
||||||
|
const handleMemberAdd = 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 organizationMemberAddCall(accessToken, organizationId, member);
|
||||||
|
|
||||||
|
message.success("Organization member added successfully");
|
||||||
|
setIsAddMemberModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
fetchOrgInfo();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("Failed to add organization member");
|
||||||
|
console.error("Error adding organization member:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrgUpdate = async (values: any) => {
|
||||||
|
try {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
organization_id: organizationId,
|
||||||
|
organization_alias: values.organization_alias,
|
||||||
|
models: values.models,
|
||||||
|
litellm_budget_table: {
|
||||||
|
tpm_limit: values.tpm_limit,
|
||||||
|
rpm_limit: values.rpm_limit,
|
||||||
|
max_budget: values.max_budget,
|
||||||
|
budget_duration: values.budget_duration,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('/organization/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updateData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to update organization');
|
||||||
|
|
||||||
|
message.success("Organization settings updated successfully");
|
||||||
|
setIsEditing(false);
|
||||||
|
fetchOrgInfo();
|
||||||
|
} catch (error) {
|
||||||
|
message.error("Failed to update organization settings");
|
||||||
|
console.error("Error updating organization:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-4">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!orgData) {
|
||||||
|
return <div className="p-4">Organization not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-screen p-4 bg-white">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<Button onClick={onClose} className="mb-4">← Back</Button>
|
||||||
|
<Title>{orgData.organization_alias}</Title>
|
||||||
|
<Text className="text-gray-500 font-mono">{orgData.organization_id}</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabGroup defaultIndex={editOrg ? 2 : 0}>
|
||||||
|
<TabList className="mb-4">
|
||||||
|
<Tab>Overview</Tab>
|
||||||
|
<Tab>Members</Tab>
|
||||||
|
<Tab>Settings</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanels>
|
||||||
|
{/* Overview Panel */}
|
||||||
|
<TabPanel>
|
||||||
|
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||||
|
<Card>
|
||||||
|
<Text>Organization Details</Text>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Text>Created: {new Date(orgData.created_at).toLocaleDateString()}</Text>
|
||||||
|
<Text>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</Text>
|
||||||
|
<Text>Created By: {orgData.created_by}</Text>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Text>Budget Status</Text>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Title>${orgData.spend.toFixed(6)}</Title>
|
||||||
|
<Text>of {orgData.litellm_budget_table.max_budget === null ? "Unlimited" : `$${orgData.litellm_budget_table.max_budget}`}</Text>
|
||||||
|
{orgData.litellm_budget_table.budget_duration && (
|
||||||
|
<Text className="text-gray-500">Reset: {orgData.litellm_budget_table.budget_duration}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Text>Rate Limits</Text>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Text>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</Text>
|
||||||
|
<Text>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</Text>
|
||||||
|
{orgData.litellm_budget_table.max_parallel_requests && (
|
||||||
|
<Text>Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests}</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Text>Models</Text>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{orgData.models.map((model, index) => (
|
||||||
|
<Badge key={index} color="red">
|
||||||
|
{model}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* Budget Panel */}
|
||||||
|
<TabPanel>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[75vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>User ID</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Role</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Created At</TableHeaderCell>
|
||||||
|
<TableHeaderCell></TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableBody>
|
||||||
|
{orgData.members?.map((member, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>
|
||||||
|
<Text className="font-mono">{member.user_id}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text className="font-mono">{member.user_role}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>${member.spend.toFixed(6)}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>{new Date(member.created_at).toLocaleString()}</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{canEditOrg && (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
icon={PencilAltIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Implement edit member functionality
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
icon={TrashIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
// TODO: Implement delete member functionality
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
{canEditOrg && (
|
||||||
|
<TremorButton onClick={() => {
|
||||||
|
setIsAddMemberModalVisible(true);
|
||||||
|
}}>
|
||||||
|
Add Member
|
||||||
|
</TremorButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* Settings Panel */}
|
||||||
|
<TabPanel>
|
||||||
|
<Card>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<Title>Organization Settings</Title>
|
||||||
|
{(canEditOrg && !isEditing) && (
|
||||||
|
<TremorButton
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
Edit Settings
|
||||||
|
</TremorButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleOrgUpdate}
|
||||||
|
initialValues={{
|
||||||
|
organization_alias: orgData.organization_alias,
|
||||||
|
models: orgData.models,
|
||||||
|
tpm_limit: orgData.litellm_budget_table.tpm_limit,
|
||||||
|
rpm_limit: orgData.litellm_budget_table.rpm_limit,
|
||||||
|
max_budget: orgData.litellm_budget_table.max_budget,
|
||||||
|
budget_duration: orgData.litellm_budget_table.budget_duration,
|
||||||
|
}}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Organization Name"
|
||||||
|
name="organization_alias"
|
||||||
|
rules={[{ required: true, message: "Please input an organization name" }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</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) => (
|
||||||
|
<Select.Option key={model} value={model}>
|
||||||
|
{getModelDisplayName(model)}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||||
|
<InputNumber 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">
|
||||||
|
<InputNumber step={1} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
|
||||||
|
<InputNumber step={1} style={{ width: "100%" }} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-6">
|
||||||
|
<Button onClick={() => setIsEditing(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<TremorButton type="submit">
|
||||||
|
Save Changes
|
||||||
|
</TremorButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Organization Name</Text>
|
||||||
|
<div>{orgData.organization_alias}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Organization ID</Text>
|
||||||
|
<div className="font-mono">{orgData.organization_id}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Created At</Text>
|
||||||
|
<div>{new Date(orgData.created_at).toLocaleString()}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Models</Text>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-1">
|
||||||
|
{orgData.models.map((model, index) => (
|
||||||
|
<Badge key={index} color="red">
|
||||||
|
{model}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Rate Limits</Text>
|
||||||
|
<div>TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}</div>
|
||||||
|
<div>RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Budget</Text>
|
||||||
|
<div>Max: {orgData.litellm_budget_table.max_budget !== null ? `$${orgData.litellm_budget_table.max_budget}` : 'No Limit'}</div>
|
||||||
|
<div>Reset: {orgData.litellm_budget_table.budget_duration || 'Never'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
|
<UserSearchModal
|
||||||
|
isVisible={isAddMemberModalVisible}
|
||||||
|
onCancel={() => setIsAddMemberModalVisible(false)}
|
||||||
|
onSubmit={handleMemberAdd}
|
||||||
|
accessToken={accessToken}
|
||||||
|
title="Add Organization Member"
|
||||||
|
roles={[
|
||||||
|
{ label: "org_admin", value: "org_admin", description: "Can add and remove members, and change their roles." },
|
||||||
|
{ label: "internal_user", value: "internal_user", description: "Can view/create keys for themselves within organization." },
|
||||||
|
{ label: "internal_user_viewer", value: "internal_user_viewer", description: "Can only view their keys within organization." }
|
||||||
|
]}
|
||||||
|
defaultRole="internal_user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationInfoView;
|
|
@ -1,282 +1,392 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking";
|
|
||||||
import { Team } from "./key_team_helpers/key_list";
|
|
||||||
import {
|
import {
|
||||||
Col,
|
Table,
|
||||||
|
TableHead,
|
||||||
|
TableHeaderCell,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
Card,
|
||||||
|
Text,
|
||||||
|
Badge,
|
||||||
|
Icon,
|
||||||
Grid,
|
Grid,
|
||||||
Text
|
Col,
|
||||||
|
Button,
|
||||||
|
TabGroup,
|
||||||
|
TabList,
|
||||||
|
Tab,
|
||||||
|
TabPanels,
|
||||||
|
TabPanel,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import OrganizationForm from "@/components/organization/add_org";
|
import { Modal, Form, InputNumber, Tooltip, Select as Select2 } from "antd";
|
||||||
import AddOrgAdmin from "@/components/organization/add_org_admin";
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import MemberListTable from "@/components/organization/view_members_of_org";
|
import { PencilAltIcon, TrashIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||||
import { Select, SelectItem } from "@tremor/react";
|
import { TextInput } from "@tremor/react";
|
||||||
const isLocal = process.env.NODE_ENV === "development";
|
import { getModelDisplayName } from './key_team_helpers/fetch_available_models_team_key';
|
||||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
import OrganizationInfoView from './organization/organization_view';
|
||||||
if (isLocal != true) {
|
import { Organization, organizationListCall, organizationCreateCall } from './networking';
|
||||||
console.log = function() {};
|
interface OrganizationsTableProps {
|
||||||
}
|
organizations: Organization[];
|
||||||
interface TeamProps {
|
userRole: string;
|
||||||
teams: Team[] | null;
|
userModels: string[];
|
||||||
searchParams: any;
|
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
setTeams: React.Dispatch<React.SetStateAction<Team[] | null>>;
|
lastRefreshed?: string;
|
||||||
userID: string | null;
|
handleRefreshClick?: () => void;
|
||||||
userRole: string | null;
|
currentOrg?: any;
|
||||||
|
guardrailsList?: string[];
|
||||||
|
setOrganizations: (organizations: Organization[]) => void;
|
||||||
premiumUser: boolean;
|
premiumUser: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchOrganizations = async (accessToken: string, setOrganizations: (organizations: Organization[]) => void) => {
|
||||||
import DataTable from "@/components/common_components/all_view";
|
const organizations = await organizationListCall(accessToken);
|
||||||
import { Action } from "@/components/common_components/all_view";
|
setOrganizations(organizations);
|
||||||
import { Typography, message } from "antd";
|
|
||||||
import { Organization, EditModalProps } from "@/components/organization/types";
|
|
||||||
|
|
||||||
interface OrganizationsTableProps {
|
|
||||||
organizations: Organization[];
|
|
||||||
userRole?: string;
|
|
||||||
onEdit?: (organization: Organization) => void;
|
|
||||||
onDelete?: (organization: Organization) => void;
|
|
||||||
isDeleteModalOpen: boolean;
|
|
||||||
setIsDeleteModalOpen: (value: boolean) => void;
|
|
||||||
selectedOrganization: Organization | null;
|
|
||||||
setSelectedOrganization: (value: Organization | null) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const EditOrganizationModal: React.FC<EditModalProps> = ({
|
|
||||||
visible,
|
|
||||||
onCancel,
|
|
||||||
entity,
|
|
||||||
onSubmit
|
|
||||||
}) => {
|
|
||||||
return <div/>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Inside your Teams component
|
|
||||||
const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
|
||||||
organizations,
|
organizations,
|
||||||
userRole,
|
userRole,
|
||||||
onEdit,
|
userModels,
|
||||||
onDelete,
|
|
||||||
isDeleteModalOpen,
|
|
||||||
setIsDeleteModalOpen,
|
|
||||||
selectedOrganization,
|
|
||||||
setSelectedOrganization
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
header: "Organization Name",
|
|
||||||
accessor: "organization_alias",
|
|
||||||
width: "4px",
|
|
||||||
style: {
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
overflow: "hidden"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Organization ID",
|
|
||||||
accessor: "organization_id",
|
|
||||||
width: "4px",
|
|
||||||
style: {
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
fontSize: "0.75em"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Spend (USD)",
|
|
||||||
accessor: "spend"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Budget (USD)",
|
|
||||||
accessor: "max_budget",
|
|
||||||
cellRenderer: (value: number | null) =>
|
|
||||||
value !== null && value !== undefined ? value : "No limit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Models",
|
|
||||||
accessor: "models"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "TPM / RPM Limits",
|
|
||||||
accessor: "limits",
|
|
||||||
cellRenderer: (value: any, row: Organization) => (
|
|
||||||
<div className="text-sm">
|
|
||||||
<span>TPM: {row.tpm_limit ? row.tpm_limit : "Unlimited"}</span>
|
|
||||||
<br />
|
|
||||||
<span>RPM: {row.rpm_limit ? row.rpm_limit : "Unlimited"}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Info",
|
|
||||||
accessor: "info",
|
|
||||||
cellRenderer: (value: any, row: Organization) => (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-sm">
|
|
||||||
{row.members?.length || 0} Members
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const actions: Action[] = [
|
|
||||||
...(onEdit && userRole === "Admin" ? [{
|
|
||||||
icon: undefined, // Replace with your PencilAltIcon
|
|
||||||
onClick: (org: Organization) => onEdit(org),
|
|
||||||
condition: () => userRole === "Admin",
|
|
||||||
tooltip: "Edit organization"
|
|
||||||
}] : []),
|
|
||||||
...(onDelete && userRole === "Admin" ? [{
|
|
||||||
icon: undefined, // Replace with your TrashIcon
|
|
||||||
onClick: (org: Organization) => onDelete(org),
|
|
||||||
condition: () => userRole === "Admin",
|
|
||||||
tooltip: "Delete organization"
|
|
||||||
}] : [])
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
data={organizations}
|
|
||||||
columns={columns}
|
|
||||||
actions={actions}
|
|
||||||
emptyMessage="No organizations available"
|
|
||||||
deleteModal={{
|
|
||||||
isOpen: isDeleteModalOpen,
|
|
||||||
onConfirm: () => {
|
|
||||||
if (selectedOrganization && onDelete) {
|
|
||||||
onDelete(selectedOrganization);
|
|
||||||
}
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setSelectedOrganization(null);
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
setSelectedOrganization(null);
|
|
||||||
},
|
|
||||||
title: "Delete Organization",
|
|
||||||
message: "Are you sure you want to delete this organization?"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Organizations: React.FC<TeamProps> = ({
|
|
||||||
accessToken,
|
accessToken,
|
||||||
userID,
|
lastRefreshed,
|
||||||
userRole,
|
handleRefreshClick,
|
||||||
|
currentOrg,
|
||||||
|
guardrailsList = [],
|
||||||
|
setOrganizations,
|
||||||
premiumUser
|
premiumUser
|
||||||
}) => {
|
}) => {
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
|
||||||
const { Title, Paragraph } = Typography;
|
const [editOrg, setEditOrg] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [selectedOrganization, setSelectedOrganization] = useState<Organization | null>(null);
|
const [orgToDelete, setOrgToDelete] = useState<string | null>(null);
|
||||||
const [userModels, setUserModels] = useState([]);
|
const [isOrgModalVisible, setIsOrgModalVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accessToken || !userID || !userRole) return;
|
if (organizations.length === 0 && accessToken) {
|
||||||
|
fetchOrganizations(accessToken, setOrganizations);
|
||||||
const fetchUserModels = async () => {
|
|
||||||
try {
|
|
||||||
const model_available = await modelAvailableCall(
|
|
||||||
accessToken,
|
|
||||||
userID,
|
|
||||||
userRole
|
|
||||||
);
|
|
||||||
let available_model_names = model_available["data"].map(
|
|
||||||
(element: { id: string }) => element.id
|
|
||||||
);
|
|
||||||
console.log("available_model_names:", available_model_names);
|
|
||||||
setUserModels(available_model_names);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user models:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
let givenOrganizations;
|
|
||||||
givenOrganizations = await organizationListCall(accessToken)
|
|
||||||
console.log(`givenOrganizations: ${givenOrganizations}`)
|
|
||||||
setOrganizations(givenOrganizations)
|
|
||||||
sessionStorage.setItem('organizations', JSON.stringify(givenOrganizations));
|
|
||||||
}
|
}
|
||||||
if (premiumUser) {
|
}, [organizations, accessToken]);
|
||||||
fetchUserModels()
|
|
||||||
fetchData()
|
const handleDelete = (orgId: string | null) => {
|
||||||
}
|
if (!orgId) return;
|
||||||
}, [accessToken]);
|
|
||||||
|
setOrgToDelete(orgId);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!orgToDelete || !accessToken) return;
|
||||||
|
|
||||||
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
|
||||||
if (!selectedOrganization || !accessToken) return;
|
|
||||||
try {
|
try {
|
||||||
let member: Member = {
|
const response = await fetch(`/organization/delete?organization_id=${orgToDelete}`, {
|
||||||
user_email: formValues.user_email,
|
method: 'DELETE',
|
||||||
user_id: formValues.user_id,
|
headers: {
|
||||||
role: formValues.role
|
'Authorization': `Bearer ${accessToken}`
|
||||||
}
|
}
|
||||||
await organizationMemberAddCall(
|
});
|
||||||
accessToken,
|
|
||||||
selectedOrganization["organization_id"],
|
if (!response.ok) throw new Error('Failed to delete organization');
|
||||||
member
|
|
||||||
);
|
setIsDeleteModalOpen(false);
|
||||||
message.success("Member added");
|
setOrgToDelete(null);
|
||||||
|
// Refresh organizations list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating the team:", error);
|
console.error('Error deleting organization:', error);
|
||||||
message.error("Error creating the organization: " + error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cancelDelete = () => {
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setOrgToDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async (values: any) => {
|
||||||
|
try {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
await organizationCreateCall(accessToken, values);
|
||||||
|
setIsOrgModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
// Refresh organizations list
|
||||||
|
fetchOrganizations(accessToken, setOrganizations);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating organization:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsOrgModalVisible(false);
|
||||||
|
form.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!premiumUser) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text>This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key <a href="https://litellm.ai/pricing" target="_blank" rel="noopener noreferrer">here</a>.</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedOrgId) {
|
||||||
|
return (
|
||||||
|
<OrganizationInfoView
|
||||||
|
organizationId={selectedOrgId}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedOrgId(null);
|
||||||
|
setEditOrg(false);
|
||||||
|
}}
|
||||||
|
accessToken={accessToken}
|
||||||
|
is_org_admin={true} // You'll need to implement proper org admin check
|
||||||
|
is_proxy_admin={userRole === "Admin"}
|
||||||
|
userModels={userModels}
|
||||||
|
editOrg={editOrg}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-4">
|
<TabGroup className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||||
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||||
<Col numColSpan={1}>
|
<div className="flex">
|
||||||
<Title level={4}>✨ All Organizations</Title>
|
<Tab>Your Organizations</Tab>
|
||||||
<Text className="mb-2">This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key <a href="https://www.litellm.ai/#trial" className="text-blue-600 hover:text-blue-800 underline" target="_blank" rel="noopener noreferrer">here</a></Text>
|
</div>
|
||||||
{userRole ? OrganizationsTable({organizations, userRole, isDeleteModalOpen, setIsDeleteModalOpen, selectedOrganization, setSelectedOrganization}) : null}
|
<div className="flex items-center space-x-2">
|
||||||
</Col>
|
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||||
{userRole == "Admin" && accessToken && premiumUser ? <OrganizationForm
|
<Icon
|
||||||
title="Organization"
|
icon={RefreshIcon}
|
||||||
accessToken={accessToken}
|
variant="shadow"
|
||||||
availableModels={userModels}
|
size="xs"
|
||||||
submitButtonText="Create Organization"
|
className="self-center"
|
||||||
/> : null}
|
onClick={handleRefreshClick}
|
||||||
{premiumUser ?
|
/>
|
||||||
<Col numColSpan={1}>
|
</div>
|
||||||
<Title level={4}>Organization Members</Title>
|
</TabList>
|
||||||
<Paragraph>
|
<TabPanels>
|
||||||
If you belong to multiple organizations, this setting controls which organizations'
|
<TabPanel>
|
||||||
members you see.
|
<Text>
|
||||||
</Paragraph>
|
Click on “Organization ID” to view organization details.
|
||||||
|
</Text>
|
||||||
|
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Organization ID</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Organization Name</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Created</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Budget (USD)</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Models</TableHeaderCell>
|
||||||
|
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Info</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Actions</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
{organizations && organizations.length > 0 ? (
|
<TableBody>
|
||||||
<Select>
|
{organizations && organizations.length > 0
|
||||||
{organizations.map((organization: any, index) => (
|
? organizations
|
||||||
<SelectItem
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||||
key={index}
|
.map((org: Organization) => (
|
||||||
value={String(index)}
|
<TableRow key={org.organization_id}>
|
||||||
onClick={() => {
|
<TableCell>
|
||||||
setSelectedOrganization(organization);
|
<div className="overflow-hidden">
|
||||||
}}
|
<Tooltip title={org.organization_id}>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate max-w-[200px]"
|
||||||
|
onClick={() => setSelectedOrgId(org.organization_id)}
|
||||||
|
>
|
||||||
|
{org.organization_id?.slice(0, 7)}...
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{org.organization_alias}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{org.spend}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{org.litellm_budget_table?.max_budget !== null && org.litellm_budget_table?.max_budget !== undefined ? org.litellm_budget_table?.max_budget : "No limit"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{Array.isArray(org.models) && (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{org.models.length === 0 ? (
|
||||||
|
<Badge size="xs" className="mb-1" color="red">
|
||||||
|
All Proxy Models
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
org.models.map((model, index) =>
|
||||||
|
model === "all-proxy-models" ? (
|
||||||
|
<Badge key={index} size="xs" className="mb-1" color="red">
|
||||||
|
All Proxy Models
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge key={index} size="xs" className="mb-1" color="blue">
|
||||||
|
{model.length > 30
|
||||||
|
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||||
|
: getModelDisplayName(model)}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>
|
||||||
|
TPM: {org.litellm_budget_table?.tpm_limit ? org.litellm_budget_table?.tpm_limit : "Unlimited"}
|
||||||
|
<br />
|
||||||
|
RPM: {org.litellm_budget_table?.rpm_limit ? org.litellm_budget_table?.rpm_limit : "Unlimited"}
|
||||||
|
</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Text>{org.members?.length || 0} Members</Text>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{userRole === "Admin" && (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
icon={PencilAltIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedOrgId(org.organization_id);
|
||||||
|
setEditOrg(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
onClick={() => handleDelete(org.organization_id)}
|
||||||
|
icon={TrashIcon}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
{(userRole === "Admin" || userRole === "Org Admin") && (
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Button
|
||||||
|
className="mx-auto"
|
||||||
|
onClick={() => setIsOrgModalVisible(true)}
|
||||||
>
|
>
|
||||||
{organization["organization_alias"]}
|
+ Create New Organization
|
||||||
</SelectItem>
|
</Button>
|
||||||
))}
|
<Modal
|
||||||
</Select>
|
title="Create Organization"
|
||||||
) : (
|
visible={isOrgModalVisible}
|
||||||
<Paragraph>
|
width={800}
|
||||||
No team created. <b>Defaulting to personal account.</b>
|
footer={null}
|
||||||
</Paragraph>
|
onCancel={handleCancel}
|
||||||
)}
|
>
|
||||||
</Col> : null}
|
<Form
|
||||||
{userRole == "Admin" && userID && selectedOrganization && premiumUser ? <AddOrgAdmin userRole={userRole} userID={userID} selectedOrganization={selectedOrganization} onMemberAdd={handleMemberCreate} /> : null}
|
form={form}
|
||||||
{userRole == "Admin" && userID && selectedOrganization && premiumUser ? <MemberListTable selectedEntity={selectedOrganization} onEditSubmit={() => {}} editModalComponent={EditOrganizationModal} entityType="organization" /> : null}
|
onFinish={handleCreate}
|
||||||
</Grid>
|
labelCol={{ span: 8 }}
|
||||||
</div>
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Organization Name"
|
||||||
|
name="organization_alias"
|
||||||
|
rules={[{ required: true, message: "Please input an organization name" }]}
|
||||||
|
>
|
||||||
|
<TextInput placeholder="" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Models" name="models">
|
||||||
|
<Select2
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Select models"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
<Select2.Option key="all-proxy-models" value="all-proxy-models">
|
||||||
|
All Proxy Models
|
||||||
|
</Select2.Option>
|
||||||
|
{userModels && userModels.length > 0 && userModels.map((model) => (
|
||||||
|
<Select2.Option key={model} value={model}>
|
||||||
|
{getModelDisplayName(model)}
|
||||||
|
</Select2.Option>
|
||||||
|
))}
|
||||||
|
</Select2>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Max Budget (USD)" name="max_budget">
|
||||||
|
<InputNumber step={0.01} precision={2} width={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Reset Budget" name="budget_duration">
|
||||||
|
<Select2 defaultValue={null} placeholder="n/a">
|
||||||
|
<Select2.Option value="24h">daily</Select2.Option>
|
||||||
|
<Select2.Option value="7d">weekly</Select2.Option>
|
||||||
|
<Select2.Option value="30d">monthly</Select2.Option>
|
||||||
|
</Select2>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
|
||||||
|
<InputNumber step={1} width={400} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||||
|
<Button type="submit">Create Organization</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
|
||||||
|
{isDeleteModalOpen ? (
|
||||||
|
<div className="fixed z-10 inset-0 overflow-y-auto">
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<div className="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||||
|
<div className="absolute inset-0 bg-gray-500 opacity-75"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||||
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
|
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
Delete Organization
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Are you sure you want to delete this organization?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||||
|
<Button onClick={confirmDelete} color="red" className="ml-2">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button onClick={cancelDelete}>Cancel</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : <></>}
|
||||||
|
</TabGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Organizations;
|
export default OrganizationsTable;
|
Loading…
Add table
Add a link
Reference in a new issue