fix(usage.tsx): show top users and top keys driving spend

This commit is contained in:
Krrish Dholakia 2024-02-09 19:50:07 -08:00
parent 6710c2ee5d
commit 4678c6b936
3 changed files with 129 additions and 42 deletions

View file

@ -3185,7 +3185,7 @@ async def view_spend_logs(
# SQL query # SQL query
response = await prisma_client.db.litellm_spendlogs.group_by( response = await prisma_client.db.litellm_spendlogs.group_by(
by=["api_key", "startTime"], by=["api_key", "user", "model", "startTime"],
where=filter_query, # type: ignore where=filter_query, # type: ignore
sum={ sum={
"spend": True, "spend": True,
@ -3204,14 +3204,22 @@ async def view_spend_logs(
) # type: ignore ) # type: ignore
date = dt_object.date() date = dt_object.date()
if date not in result: if date not in result:
result[date] = {} result[date] = {"users": {}, "models": {}}
api_key = record["api_key"] api_key = record["api_key"]
user_id = record["user"]
model = record["model"]
result[date]["spend"] = ( result[date]["spend"] = (
result[date].get("spend", 0) + record["_sum"]["spend"] result[date].get("spend", 0) + record["_sum"]["spend"]
) )
result[date][api_key] = ( result[date][api_key] = (
result[date].get(api_key, 0) + record["_sum"]["spend"] result[date].get(api_key, 0) + record["_sum"]["spend"]
) )
result[date]["users"][user_id] = (
result[date]["users"].get(user_id, 0) + record["_sum"]["spend"]
)
result[date]["models"][model] = (
result[date]["models"].get(model, 0) + record["_sum"]["spend"]
)
return_list = [] return_list = []
final_date = None final_date = None
for k, v in sorted(result.items()): for k, v in sorted(result.items()):
@ -3224,7 +3232,12 @@ async def view_spend_logs(
while current_date <= end_date_date: while current_date <= end_date_date:
# Represent current_date as string because original response has it this way # Represent current_date as string because original response has it this way
return_list.append( return_list.append(
{"startTime": current_date, "spend": 0} {
"startTime": current_date,
"spend": 0,
"users": {},
"models": {},
}
) # If no data, will stay as zero ) # If no data, will stay as zero
current_date += timedelta(days=1) # Move on to the next day current_date += timedelta(days=1) # Move on to the next day

View file

@ -27,7 +27,13 @@ const customTooltip = (props: CustomTooltipTypeBar) => {
// Convert the object into an array of key-value pairs // Convert the object into an array of key-value pairs
const entries: [string, number][] = Object.entries(value) const entries: [string, number][] = Object.entries(value)
.filter(([key]) => key !== "spend" && key !== "startTime") .filter(
([key]) =>
key !== "spend" &&
key !== "startTime" &&
key !== "models" &&
key !== "users"
)
.map(([key, value]) => [key, value as number]); // Type assertion to specify the value as number .map(([key, value]) => [key, value as number]); // Type assertion to specify the value as number
// Sort the array based on the float value in descending order // Sort the array based on the float value in descending order
@ -53,26 +59,68 @@ const customTooltip = (props: CustomTooltipTypeBar) => {
))} ))}
</div> </div>
); );
// return (
// <div className="w-56 rounded-tremor-default border border-tremor-border bg-tremor-background p-2 text-tremor-default shadow-tremor-dropdown">
// {payload.map((category: any, idx: number) => {
// <div key={idx} className="flex flex-1 space-x-2.5">
// <div
// className={`flex w-1 flex-col bg-${category.color}-500 rounded`}
// />
// <div className="space-y-1">
// <p className="text-tremor-content">{category.dataKey}</p>
// <p className="font-medium text-tremor-content-emphasis">
// {category.value} bpm
// </p>
// </div>
// </div>;
// })}
// </div>
// );
}; };
function getTopKeys(data: Array<{ [key: string]: unknown }>): any[] {
const spendKeys: { key: string; spend: unknown }[] = [];
data.forEach((dict) => {
Object.entries(dict).forEach(([key, value]) => {
if (
key !== "spend" &&
key !== "startTime" &&
key !== "models" &&
key !== "users"
) {
spendKeys.push({ key, spend: value });
}
});
});
spendKeys.sort((a, b) => Number(b.spend) - Number(a.spend));
const topKeys = spendKeys.slice(0, 5);
console.log(`topKeys: ${Object.keys(topKeys[0])}`);
return topKeys;
}
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> = ({ const UsagePage: React.FC<UsagePageProps> = ({
accessToken, accessToken,
token, token,
@ -81,7 +129,8 @@ const UsagePage: React.FC<UsagePageProps> = ({
}) => { }) => {
const currentDate = new Date(); const currentDate = new Date();
const [keySpendData, setKeySpendData] = useState<any[]>([]); const [keySpendData, setKeySpendData] = useState<any[]>([]);
const [keyCategories, setKeyCategories] = useState<string[]>([]); const [topKeys, setTopKeys] = useState<any[]>([]);
const [topUsers, setTopUsers] = useState<any[]>([]);
const firstDay = new Date( const firstDay = new Date(
currentDate.getFullYear(), currentDate.getFullYear(),
@ -128,23 +177,9 @@ const UsagePage: React.FC<UsagePageProps> = ({
endTime endTime
); );
const uniqueKeys: Set<string> = new Set(); setTopKeys(getTopKeys(response));
setTopUsers(getTopUsers(response));
response.forEach((item: any) => {
Object.keys(item).forEach((key) => {
if (key !== "spend" && key !== "startTime") {
uniqueKeys.add(key);
}
});
});
let uniqueKeysList = Array.from(uniqueKeys);
setKeyCategories(uniqueKeysList);
setKeySpendData(response); setKeySpendData(response);
// localStorage.setItem("keySpendData", JSON.stringify(response));
// localStorage.setItem(
// "keyCategories",
// JSON.stringify(uniqueKeysList)
// );
} catch (error) { } catch (error) {
console.error("There was an error fetching the data", error); console.error("There was an error fetching the data", error);
// Optionally, update your UI to reflect the error state here as well // Optionally, update your UI to reflect the error state here as well
@ -153,10 +188,16 @@ const UsagePage: React.FC<UsagePageProps> = ({
fetchData(); fetchData();
} }
}, [accessToken, token, userRole, userID]); }, [accessToken, token, userRole, userID]);
topUsers.forEach((obj) => {
Object.values(obj).forEach((value) => {
console.log(value);
});
});
return ( return (
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Grid numItems={1} className="gap-0 p-10 h-[75vh] w-full"> <Grid numItems={2} className="gap-2 p-10 h-[75vh] w-full">
<Col numColSpan={1}> <Col numColSpan={2}>
<Card> <Card>
<Title>Monthly Spend</Title> <Title>Monthly Spend</Title>
<BarChart <BarChart
@ -171,6 +212,39 @@ const UsagePage: React.FC<UsagePageProps> = ({
/> />
</Card> </Card>
</Col> </Col>
<Col numColSpan={1}>
<Card>
<Title>Top API Keys</Title>
<BarChart
className="mt-4 h-40"
data={topKeys}
index="key"
categories={["spend"]}
colors={["blue"]}
yAxisWidth={200}
tickGap={5}
layout="vertical"
showXAxis={false}
showLegend={false}
/>
</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>
</Grid> </Grid>
</div> </div>
); );

View file

@ -89,7 +89,7 @@ module.exports = {
"tremor-full": "9999px", "tremor-full": "9999px",
}, },
fontSize: { fontSize: {
"tremor-label": ["0.75rem", { lineHeight: "1rem" }], "tremor-label": ["0.75rem", { lineHeight: "0.4rem" }],
"tremor-default": ["0.875rem", { lineHeight: "1.25rem" }], "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
"tremor-title": ["1.125rem", { lineHeight: "1.75rem" }], "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
"tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }], "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],