From c60fa9c84d103d0d789b78284895bd4f7ebc38da Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 29 Feb 2024 16:04:52 -0800 Subject: [PATCH 1/3] feat(proxy/utils.py): enable end_user + team id tracking in spend logs allows separating proxy owner vs. end user in the spen d logs --- litellm/proxy/proxy_server.py | 2 +- litellm/proxy/schema.prisma | 3 ++- litellm/proxy/utils.py | 8 +++++++- schema.prisma | 2 ++ ui/litellm-dashboard/src/components/admins.tsx | 9 ++++++--- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 39eaf6519d5..544eb77315a 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1152,9 +1152,9 @@ async def update_database( payload["spend"] = response_cost if prisma_client is not None: await prisma_client.insert_data(data=payload, table_name="spend") - elif custom_db_client is not None: await custom_db_client.insert_data(payload, table_name="spend") + except Exception as e: verbose_proxy_logger.info(f"Update Spend Logs DB failed to execute") diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 7e8e26fcdf9..eaf15d79266 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -97,8 +97,9 @@ model LiteLLM_SpendLogs { cache_hit String @default("") cache_key String @default("") request_tags Json @default("[]") + team_id String? + end_user String? } - // Beta - allow team members to request access to a model model LiteLLM_UserNotifications { request_id String @unique diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 6a010944333..76fc21242be 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1528,7 +1528,12 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time): "startTime": start_time, "endTime": end_time, "model": kwargs.get("model", ""), - "user": kwargs.get("user", ""), + "user": kwargs.get("litellm_params", {}) + .get("metadata", {}) + .get("user_api_key_user_id", ""), + "team_id": kwargs.get("litellm_params", {}) + .get("metadata", {}) + .get("user_api_key_team_id", ""), "metadata": metadata, "cache_key": cache_key, "spend": kwargs.get("response_cost", 0), @@ -1536,6 +1541,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time): "prompt_tokens": usage.get("prompt_tokens", 0), "completion_tokens": usage.get("completion_tokens", 0), "request_tags": metadata.get("tags", []), + "end_user": kwargs.get("user", ""), } verbose_proxy_logger.debug(f"SpendTable: created payload - payload: {payload}\n\n") diff --git a/schema.prisma b/schema.prisma index 7e8e26fcdf9..383b8d96267 100644 --- a/schema.prisma +++ b/schema.prisma @@ -97,6 +97,8 @@ model LiteLLM_SpendLogs { cache_hit String @default("") cache_key String @default("") request_tags Json @default("[]") + team_id String? + end_user String? } // Beta - allow team members to request access to a model diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 13cc34cfd83..1d0568752ba 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -138,10 +138,13 @@ const AdminPanel: React.FC = ({ console.log(`admins: ${admins?.length}`); return (
- Proxy Admins + Restricted Access - Add other people to just view global spend. They cannot create teams or - grant users access to new models. + Add other people to just view spend. They cannot create keys, teams or + grant users access to new models.{" "} + + Requires SSO Setup + From 1788c894f2522374b1f20ad1228b1a62e7dc22a9 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 29 Feb 2024 16:30:12 -0800 Subject: [PATCH 2/3] fix: enable end user tracking on ui --- litellm/proxy/proxy_server.py | 22 ++++++ litellm/proxy/utils.py | 20 +++++ .../src/components/networking.tsx | 40 ++++++++-- .../src/components/view_users.tsx | 73 ++++++++++++++----- 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 544eb77315a..b5bf0b41896 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4113,6 +4113,28 @@ async def global_spend_keys( return response +@router.get( + "/global/spend/end_users", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], +) +async def global_spend_end_users(): + """ + [BETA] This is a beta endpoint. It will change. + + Use this to get the top 'n' keys with the highest spend, ordered by spend. + """ + global prisma_client + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + sql_query = f"""SELECT * FROM "Last30dTopEndUsersSpend";""" + + response = await prisma_client.db.query_raw(query=sql_query) + + return response + + @router.get( "/global/spend/models", tags=["Budget & Spend Tracking"], diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 76fc21242be..c7213d465cd 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -593,6 +593,26 @@ class PrismaClient: print("Last30dModelsBySpend Created!") # noqa + try: + await self.db.query_raw( + """SELECT 1 FROM "Last30dTopEndUsersSpend" LIMIT 1""" + ) + print("Last30dTopEndUsersSpend Exists!") # noqa + except Exception as e: + sql_query = """ + CREATE VIEW "Last30dTopEndUsersSpend" AS + SELECT end_user, SUM(spend) AS total_spend + FROM "LiteLLM_SpendLogs" + WHERE end_user <> '' AND end_user <> user + AND "startTime" >= CURRENT_DATE - INTERVAL '30 days' + GROUP BY end_user + ORDER BY total_spend DESC + LIMIT 100; + """ + await self.db.execute_raw(query=sql_query) + + print("Last30dTopEndUsersSpend Created!") # noqa + return @backoff.on_exception( diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 0588ba34b71..b64ec7e684c 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -404,13 +404,13 @@ export const adminTopKeysCall = async (accessToken: String) => { } }; -export const adminTopModelsCall = async (accessToken: String) => { +export const adminTopEndUsersCall = async (accessToken: String) => { try { let url = proxyBaseUrl - ? `${proxyBaseUrl}/global/spend/models?limit=5` - : `/global/spend/models?limit=5`; + ? `${proxyBaseUrl}/global/spend/end_users` + : `/global/spend/end_users`; - message.info("Making spend models request"); + message.info("Making top end users request"); const response = await fetch(url, { method: "GET", headers: { @@ -426,7 +426,37 @@ export const adminTopModelsCall = async (accessToken: String) => { const data = await response.json(); console.log(data); - message.success("Spend Logs received"); + message.success("Top End users received"); + return data; + } catch (error) { + console.error("Failed to create key:", error); + throw error; + } +}; + +export const adminTopModelsCall = async (accessToken: String) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/global/spend/models?limit=5` + : `/global/spend/models?limit=5`; + + message.info("Making top models request"); + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + const errorData = await response.text(); + message.error(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log(data); + message.success("Top Models received"); return data; } catch (error) { console.error("Failed to create key:", error); diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 98cddae9825..763176a308f 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -8,6 +8,9 @@ import { TableRow, TableCell, TableBody, + Tab, + TabGroup, + TabList, Metric, Grid, } from "@tremor/react"; @@ -29,8 +32,9 @@ const ViewUserDashboard: React.FC = ({ userRole, userID, }) => { - const [userData, setuserData] = useState(null); - const [pendingRequests, setPendingRequests] = useState([]); + const [userData, setUserData] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const defaultPageSize = 25; useEffect(() => { if (!accessToken || !token || !userRole || !userID) { @@ -46,7 +50,7 @@ const ViewUserDashboard: React.FC = ({ true ); console.log("user data response:", userDataResponse); - setuserData(userDataResponse); + setUserData(userDataResponse); } catch (error) { console.error("There was an error fetching the model data", error); } @@ -65,11 +69,49 @@ const ViewUserDashboard: React.FC = ({ return
Loading...
; } + function renderPagination() { + if (!userData) return null; + + const totalPages = Math.ceil(userData.length / defaultPageSize); + const startItem = (currentPage - 1) * defaultPageSize + 1; + const endItem = Math.min(currentPage * defaultPageSize, userData.length); + + return ( +
+
+ Showing {startItem} – {endItem} of {userData.length} +
+
+ + +
+
+ ); + } + return (
+ + + Key Owners + End-Users + + @@ -93,34 +135,25 @@ const ViewUserDashboard: React.FC = ({ {userData.map((user: any) => ( + {user.user_id} - {user.user_id} + {user.user_role ? user.user_role : "app_owner"} - - {user.user_role ? user.user_role : "app_user"} - + {user.models && user.models.length > 0 + ? user.models + : "All Models"} + {user.spend ? user.spend : 0} - - {user.models && user.models.length > 0 - ? user.models - : "All Models"} - - - - {user.spend ? user.spend : 0} - - - - {user.max_budget ? user.max_budget : "Unlimited"} - + {user.max_budget ? user.max_budget : "Unlimited"} ))}
+ {renderPagination()}
); From 7cfcaa9556e4f5f26051e29216f61190015a1541 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 29 Feb 2024 19:11:50 -0800 Subject: [PATCH 3/3] fix: return end user top spend --- litellm/proxy/utils.py | 2 +- .../src/components/view_users.tsx | 122 ++++++++++++------ 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index c7213d465cd..04a692e8f07 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -601,7 +601,7 @@ class PrismaClient: except Exception as e: sql_query = """ CREATE VIEW "Last30dTopEndUsersSpend" AS - SELECT end_user, SUM(spend) AS total_spend + SELECT end_user, COUNT(*) AS total_events, SUM(spend) AS total_spend FROM "LiteLLM_SpendLogs" WHERE end_user <> '' AND end_user <> user AND "startTime" >= CURRENT_DATE - INTERVAL '30 days' diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index 763176a308f..00329e206ee 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -5,16 +5,19 @@ import { Subtitle, Table, TableHead, + TableHeaderCell, TableRow, TableCell, TableBody, Tab, TabGroup, TabList, + TabPanels, Metric, Grid, + TabPanel, } from "@tremor/react"; -import { userInfoCall } from "./networking"; +import { userInfoCall, adminTopEndUsersCall } from "./networking"; import { Badge, BadgeDelta, Button } from "@tremor/react"; import RequestAccess from "./request_model_access"; import CreateUser from "./create_user_button"; @@ -33,6 +36,7 @@ const ViewUserDashboard: React.FC = ({ userID, }) => { const [userData, setUserData] = useState(null); + const [endUsers, setEndUsers] = useState(null); const [currentPage, setCurrentPage] = useState(1); const defaultPageSize = 25; @@ -56,9 +60,26 @@ const ViewUserDashboard: React.FC = ({ } }; - if (accessToken && token && userRole && userID) { + if (accessToken && token && userRole && userID && !userData) { fetchData(); } + + const fetchEndUserSpend = async () => { + try { + const topEndUsers = await adminTopEndUsersCall(accessToken); + 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]); if (!userData) { @@ -105,53 +126,68 @@ const ViewUserDashboard: React.FC = ({
- + Key Owners End-Users + + + + + + User ID + User Role + User Models + User Spend ($ USD) + User Max Budget ($ USD) + + + + {userData.map((user: any) => ( + + {user.user_id} + + {user.user_role ? user.user_role : "app_owner"} + + + {user.models && user.models.length > 0 + ? user.models + : "All Models"} + + {user.spend ? user.spend : 0} + + {user.max_budget ? user.max_budget : "Unlimited"} + + + ))} + +
+
+ + + + + End User + Spend + Total Events + + + + + {endUsers?.map((user: any, index: number) => ( + + {user.end_user} + {user.total_spend} + {user.total_events} + + ))} + +
+
+
- - - - - User ID - - - User Role - - - User Models - - - User Spend ($ USD) - - - User Max Budget ($ USD) - - - - - {userData.map((user: any) => ( - - {user.user_id} - - {user.user_role ? user.user_role : "app_owner"} - - - {user.models && user.models.length > 0 - ? user.models - : "All Models"} - - {user.spend ? user.spend : 0} - - {user.max_budget ? user.max_budget : "Unlimited"} - - - ))} - -
{renderPagination()}