Merge pull request #3530 from BerriAI/ui_show_spend_end_user

[UI] show `End-User` Usage on Usage Tab
This commit is contained in:
Ishaan Jaff 2024-05-08 18:36:51 -07:00 committed by GitHub
commit b15c1c907a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 174 additions and 132 deletions

View file

@ -981,7 +981,7 @@ def anthropic_messages_pt(messages: list):
# add role=tool support to allow function call result/error submission # add role=tool support to allow function call result/error submission
user_message_types = {"user", "tool", "function"} user_message_types = {"user", "tool", "function"}
# reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, merge them. # reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, merge them.
new_messages = [] new_messages: list = []
msg_i = 0 msg_i = 0
tool_use_param = False tool_use_param = False
while msg_i < len(messages): while msg_i < len(messages):

View file

@ -494,6 +494,8 @@ class NewTeamRequest(TeamBase):
class GlobalEndUsersSpend(LiteLLMBase): class GlobalEndUsersSpend(LiteLLMBase):
api_key: Optional[str] = None api_key: Optional[str] = None
startTime: Optional[datetime] = None
endTime: Optional[datetime] = None
class TeamMemberAddRequest(LiteLLMBase): class TeamMemberAddRequest(LiteLLMBase):

View file

@ -5863,35 +5863,38 @@ async def global_spend_end_users(data: Optional[GlobalEndUsersSpend] = None):
if prisma_client is None: if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"}) raise HTTPException(status_code=500, detail={"error": "No db connected"})
if data is None:
sql_query = f"""SELECT * FROM "Last30dTopEndUsersSpend";"""
response = await prisma_client.db.query_raw(query=sql_query)
else:
""" """
Gets the top 100 end-users for a given api key Gets the top 100 end-users for a given api key
""" """
current_date = datetime.now() startTime = None
past_date = current_date - timedelta(days=30) endTime = None
response = await prisma_client.db.litellm_spendlogs.group_by( # type: ignore selected_api_key = None
by=["end_user"], if data is not None:
where={ startTime = data.startTime
"AND": [{"startTime": {"gte": past_date}}, {"api_key": data.api_key}] # type: ignore endTime = data.endTime
}, selected_api_key = data.api_key
sum={"spend": True},
order={"_sum": {"spend": "desc"}}, # type: ignore startTime = startTime or datetime.now() - timedelta(days=30)
take=100, endTime = endTime or datetime.now()
count=True,
sql_query = """
SELECT end_user, COUNT(*) AS total_count, SUM(spend) AS total_spend
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= $1::timestamp
AND "startTime" < $2::timestamp
AND (
CASE
WHEN $3::TEXT IS NULL THEN TRUE
ELSE api_key = $3
END
)
GROUP BY end_user
ORDER BY total_spend DESC
LIMIT 100
"""
response = await prisma_client.db.query_raw(
sql_query, startTime, endTime, selected_api_key
) )
if response is not None and isinstance(response, list):
new_response = []
for r in response:
new_r = r
new_r["total_spend"] = r["_sum"]["spend"]
new_r["total_count"] = r["_count"]["_all"]
new_r.pop("_sum")
new_r.pop("_count")
new_response.append(new_r)
return response return response

View file

@ -189,6 +189,7 @@ const CreateKeyPage = () => {
userRole={userRole} userRole={userRole}
token={token} token={token}
accessToken={accessToken} accessToken={accessToken}
keys={keys}
/> />
)} )}
</div> </div>

View file

