forked from phoenix/litellm-mirror
Merge pull request #3530 from BerriAI/ui_show_spend_end_user
[UI] show `End-User` Usage on Usage Tab
This commit is contained in:
commit
b15c1c907a
7 changed files with 174 additions and 132 deletions
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -189,6 +189,7 @@ const CreateKeyPage = () => {
|
|||
userRole={userRole}
|
||||
token={token}
|
||||
accessToken={accessToken}
|
||||
keys={keys}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue