Skip to content

Commit

Permalink
Litellm dev 01 24 2025 p4 (#7992)
Browse files Browse the repository at this point in the history
* 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'
  • Loading branch information
krrishdholakia authored Jan 25, 2025
1 parent f778829 commit 1ab10d8
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 21 deletions.
4 changes: 3 additions & 1 deletion litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion litellm/proxy/_new_secret_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ model_list:
num_retries: 0

litellm_settings:
callbacks: ["langsmith"]
callbacks: ["langsmith"]
default_internal_user_params:
available_teams: ["litellm_dashboard_54a81fa9-9c69-45e8-b256-0c36bf104e5f", "a29a2dc6-1347-4ebc-a428-e6b56bbba611", "test-group-12"]
5 changes: 5 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ class LiteLLMRoutes(enum.Enum):
"/key/health",
"/team/info",
"/team/list",
"/team/available",
"/user/info",
"/model/info",
"/v2/model/info",
Expand Down Expand Up @@ -284,6 +285,7 @@ class LiteLLMRoutes(enum.Enum):
"/team/info",
"/team/block",
"/team/unblock",
"/team/available",
# model
"/model/new",
"/model/update",
Expand Down Expand Up @@ -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
Expand All @@ -1571,6 +1574,8 @@ def set_model_info(cls, values):
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=())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
68 changes: 68 additions & 0 deletions litellm/proxy/management_endpoints/team_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)]
)
Expand Down
4 changes: 3 additions & 1 deletion model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
34 changes: 33 additions & 1 deletion ui/litellm-dashboard/src/components/networking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
143 changes: 143 additions & 0 deletions ui/litellm-dashboard/src/components/team/available_teams.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 1ab10d8

Please sign in to comment.