@ -786,7 +786,9 @@ export const adminTopKeysCall = async (accessToken: String) => {
export const adminTopEndUsersCall = async ( export const adminTopEndUsersCall = async (
accessToken: String, accessToken: String,
keyToken: String | null keyToken: String | null,
startTime: String | undefined,
endTime: String | undefined
) => { ) => {
try { try {
let url = proxyBaseUrl let url = proxyBaseUrl
@ -795,8 +797,11 @@ export const adminTopEndUsersCall = async (
let body = ""; let body = "";
if (keyToken) { if (keyToken) {
body = JSON.stringify({ api_key: keyToken }); body = JSON.stringify({ api_key: keyToken, startTime: startTime, endTime: endTime });
} else {
body = JSON.stringify({ startTime: startTime, endTime: endTime });
} }
//message.info("Making top end users request"); //message.info("Making top end users request");
// Define requestOptions with body as an optional property // Define requestOptions with body as an optional property
@ -815,9 +820,7 @@ export const adminTopEndUsersCall = async (
}, },
}; };
if (keyToken) { requestOptions.body = body;
requestOptions.body = JSON.stringify({ api_key: keyToken });
}
const response = await fetch(url, requestOptions); const response = await fetch(url, requestOptions);
if (!response.ok) { if (!response.ok) {

View file

@ -3,13 +3,14 @@ import { BarChart, BarList, Card, Title, Table, TableHead, TableHeaderCell, Tabl
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import ViewUserSpend from "./view_user_spend"; import ViewUserSpend from "./view_user_spend";
import { Grid, Col, Text, LineChart, TabPanel, TabPanels, TabGroup, TabList, Tab, Select, SelectItem } from "@tremor/react"; import { Grid, Col, Text, LineChart, TabPanel, TabPanels, TabGroup, TabList, Tab, Select, SelectItem, DateRangePicker, DateRangePickerValue } from "@tremor/react";
import { import {
userSpendLogsCall, userSpendLogsCall,
keyInfoCall, keyInfoCall,
adminSpendLogsCall, adminSpendLogsCall,
adminTopKeysCall, adminTopKeysCall,
adminTopModelsCall, adminTopModelsCall,
adminTopEndUsersCall,
teamSpendLogsCall, teamSpendLogsCall,
tagsSpendLogsCall, tagsSpendLogsCall,
modelMetricsCall, modelMetricsCall,
@ -23,6 +24,7 @@ interface UsagePageProps {
token: string | null; token: string | null;
userRole: string | null; userRole: string | null;
userID: string | null; userID: string | null;
keys: any[] | null;
} }
type CustomTooltipTypeBar = { type CustomTooltipTypeBar = {
@ -95,47 +97,14 @@ function getTopKeys(data: Array<{ [key: string]: unknown }>): any[] {
} }
type DataDict = { [key: string]: unknown }; type DataDict = { [key: string]: unknown };
type UserData = { user_id: string; spend: number }; type UserData = { user_id: string; spend: number };
function getTopUsers(data: Array<DataDict>): UserData[] {
const userSpend: { [key: string]: number } = {};
data.forEach((dict) => {
const payload: DataDict = dict["users"] as DataDict;
Object.entries(payload).forEach(([user_id, value]) => {
if (
user_id === "" ||
user_id === undefined ||
user_id === null ||
user_id == "None"
) {
return;
}
if (!userSpend[user_id]) {
userSpend[user_id] = 0;
}
userSpend[user_id] += value as number;
});
});
const spendUsers: UserData[] = Object.entries(userSpend).map(
([user_id, spend]) => ({
user_id,
spend,
})
);
spendUsers.sort((a, b) => b.spend - a.spend);
const topKeys = spendUsers.slice(0, 5);
console.log(`topKeys: ${Object.values(topKeys[0])}`);
return topKeys;
}
const UsagePage: React.FC<UsagePageProps> = ({ const UsagePage: React.FC<UsagePageProps> = ({
accessToken, accessToken,
token, token,
userRole, userRole,
userID, userID,
keys,
}) => { }) => {
const currentDate = new Date(); const currentDate = new Date();
const [keySpendData, setKeySpendData] = useState<any[]>([]); const [keySpendData, setKeySpendData] = useState<any[]>([]);
@ -146,6 +115,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
const [topTagsData, setTopTagsData] = useState<any[]>([]); const [topTagsData, setTopTagsData] = useState<any[]>([]);
const [uniqueTeamIds, setUniqueTeamIds] = useState<any[]>([]); const [uniqueTeamIds, setUniqueTeamIds] = useState<any[]>([]);
const [totalSpendPerTeam, setTotalSpendPerTeam] = useState<any[]>([]); const [totalSpendPerTeam, setTotalSpendPerTeam] = useState<any[]>([]);
const [selectedKeyID, setSelectedKeyID] = useState<string | null>("");
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
to: new Date(),
});
const firstDay = new Date( const firstDay = new Date(
currentDate.getFullYear(), currentDate.getFullYear(),
@ -161,6 +135,26 @@ const UsagePage: React.FC<UsagePageProps> = ({
let startTime = formatDate(firstDay); let startTime = formatDate(firstDay);
let endTime = formatDate(lastDay); let endTime = formatDate(lastDay);
console.log("keys in usage", keys);
const updateEndUserData = async (startTime: Date | undefined, endTime: Date | undefined, uiSelectedKey: string | null) => {
if (!startTime || !endTime || !accessToken) {
return;
}
console.log("uiSelectedKey", uiSelectedKey);
let newTopUserData = await adminTopEndUsersCall(
accessToken,
uiSelectedKey,
startTime.toISOString(),
endTime.toISOString()
)
console.log("End user data updated successfully", newTopUserData);
setTopUsers(newTopUserData);
}
function formatDate(date: Date) { function formatDate(date: Date) {
const year = date.getFullYear(); const year = date.getFullYear();
let month = date.getMonth() + 1; // JS month index starts from 0 let month = date.getMonth() + 1; // JS month index starts from 0
@ -227,6 +221,12 @@ const UsagePage: React.FC<UsagePageProps> = ({
const top_tags = await tagsSpendLogsCall(accessToken); const top_tags = await tagsSpendLogsCall(accessToken);
setTopTagsData(top_tags.top_10_tags); setTopTagsData(top_tags.top_10_tags);
// get spend per end-user
let spend_user_call = await adminTopEndUsersCall(accessToken, null);
setTopUsers(spend_user_call);
console.log("spend/user result", spend_user_call);
} else if (userRole == "App Owner") { } else if (userRole == "App Owner") {
await userSpendLogsCall( await userSpendLogsCall(
accessToken, accessToken,
@ -258,7 +258,6 @@ const UsagePage: React.FC<UsagePageProps> = ({
spend: k["spend"], spend: k["spend"],
})); }));
setTopKeys(filtered_keys); setTopKeys(filtered_keys);
setTopUsers(getTopUsers(response));
setKeySpendData(response); setKeySpendData(response);
} }
}); });
@ -286,6 +285,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
<TabList className="mt-2"> <TabList className="mt-2">
<Tab>All Up</Tab> <Tab>All Up</Tab>
<Tab>Team Based Usage</Tab> <Tab>Team Based Usage</Tab>
<Tab>End User Usage</Tab>
<Tab>Tag Based Usage</Tab> <Tab>Tag Based Usage</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
@ -323,22 +323,6 @@ const UsagePage: React.FC<UsagePageProps> = ({
/> />
</Card> </Card>
</Col> </Col>
<Col numColSpan={1}>
<Card>
<Title>Top Users</Title>
<BarChart
className="mt-4 h-40"
data={topUsers}
index="user_id"
categories={["spend"]}
colors={["blue"]}
yAxisWidth={200}
layout="vertical"
showXAxis={false}
showLegend={false}
/>
</Card>
</Col>
<Col numColSpan={1}> <Col numColSpan={1}>
<Card> <Card>
<Title>Top Models</Title> <Title>Top Models</Title>
@ -354,6 +338,10 @@ const UsagePage: React.FC<UsagePageProps> = ({
showLegend={false} showLegend={false}
/> />
</Card> </Card>
</Col>
<Col numColSpan={1}>
</Col> </Col>
</Grid> </Grid>
</TabPanel> </TabPanel>
@ -385,6 +373,88 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Col numColSpan={2}> <Col numColSpan={2}>
</Col> </Col>
</Grid> </Grid>
</TabPanel>
<TabPanel>
<p className="mb-2 text-gray-500 italic text-[12px]">End-Users of your LLM API calls. Tracked when a `user` param is passed in your LLM calls <a className="text-blue-500" href="https://docs.litellm.ai/docs/proxy/users" target="_blank">docs here</a></p>
<Grid numItems={2}>
<Col>
<Text>Select Time Range</Text>
<DateRangePicker
enableSelect={true}
value={dateValue}
onValueChange={(value) => {
setDateValue(value);
updateEndUserData(value.from, value.to); // Call updateModelMetrics with the new date range
}}
/>
</Col>
<Col>
<Text>Select Key</Text>
<Select defaultValue="all-keys">
<SelectItem
key="all-keys"
value="all-keys"
onClick={() => {
updateEndUserData(dateValue.from, dateValue.to, null);
}}
>
All Keys
</SelectItem>
{keys?.map((key: any, index: number) => {
if (
key &&
key["key_alias"] !== null &&
key["key_alias"].length > 0
) {
return (
<SelectItem
key={index}
value={String(index)}
onClick={() => {
updateEndUserData(dateValue.from, dateValue.to, key["token"]);
}}
>
{key["key_alias"]}
</SelectItem>
);
}
return null; // Add this line to handle the case when the condition is not met
})}
</Select>
</Col>
</Grid>
<Card className="mt-4">
<Table className="max-h-[70vh] min-h-[500px]">
<TableHead>
<TableRow>
<TableHeaderCell>End User</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Total Events</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{topUsers?.map((user: any, index: number) => (
<TableRow key={index}>
<TableCell>{user.end_user}</TableCell>
<TableCell>{user.total_spend?.toFixed(4)}</TableCell>
<TableCell>{user.total_count}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<Grid numItems={2} className="gap-2 h-[75vh] w-full mb-4"> <Grid numItems={2} className="gap-2 h-[75vh] w-full mb-4">

View file

@ -83,22 +83,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
fetchData(); fetchData();
} }
const fetchEndUserSpend = async () => {
try {
const topEndUsers = await adminTopEndUsersCall(accessToken, null);
console.log("user data response:", topEndUsers);
setEndUsers(topEndUsers);
} catch (error) {
console.error("There was an error fetching the model data", error);
}
};
if (
userRole &&
(userRole == "Admin" || userRole == "Admin Viewer") &&
!endUsers
) {
fetchEndUserSpend();
}
}, [accessToken, token, userRole, userID, currentPage]); }, [accessToken, token, userRole, userID, currentPage]);
if (!userData) { if (!userData) {
@ -157,14 +142,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<CreateUser userID={userID} accessToken={accessToken} teams={teams}/> <CreateUser userID={userID} accessToken={accessToken} teams={teams}/>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4"> <Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
<div className="mb-4 mt-1"> <div className="mb-4 mt-1">
<Text><b>Key Owners: </b> Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM</Text> <Text>These are Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM</Text>
<Text className="mt-1"><b>End Users: </b>End Users of your LLM API calls. Tracked When a `user` param is passed in your LLM calls</Text>
</div> </div>
<TabGroup> <TabGroup>
<TabList variant="line" defaultValue="1">
<Tab value="1">Key Owners</Tab>
<Tab value="2">End-Users</Tab>
</TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
@ -190,7 +172,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
? user.models ? user.models
: "All Models"} : "All Models"}
</TableCell> </TableCell>
<TableCell>{user.spend ? user.spend : 0}</TableCell> <TableCell>{user.spend ? user.spend?.toFixed(2) : 0}</TableCell>
<TableCell> <TableCell>
{user.max_budget ? user.max_budget : "Unlimited"} {user.max_budget ? user.max_budget : "Unlimited"}
</TableCell> </TableCell>
@ -220,29 +202,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-1"></div> <div className="flex-1"></div>
<div className="flex-1 flex justify-between items-center"> <div className="flex-1 flex justify-between items-center">
<Text className="w-1/4 mr-2 text-right">Key</Text>
<Select defaultValue="1" className="w-3/4">
{keys?.map((key: any, index: number) => {
if (
key &&
key["key_alias"] !== null &&
key["key_alias"].length > 0
) {
return (
<SelectItem
key={index}
value={String(index)}
onClick={() => onKeyClick(key["token"])}
>
{key["key_alias"]}
</SelectItem>
);
}
})}
</Select>
</div> </div>
</div> </div>
<Table className="max-h-[70vh] min-h-[500px]"> {/* <Table className="max-h-[70vh] min-h-[500px]">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableHeaderCell>End User</TableHeaderCell> <TableHeaderCell>End User</TableHeaderCell>
@ -260,7 +223,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
</TableRow> </TableRow>
))} ))}
</TableBody> </TableBody>
</Table> </Table> */}
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
</TabGroup> </TabGroup>