diff --git a/enterprise/utils.py b/enterprise/utils.py index 90b14314c2..b8f660927b 100644 --- a/enterprise/utils.py +++ b/enterprise/utils.py @@ -1,5 +1,7 @@ # Enterprise Proxy Util Endpoints +from typing import Optional, List from litellm._logging import verbose_logger +from litellm.proxy.proxy_server import PrismaClient, HTTPException import collections from datetime import datetime @@ -19,27 +21,76 @@ async def get_spend_by_tags(start_date=None, end_date=None, prisma_client=None): return response -async def ui_get_spend_by_tags(start_date: str, end_date: str, prisma_client): - - sql_query = """ - SELECT - jsonb_array_elements_text(request_tags) AS individual_request_tag, - DATE(s."startTime") AS spend_date, - COUNT(*) AS log_count, - SUM(spend) AS total_spend - FROM "LiteLLM_SpendLogs" s - WHERE - DATE(s."startTime") >= $1::date - AND DATE(s."startTime") <= $2::date - GROUP BY individual_request_tag, spend_date - ORDER BY spend_date - LIMIT 100; +async def ui_get_spend_by_tags( + start_date: str, + end_date: str, + prisma_client: Optional[PrismaClient] = None, + tags_str: Optional[str] = None, +): """ - response = await prisma_client.db.query_raw( - sql_query, - start_date, - end_date, - ) + Should cover 2 cases: + 1. When user is getting spend for all_tags. "all_tags" in tags_list + 2. When user is getting spend for specific tags. + """ + + # tags_str is a list of strings csv of tags + # tags_str = tag1,tag2,tag3 + # convert to list if it's not None + tags_list: Optional[List[str]] = None + if tags_str is not None and len(tags_str) > 0: + tags_list = tags_str.split(",") + + if prisma_client is None: + raise HTTPException(status_code=500, detail={"error": "No db connected"}) + + response = None + if tags_list is None or (isinstance(tags_list, list) and "all-tags" in tags_list): + # Get spend for all tags + sql_query = """ + SELECT + jsonb_array_elements_text(request_tags) AS individual_request_tag, + DATE(s."startTime") AS spend_date, + COUNT(*) AS log_count, + SUM(spend) AS total_spend + FROM "LiteLLM_SpendLogs" s + WHERE + DATE(s."startTime") >= $1::date + AND DATE(s."startTime") <= $2::date + GROUP BY individual_request_tag, spend_date + ORDER BY total_spend DESC; + """ + response = await prisma_client.db.query_raw( + sql_query, + start_date, + end_date, + ) + else: + # filter by tags list + sql_query = """ + SELECT + individual_request_tag, + COUNT(*) AS log_count, + SUM(spend) AS total_spend + FROM ( + SELECT + jsonb_array_elements_text(request_tags) AS individual_request_tag, + DATE(s."startTime") AS spend_date, + spend + FROM "LiteLLM_SpendLogs" s + WHERE + DATE(s."startTime") >= $1::date + AND DATE(s."startTime") <= $2::date + ) AS subquery + WHERE individual_request_tag = ANY($3::text[]) + GROUP BY individual_request_tag + ORDER BY total_spend DESC; + """ + response = await prisma_client.db.query_raw( + sql_query, + start_date, + end_date, + tags_list, + ) # print("tags - spend") # print(response) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9ba05e5ff5..1edc85ef3d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -8229,6 +8229,56 @@ async def get_global_spend_report( ) +@router.get( + "/global/spend/all_tag_names", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], + include_in_schema=False, + responses={ + 200: {"model": List[LiteLLM_SpendLogs]}, + }, +) +async def global_get_all_tag_names(): + try: + if prisma_client is None: + raise Exception( + f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" + ) + + sql_query = """ + SELECT DISTINCT + jsonb_array_elements_text(request_tags) AS individual_request_tag + FROM "LiteLLM_SpendLogs"; + """ + + db_response = await prisma_client.db.query_raw(sql_query) + if db_response is None: + return [] + + _tag_names = [] + for row in db_response: + _tag_names.append(row.get("individual_request_tag")) + + return {"tag_names": _tag_names} + + except Exception as e: + if isinstance(e, HTTPException): + raise ProxyException( + message=getattr(e, "detail", f"/spend/all_tag_names Error({str(e)})"), + type="internal_error", + param=getattr(e, "param", "None"), + code=getattr(e, "status_code", status.HTTP_500_INTERNAL_SERVER_ERROR), + ) + elif isinstance(e, ProxyException): + raise e + raise ProxyException( + message="/spend/all_tag_names Error" + str(e), + type="internal_error", + param=getattr(e, "param", "None"), + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + @router.get( "/global/spend/tags", tags=["Budget & Spend Tracking"], @@ -8247,19 +8297,23 @@ async def global_view_spend_tags( default=None, description="Time till which to view key spend", ), + tags: Optional[str] = fastapi.Query( + default=None, + description="comman separated tags to filter on", + ), ): """ LiteLLM Enterprise - View Spend Per Request Tag. Used by LiteLLM UI Example Request: ``` - curl -X GET "http://0.0.0.0:8000/spend/tags" \ + curl -X GET "http://0.0.0.0:4000/spend/tags" \ -H "Authorization: Bearer sk-1234" ``` Spend with Start Date and End Date ``` - curl -X GET "http://0.0.0.0:8000/spend/tags?start_date=2022-01-01&end_date=2022-02-01" \ + curl -X GET "http://0.0.0.0:4000/spend/tags?start_date=2022-01-01&end_date=2022-02-01" \ -H "Authorization: Bearer sk-1234" ``` """ @@ -8281,7 +8335,10 @@ async def global_view_spend_tags( code=status.HTTP_400_BAD_REQUEST, ) response = await ui_get_spend_by_tags( - start_date=start_date, end_date=end_date, prisma_client=prisma_client + start_date=start_date, + end_date=end_date, + tags_str=tags, + prisma_client=prisma_client, ) return response diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 7a57a0a960..b7660ee0e9 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -979,7 +979,8 @@ export const teamSpendLogsCall = async (accessToken: String) => { export const tagsSpendLogsCall = async ( accessToken: String, startTime: String | undefined, - endTime: String | undefined + endTime: String | undefined, + tags: String[] | undefined ) => { try { let url = proxyBaseUrl @@ -990,6 +991,11 @@ export const tagsSpendLogsCall = async ( url = `${url}?start_date=${startTime}&end_date=${endTime}`; } + // if tags, convert the list to a comma separated string + if (tags) { + url += `${url}&tags=${tags.join(",")}`; + } + console.log("in tagsSpendLogsCall:", url); const response = await fetch(`${url}`, { method: "GET", @@ -1012,6 +1018,38 @@ export const tagsSpendLogsCall = async ( } }; +export const allTagNamesCall = async ( + accessToken: String, +) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/global/spend/all_tag_names` + : `/global/spend/all_tag_names`; + + + console.log("in global/spend/all_tag_names call", url); + const response = await fetch(`${url}`, { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + if (!response.ok) { + const errorData = await response.text(); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log(data); + return data; + } catch (error) { + console.error("Failed to create key:", error); + throw error; + } +}; + + export const userSpendLogsCall = async ( accessToken: String, token: String, diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index ea50422c22..732df45245 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -12,7 +12,14 @@ import { AreaChart, Callout, Button, + MultiSelect, + MultiSelectItem, } from "@tremor/react"; + +import { + Select as Select2 +} from "antd"; + import { userSpendLogsCall, keyInfoCall, @@ -22,6 +29,7 @@ import { adminTopEndUsersCall, teamSpendLogsCall, tagsSpendLogsCall, + allTagNamesCall, modelMetricsCall, modelAvailableCall, modelInfoCall, @@ -134,12 +142,14 @@ const UsagePage: React.FC = ({ const [topUsers, setTopUsers] = useState([]); const [teamSpendData, setTeamSpendData] = useState([]); const [topTagsData, setTopTagsData] = useState([]); + const [allTagNames, setAllTagNames] = useState([]); const [uniqueTeamIds, setUniqueTeamIds] = useState([]); const [totalSpendPerTeam, setTotalSpendPerTeam] = useState([]); const [spendByProvider, setSpendByProvider] = useState([]); const [globalActivity, setGlobalActivity] = useState({} as GlobalActivityData); const [globalActivityPerModel, setGlobalActivityPerModel] = useState([]); const [selectedKeyID, setSelectedKeyID] = useState(""); + const [selectedTags, setSelectedTags] = useState(["all-tags"]); const [dateValue, setDateValue] = useState({ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), to: new Date(), @@ -171,6 +181,10 @@ const UsagePage: React.FC = ({ return formatter.format(number); } + + useEffect(() => { + updateTagSpendData(dateValue.from, dateValue.to); + }, [dateValue, selectedTags]); const updateEndUserData = async (startTime: Date | undefined, endTime: Date | undefined, uiSelectedKey: string | null) => { @@ -208,12 +222,15 @@ const UsagePage: React.FC = ({ // startTime put it to the first hour of the selected date startTime.setHours(0, 0, 0, 0); - let top_tags = await tagsSpendLogsCall(accessToken, startTime.toISOString(), endTime.toISOString()); + let top_tags = await tagsSpendLogsCall( + accessToken, + startTime.toISOString(), + endTime.toISOString(), + selectedTags.length === 0 ? undefined : selectedTags + ); setTopTagsData(top_tags.spend_per_tag); console.log("Tag spend data updated successfully"); - - } function formatDate(date: Date) { @@ -289,10 +306,15 @@ const UsagePage: React.FC = ({ setTotalSpendPerTeam(total_spend_per_team); + // all_tag_names -> used for dropdown + const all_tag_names = await allTagNamesCall(accessToken); + setAllTagNames(all_tag_names.tag_names); + //get top tags - const top_tags = await tagsSpendLogsCall(accessToken, dateValue.from?.toISOString(), dateValue.to?.toISOString()); + const top_tags = await tagsSpendLogsCall(accessToken, dateValue.from?.toISOString(), dateValue.to?.toISOString(), undefined); setTopTagsData(top_tags.spend_per_tag); + // get spend per end-user let spend_user_call = await adminTopEndUsersCall(accessToken, null, undefined, undefined); setTopUsers(spend_user_call); @@ -354,18 +376,12 @@ const UsagePage: React.FC = ({ return (
- + All Up Team Based Usage - End User Usage + Customer Usage Tag Based Usage @@ -379,6 +395,13 @@ const UsagePage: React.FC = ({ + Monthly Spend @@ -656,7 +679,7 @@ const UsagePage: React.FC = ({ -

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

+

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

Select Time Range @@ -717,7 +740,7 @@ const UsagePage: React.FC = ({ - End User + Customer Spend Total Events @@ -738,8 +761,8 @@ const UsagePage: React.FC = ({ - - + + = ({ }} /> + + + + { + premiumUser ? ( +
+ setSelectedTags(value as string[])} + > + setSelectedTags(["all-tags"])} + > + All Tags + + {allTagNames && + allTagNames + .filter((tag) => tag !== "all-tags") + .map((tag: any, index: number) => { + return ( + + {tag} + + ); + })} + + +
+ + ) : ( +
+ + setSelectedTags(value as string[])} + > + setSelectedTags(["all-tags"])} + > + All Tags + + {allTagNames && + allTagNames + .filter((tag) => tag !== "all-tags") + .map((tag: any, index: number) => { + return ( + + ✨ {tag} (Enterpise only Feature) + + ); + })} + + + + + +
+ ) + } + + + + + + + + + Spend Per Tag - Get Started Tracking cost per tag here + Get Started Tracking cost per tag here = ({ userID, userRole, accessT ${roundedSpend}

-
+ {/*
Team Models @@ -144,7 +144,7 @@ const ViewUserSpend: React.FC = ({ userID, userRole, accessT -
+
*/} ); }