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
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.
new_messages = []
new_messages: list = []
msg_i = 0
tool_use_param = False
while msg_i < len(messages):

View file

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

View file

@ -5863,35 +5863,38 @@ async def global_spend_end_users(data: Optional[GlobalEndUsersSpend] = None):
if prisma_client is None:
raise HTTPException(status_code=500, detail={"error": "No db connected"})
if data is None:
sql_query = f"""SELECT * FROM "Last30dTopEndUsersSpend";"""
"""
Gets the top 100 end-users for a given api key
"""
startTime = None
endTime = None
selected_api_key = None
if data is not None:
startTime = data.startTime
endTime = data.endTime
selected_api_key = data.api_key
response = await prisma_client.db.query_raw(query=sql_query)
else:
"""
Gets the top 100 end-users for a given api key
"""
current_date = datetime.now()
past_date = current_date - timedelta(days=30)
response = await prisma_client.db.litellm_spendlogs.group_by( # type: ignore
by=["end_user"],
where={
"AND": [{"startTime": {"gte": past_date}}, {"api_key": data.api_key}] # type: ignore
},
sum={"spend": True},
order={"_sum": {"spend": "desc"}}, # type: ignore
take=100,
count=True,
)
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)
startTime = startTime or datetime.now() - timedelta(days=30)
endTime = endTime or datetime.now()
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
)
return response

View file

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

View file

@ -786,7 +786,9 @@ export const adminTopKeysCall = async (accessToken: String) => {
export const adminTopEndUsersCall = async (
accessToken: String,
keyToken: String | null
keyToken: String | null,
startTime: String | undefined,
endTime: String | undefined
) => {
try {
let url = proxyBaseUrl
@ -795,8 +797,11 @@ export const adminTopEndUsersCall = async (
let body = "";
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");
// Define requestOptions with body as an optional property
@ -815,9 +820,7 @@ export const adminTopEndUsersCall = async (
},
};
if (keyToken) {
requestOptions.body = JSON.stringify({ api_key: keyToken });
}
requestOptions.body = body;
const response = await fetch(url, requestOptions);
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 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 {
userSpendLogsCall,
keyInfoCall,
adminSpendLogsCall,
adminTopKeysCall,
adminTopModelsCall,
adminTopEndUsersCall,
teamSpendLogsCall,
tagsSpendLogsCall,
modelMetricsCall,
@ -23,6 +24,7 @@ interface UsagePageProps {
token: string | null;
userRole: string | null;
userID: string | null;
keys: any[] | null;
}
type CustomTooltipTypeBar = {
@ -95,47 +97,14 @@ function getTopKeys(data: Array<{ [key: string]: unknown }>): any[] {
}
type DataDict = { [key: string]: unknown };
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> = ({
accessToken,
token,
userRole,
userID,
keys,
}) => {
const currentDate = new Date();
const [keySpendData, setKeySpendData] = useState<any[]>([]);
@ -146,6 +115,11 @@ const UsagePage: React.FC<UsagePageProps> = ({
const [topTagsData, setTopTagsData] = useState<any[]>([]);
const [uniqueTeamIds, setUniqueTeamIds] = 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(
currentDate.getFullYear(),
@ -161,6 +135,26 @@ const UsagePage: React.FC<UsagePageProps> = ({
let startTime = formatDate(firstDay);
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) {
const year = date.getFullYear();
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);
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") {
await userSpendLogsCall(
accessToken,
@ -258,7 +258,6 @@ const UsagePage: React.FC<UsagePageProps> = ({
spend: k["spend"],
}));
setTopKeys(filtered_keys);
setTopUsers(getTopUsers(response));
setKeySpendData(response);
}
});
@ -286,6 +285,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
<TabList className="mt-2">
<Tab>All Up</Tab>
<Tab>Team Based Usage</Tab>
<Tab>End User Usage</Tab>
<Tab>Tag Based Usage</Tab>
</TabList>
<TabPanels>
@ -324,23 +324,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
</Card>
</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}>
<Card>
<Card>
<Title>Top Models</Title>
<BarChart
className="mt-4 h-40"
@ -354,6 +338,10 @@ const UsagePage: React.FC<UsagePageProps> = ({
showLegend={false}
/>
</Card>
</Col>
<Col numColSpan={1}>
</Col>
</Grid>
</TabPanel>
@ -385,6 +373,88 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Col numColSpan={2}>
</Col>
</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>
<Grid numItems={2} className="gap-2 h-[75vh] w-full mb-4">

View file

@ -83,22 +83,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
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]);
if (!userData) {
@ -157,14 +142,11 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<CreateUser userID={userID} accessToken={accessToken} teams={teams}/>
<Card className="w-full mx-auto flex-auto overflow-y-auto max-h-[80vh] mb-4">
<div className="mb-4 mt-1">
<Text><b>Key Owners: </b> 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>
<Text>These are Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM</Text>
</div>
<TabGroup>
<TabList variant="line" defaultValue="1">
<Tab value="1">Key Owners</Tab>
<Tab value="2">End-Users</Tab>
</TabList>
<TabPanels>
<TabPanel>
@ -190,7 +172,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
? user.models
: "All Models"}
</TableCell>
<TableCell>{user.spend ? user.spend : 0}</TableCell>
<TableCell>{user.spend ? user.spend?.toFixed(2) : 0}</TableCell>
<TableCell>
{user.max_budget ? user.max_budget : "Unlimited"}
</TableCell>
@ -220,29 +202,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
<div className="flex items-center">
<div className="flex-1"></div>
<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>
<Table className="max-h-[70vh] min-h-[500px]">
{/* <Table className="max-h-[70vh] min-h-[500px]">
<TableHead>
<TableRow>
<TableHeaderCell>End User</TableHeaderCell>
@ -260,7 +223,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
</TableRow>
))}
</TableBody>
</Table>
</Table> */}
</TabPanel>
</TabPanels>
</TabGroup>