diff --git a/litellm/llms/prompt_templates/factory.py b/litellm/llms/prompt_templates/factory.py index 082030368..24a076dd0 100644 --- a/litellm/llms/prompt_templates/factory.py +++ b/litellm/llms/prompt_templates/factory.py @@ -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): diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b7b2d0ab6..0a86c8424 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -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): diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 53352a4f8..c1394a5a1 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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 diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 96731f7f5..ce6925081 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -189,6 +189,7 @@ const CreateKeyPage = () => { userRole={userRole} token={token} accessToken={accessToken} + keys={keys} /> )} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index d65482b8f..e1375e7d4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -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) { diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index f0b4f0551..933bc0c36 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -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): 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 = ({ accessToken, token, userRole, userID, + keys, }) => { const currentDate = new Date(); const [keySpendData, setKeySpendData] = useState([]); @@ -146,6 +115,11 @@ const UsagePage: React.FC = ({ const [topTagsData, setTopTagsData] = useState([]); const [uniqueTeamIds, setUniqueTeamIds] = useState([]); const [totalSpendPerTeam, setTotalSpendPerTeam] = useState([]); + const [selectedKeyID, setSelectedKeyID] = useState(""); + const [dateValue, setDateValue] = useState({ + 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 = ({ 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 = ({ 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 = ({ spend: k["spend"], })); setTopKeys(filtered_keys); - setTopUsers(getTopUsers(response)); setKeySpendData(response); } }); @@ -286,6 +285,7 @@ const UsagePage: React.FC = ({ All Up Team Based Usage + End User Usage Tag Based Usage @@ -324,23 +324,7 @@ const UsagePage: React.FC = ({ - - Top Users - - - - - + Top Models = ({ showLegend={false} /> + + + + @@ -385,6 +373,88 @@ const UsagePage: React.FC = ({ + + +

End-Users of your LLM API calls. Tracked when a `user` param is passed in your LLM calls docs here

+ + + Select Time Range + + { + setDateValue(value); + updateEndUserData(value.from, value.to); // Call updateModelMetrics with the new date range + }} + /> + + + Select Key + + + + + + + + + + + + + + + End User + Spend + Total Events + + + + + {topUsers?.map((user: any, index: number) => ( + + {user.end_user} + {user.total_spend?.toFixed(4)} + {user.total_count} + + ))} + +
+ +
+
diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index b3960965c..55d5f59eb 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -83,22 +83,7 @@ const ViewUserDashboard: React.FC = ({ 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 = ({
- Key Owners: Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM - End Users: End Users of your LLM API calls. Tracked When a `user` param is passed in your LLM calls + These are Users on LiteLLM that created API Keys. Automatically tracked by LiteLLM +
- - Key Owners - End-Users - + @@ -190,7 +172,7 @@ const ViewUserDashboard: React.FC = ({ ? user.models : "All Models"} - {user.spend ? user.spend : 0} + {user.spend ? user.spend?.toFixed(2) : 0} {user.max_budget ? user.max_budget : "Unlimited"} @@ -220,29 +202,10 @@ const ViewUserDashboard: React.FC = ({
- Key - +
- + {/*
End User @@ -260,7 +223,7 @@ const ViewUserDashboard: React.FC = ({ ))} -
+ */}