mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +00:00
Litellm dev 01 24 2025 p4 (#7992)
* feat(team_endpoints.py): new `/teams/available` endpoint - allows proxy admin to expose available teams for users to join on UI * build(ui/): available_teams.tsx allow user to join available teams on UI makes it easier to onboard new users to teams * fix(navbar.tsx): cleanup title * fix(team_endpoints.py): fix linting error * test: update groq model in test * build(model_prices_and_context_window.json): update groq 3.3 model with 'supports function calling'
This commit is contained in:
parent
f77882948d
commit
1ab10d8f72
10 changed files with 329 additions and 21 deletions
|
@ -2064,7 +2064,9 @@
|
|||
"input_cost_per_token": 0.00000059,
|
||||
"output_cost_per_token": 0.00000079,
|
||||
"litellm_provider": "groq",
|
||||
"mode": "chat"
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true
|
||||
},
|
||||
"groq/llama-3.3-70b-specdec": {
|
||||
"max_tokens": 8192,
|
||||
|
|
|
@ -13,3 +13,5 @@ model_list:
|
|||
|
||||
litellm_settings:
|
||||
callbacks: ["langsmith"]
|
||||
default_internal_user_params:
|
||||
available_teams: ["litellm_dashboard_54a81fa9-9c69-45e8-b256-0c36bf104e5f", "a29a2dc6-1347-4ebc-a428-e6b56bbba611", "test-group-12"]
|
|
@ -253,6 +253,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/key/health",
|
||||
"/team/info",
|
||||
"/team/list",
|
||||
"/team/available",
|
||||
"/user/info",
|
||||
"/model/info",
|
||||
"/v2/model/info",
|
||||
|
@ -284,6 +285,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/team/info",
|
||||
"/team/block",
|
||||
"/team/unblock",
|
||||
"/team/available",
|
||||
# model
|
||||
"/model/new",
|
||||
"/model/update",
|
||||
|
@ -1563,6 +1565,7 @@ class LiteLLM_UserTable(LiteLLMPydanticObjectBase):
|
|||
rpm_limit: Optional[int] = None
|
||||
user_role: Optional[str] = None
|
||||
organization_memberships: Optional[List[LiteLLM_OrganizationMembershipTable]] = None
|
||||
teams: List[str] = []
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
|
@ -1571,6 +1574,8 @@ class LiteLLM_UserTable(LiteLLMPydanticObjectBase):
|
|||
values.update({"spend": 0.0})
|
||||
if values.get("models") is None:
|
||||
values.update({"models": []})
|
||||
if values.get("teams") is None:
|
||||
values.update({"teams": []})
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
|
|
@ -49,7 +49,9 @@ def _update_internal_new_user_params(data_json: dict, data: NewUserRequest) -> d
|
|||
is_internal_user = True
|
||||
if litellm.default_internal_user_params:
|
||||
for key, value in litellm.default_internal_user_params.items():
|
||||
if key not in data_json or data_json[key] is None:
|
||||
if key == "available_teams":
|
||||
continue
|
||||
elif key not in data_json or data_json[key] is None:
|
||||
data_json[key] = value
|
||||
elif (
|
||||
key == "models"
|
||||
|
|
|
@ -73,6 +73,14 @@ def _is_user_team_admin(
|
|||
return False
|
||||
|
||||
|
||||
def _is_available_team(team_id: str, user_api_key_dict: UserAPIKeyAuth) -> bool:
|
||||
if litellm.default_internal_user_params is None:
|
||||
return False
|
||||
if "available_teams" in litellm.default_internal_user_params:
|
||||
return team_id in litellm.default_internal_user_params["available_teams"]
|
||||
return False
|
||||
|
||||
|
||||
async def get_all_team_memberships(
|
||||
prisma_client: PrismaClient, team_id: List[str], user_id: Optional[str] = None
|
||||
) -> List[LiteLLM_TeamMembership]:
|
||||
|
@ -656,6 +664,10 @@ async def team_member_add(
|
|||
and not _is_user_team_admin(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=complete_team_data
|
||||
)
|
||||
and not _is_available_team(
|
||||
team_id=complete_team_data.team_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
@ -1363,6 +1375,62 @@ async def unblock_team(
|
|||
return record
|
||||
|
||||
|
||||
@router.get("/team/available")
|
||||
async def list_available_teams(
|
||||
http_request: Request,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
response_model=List[LiteLLM_TeamTable],
|
||||
):
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||
)
|
||||
|
||||
available_teams = cast(
|
||||
Optional[List[str]],
|
||||
(
|
||||
litellm.default_internal_user_params.get("available_teams")
|
||||
if litellm.default_internal_user_params is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
if available_teams is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "No available teams for user to join. See how to set available teams here: https://docs.litellm.ai/docs/proxy/self_serve#all-settings-for-self-serve--sso-flow"
|
||||
},
|
||||
)
|
||||
|
||||
# filter out teams that the user is already a member of
|
||||
user_info = await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": user_api_key_dict.user_id}
|
||||
)
|
||||
if user_info is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail={"error": "User not found"},
|
||||
)
|
||||
user_info_correct_type = LiteLLM_UserTable(**user_info.model_dump())
|
||||
|
||||
available_teams = [
|
||||
team for team in available_teams if team not in user_info_correct_type.teams
|
||||
]
|
||||
|
||||
available_teams_db = await prisma_client.db.litellm_teamtable.find_many(
|
||||
where={"team_id": {"in": available_teams}}
|
||||
)
|
||||
|
||||
available_teams_correct_type = [
|
||||
LiteLLM_TeamTable(**team.model_dump()) for team in available_teams_db
|
||||
]
|
||||
|
||||
return available_teams_correct_type
|
||||
|
||||
|
||||
@router.get(
|
||||
"/team/list", tags=["team management"], dependencies=[Depends(user_api_key_auth)]
|
||||
)
|
||||
|
|
|
@ -2064,7 +2064,9 @@
|
|||
"input_cost_per_token": 0.00000059,
|
||||
"output_cost_per_token": 0.00000079,
|
||||
"litellm_provider": "groq",
|
||||
"mode": "chat"
|
||||
"mode": "chat",
|
||||
"supports_function_calling": true,
|
||||
"supports_response_schema": true
|
||||
},
|
||||
"groq/llama-3.3-70b-specdec": {
|
||||
"max_tokens": 8192,
|
||||
|
|
|
@ -112,7 +112,7 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||
</a>
|
||||
<Dropdown menu={{ items }}>
|
||||
<button className="flex items-center text-sm text-gray-600 hover:text-gray-800">
|
||||
Admin
|
||||
User
|
||||
<svg
|
||||
className="ml-1 w-4 h-4"
|
||||
fill="none"
|
||||
|
|
|
@ -742,6 +742,38 @@ export const teamListCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const availableTeamListCall = async (
|
||||
accessToken: String,
|
||||
) => {
|
||||
/**
|
||||
* Get all available teams on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/team/available` : `/team/available`;
|
||||
console.log("in availableTeamListCall");
|
||||
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("/team/available_teams API Response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const organizationListCall = async (accessToken: String) => {
|
||||
/**
|
||||
* Get all organizations on proxy
|
||||
|
@ -2278,7 +2310,7 @@ export const modelUpdateCall = async (
|
|||
export interface Member {
|
||||
role: string;
|
||||
user_id: string | null;
|
||||
user_email: string | null;
|
||||
user_email?: string | null;
|
||||
}
|
||||
|
||||
export const teamMemberAddCall = async (
|
||||
|
|
143
ui/litellm-dashboard/src/components/team/available_teams.tsx
Normal file
143
ui/litellm-dashboard/src/components/team/available_teams.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Card,
|
||||
Button,
|
||||
Text,
|
||||
Badge,
|
||||
} from "@tremor/react";
|
||||
import { message } from 'antd';
|
||||
import { availableTeamListCall, teamMemberAddCall } from "../networking";
|
||||
|
||||
interface AvailableTeam {
|
||||
team_id: string;
|
||||
team_alias: string;
|
||||
description?: string;
|
||||
models: string[];
|
||||
members_with_roles: {user_id?: string, user_email?: string, role: string}[];
|
||||
}
|
||||
|
||||
interface AvailableTeamsProps {
|
||||
accessToken: string | null;
|
||||
userID: string | null;
|
||||
}
|
||||
|
||||
const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({
|
||||
accessToken,
|
||||
userID,
|
||||
}) => {
|
||||
const [availableTeams, setAvailableTeams] = useState<AvailableTeam[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAvailableTeams = async () => {
|
||||
if (!accessToken || !userID) return;
|
||||
|
||||
try {
|
||||
const response = await availableTeamListCall(accessToken);
|
||||
|
||||
setAvailableTeams(response);
|
||||
} catch (error) {
|
||||
console.error('Error fetching available teams:', error);
|
||||
message.error('Failed to load available teams');
|
||||
}
|
||||
};
|
||||
|
||||
fetchAvailableTeams();
|
||||
}, [accessToken, userID]);
|
||||
|
||||
const handleJoinTeam = async (teamId: string) => {
|
||||
if (!accessToken || !userID) return;
|
||||
|
||||
try {
|
||||
const response = await teamMemberAddCall(accessToken, teamId, {
|
||||
"user_id": userID,
|
||||
"role": "user"
|
||||
}
|
||||
);
|
||||
|
||||
message.success('Successfully joined team');
|
||||
// Update available teams list
|
||||
setAvailableTeams(teams => teams.filter(team => team.team_id !== teamId));
|
||||
} catch (error) {
|
||||
console.error('Error joining team:', error);
|
||||
message.error('Failed to join team');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Members</TableHeaderCell>
|
||||
<TableHeaderCell>Models</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{availableTeams.map((team) => (
|
||||
<TableRow key={team.team_id}>
|
||||
<TableCell>
|
||||
<Text>{team.team_alias}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.description || 'No description available'}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.members_with_roles.length} members</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
{!team.models || team.models.length === 0 ? (
|
||||
<Badge size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
team.models.map((model, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
size="xs"
|
||||
className="mb-1"
|
||||
color="blue"
|
||||
>
|
||||
<Text>
|
||||
{model.length > 30 ? `${model.slice(0, 30)}...` : model}
|
||||
</Text>
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
onClick={() => handleJoinTeam(team.team_id)}
|
||||
>
|
||||
Join Team
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{availableTeams.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center">
|
||||
<Text>No available teams to join</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailableTeamsPanel;
|
|
@ -7,6 +7,7 @@ import {
|
|||
InformationCircleIcon,
|
||||
PencilAltIcon,
|
||||
PencilIcon,
|
||||
RefreshIcon,
|
||||
StatusOnlineIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/outline";
|
||||
|
@ -42,8 +43,14 @@ import {
|
|||
Accordion,
|
||||
AccordionHeader,
|
||||
AccordionBody,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tab
|
||||
} from "@tremor/react";
|
||||
import { CogIcon } from "@heroicons/react/outline";
|
||||
import AvailableTeamsPanel from "@/components/team/available_teams";
|
||||
const isLocal = process.env.NODE_ENV === "development";
|
||||
const proxyBaseUrl = isLocal ? "http://localhost:4000" : null;
|
||||
if (isLocal != true) {
|
||||
|
@ -83,12 +90,9 @@ const Team: React.FC<TeamProps> = ({
|
|||
userID,
|
||||
userRole,
|
||||
}) => {
|
||||
const [lastRefreshed, setLastRefreshed] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`inside useeffect - ${teams}`)
|
||||
if (teams === null && accessToken) {
|
||||
// Call your function here
|
||||
const fetchData = async () => {
|
||||
const fetchTeams = async (accessToken: string, userID: string | null, userRole: string | null) => {
|
||||
let givenTeams;
|
||||
if (userRole != "Admin" && userRole != "Admin Viewer") {
|
||||
givenTeams = await teamListCall(accessToken, userID)
|
||||
|
@ -100,10 +104,23 @@ const Team: React.FC<TeamProps> = ({
|
|||
|
||||
setTeams(givenTeams)
|
||||
}
|
||||
fetchData()
|
||||
useEffect(() => {
|
||||
console.log(`inside useeffect - ${teams}`)
|
||||
if (teams === null && accessToken) {
|
||||
// Call your function here
|
||||
fetchTeams(accessToken, userID, userRole)
|
||||
}
|
||||
}, [teams]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`inside useeffect - ${lastRefreshed}`)
|
||||
if (accessToken) {
|
||||
// Call your function here
|
||||
fetchTeams(accessToken, userID, userRole)
|
||||
}
|
||||
handleRefreshClick()
|
||||
}, [lastRefreshed]);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
const [memberForm] = Form.useForm();
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
@ -123,6 +140,7 @@ const Team: React.FC<TeamProps> = ({
|
|||
const [selectedEditMember, setSelectedEditMember] = useState<null | TeamMember>(null);
|
||||
|
||||
|
||||
|
||||
const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({});
|
||||
|
||||
// Add this state near the other useState declarations
|
||||
|
@ -552,6 +570,12 @@ const Team: React.FC<TeamProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const handleRefreshClick = () => {
|
||||
// Update the 'lastRefreshed' state to the current date and time
|
||||
const currentDate = new Date();
|
||||
setLastRefreshed(currentDate.toLocaleString());
|
||||
};
|
||||
|
||||
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
||||
_common_member_update_call(formValues, "add");
|
||||
};
|
||||
|
@ -561,9 +585,27 @@ const Team: React.FC<TeamProps> = ({
|
|||
}
|
||||
return (
|
||||
<div className="w-full mx-4">
|
||||
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||
<TabGroup className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||
<div className="flex">
|
||||
<Tab>Your Teams</Tab>
|
||||
<Tab>Available Teams</Tab>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
<Icon
|
||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
||||
variant="shadow"
|
||||
size="xs"
|
||||
className="self-center"
|
||||
onClick={handleRefreshClick}
|
||||
/>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
|
||||
<Col numColSpan={1}>
|
||||
<Title level={4}>All Teams</Title>
|
||||
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[50vh]">
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
@ -1080,6 +1122,16 @@ const Team: React.FC<TeamProps> = ({
|
|||
</Modal>
|
||||
</Col>
|
||||
</Grid>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<AvailableTeamsPanel
|
||||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
|
||||
</TabGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue