Merge pull request #3994 from BerriAI/litellm_ui_select_spend_by_tag

[Feat] Admin UI - Multi-Select Tags, Viewing spend by tags
This commit is contained in:
Ishaan Jaff
2024-06-03 16:44:02 -07:00
committed by GitHub
5 changed files with 292 additions and 43 deletions
+71 -20
View File
@@ -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)
+60 -3
View File
@@ -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
@@ -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,
+120 -17
View File
@@ -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<UsagePageProps> = ({
const [topUsers, setTopUsers] = useState<any[]>([]);
const [teamSpendData, setTeamSpendData] = useState<any[]>([]);
const [topTagsData, setTopTagsData] = useState<any[]>([]);
const [allTagNames, setAllTagNames] = useState<string[]>([]);
const [uniqueTeamIds, setUniqueTeamIds] = useState<any[]>([]);
const [totalSpendPerTeam, setTotalSpendPerTeam] = useState<any[]>([]);
const [spendByProvider, setSpendByProvider] = useState<any[]>([]);
const [globalActivity, setGlobalActivity] = useState<GlobalActivityData>({} as GlobalActivityData);
const [globalActivityPerModel, setGlobalActivityPerModel] = useState<any[]>([]);
const [selectedKeyID, setSelectedKeyID] = useState<string | null>("");
const [selectedTags, setSelectedTags] = useState<string[]>(["all-tags"]);
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
to: new Date(),
@@ -171,6 +181,10 @@ const UsagePage: React.FC<UsagePageProps> = ({
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<UsagePageProps> = ({
// 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<UsagePageProps> = ({
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<UsagePageProps> = ({
return (
<div style={{ width: "100%" }} className="p-8">
<ViewUserSpend
userID={userID}
userRole={userRole}
accessToken={accessToken}
userSpend={null}
selectedTeam={null}
/>
<TabGroup>
<TabList className="mt-2">
<Tab>All Up</Tab>
<Tab>Team Based Usage</Tab>
<Tab>End User Usage</Tab>
<Tab>Customer Usage</Tab>
<Tab>Tag Based Usage</Tab>
</TabList>
<TabPanels>
@@ -379,6 +395,13 @@ const UsagePage: React.FC<UsagePageProps> = ({
<TabPanels>
<TabPanel>
<Grid numItems={2} className="gap-2 h-[100vh] w-full">
<ViewUserSpend
userID={userID}
userRole={userRole}
accessToken={accessToken}
userSpend={null}
selectedTeam={null}
/>
<Col numColSpan={2}>
<Card>
<Title>Monthly Spend</Title>
@@ -656,7 +679,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
</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>
<p className="mb-2 text-gray-500 italic text-[12px]">Customers 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>
@@ -717,7 +740,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Table className="max-h-[70vh] min-h-[500px]">
<TableHead>
<TableRow>
<TableHeaderCell>End User</TableHeaderCell>
<TableHeaderCell>Customer</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell>Total Events</TableHeaderCell>
</TableRow>
@@ -738,8 +761,8 @@ const UsagePage: React.FC<UsagePageProps> = ({
</TabPanel>
<TabPanel>
<Grid numItems={2} className="gap-2 h-[75vh] w-full mb-4">
<Col numColSpan={2}>
<Grid numItems={2}>
<Col numColSpan={1}>
<DateRangePicker
className="mb-4"
enableSelect={true}
@@ -750,9 +773,89 @@ const UsagePage: React.FC<UsagePageProps> = ({
}}
/>
</Col>
<Col>
{
premiumUser ? (
<div>
<MultiSelect
value={selectedTags}
onValueChange={(value) => setSelectedTags(value as string[])}
>
<MultiSelectItem
key={"all-tags"}
value={"all-tags"}
onClick={() => setSelectedTags(["all-tags"])}
>
All Tags
</MultiSelectItem>
{allTagNames &&
allTagNames
.filter((tag) => tag !== "all-tags")
.map((tag: any, index: number) => {
return (
<MultiSelectItem
key={tag}
value={String(tag)}
>
{tag}
</MultiSelectItem>
);
})}
</MultiSelect>
</div>
) : (
<div>
<MultiSelect
value={selectedTags}
onValueChange={(value) => setSelectedTags(value as string[])}
>
<MultiSelectItem
key={"all-tags"}
value={"all-tags"}
onClick={() => setSelectedTags(["all-tags"])}
>
All Tags
</MultiSelectItem>
{allTagNames &&
allTagNames
.filter((tag) => tag !== "all-tags")
.map((tag: any, index: number) => {
return (
<SelectItem
key={tag}
value={String(tag)}
// @ts-ignore
disabled={true}
>
{tag} (Enterpise only Feature)
</SelectItem>
);
})}
</MultiSelect>
</div>
)
}
</Col>
</Grid>
<Grid numItems={2} className="gap-2 h-[75vh] w-full mb-4">
<Col numColSpan={2}>
<Card>
<Title>Spend Per Tag</Title>
<Text>Get Started Tracking cost per tag <a className="text-blue-500" href="https://docs.litellm.ai/docs/proxy/enterprise#tracking-spend-for-custom-tags" target="_blank">here</a></Text>
<Text>Get Started Tracking cost per tag <a className="text-blue-500" href="https://docs.litellm.ai/docs/proxy/cost_tracking" target="_blank">here</a></Text>
<BarChart
className="h-72"
data={topTagsData}
@@ -131,7 +131,7 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userID, userRole, accessT
${roundedSpend}
</p>
</div>
<div className="ml-auto">
{/* <div className="ml-auto">
<Accordion>
<AccordionHeader><Text>Team Models</Text></AccordionHeader>
<AccordionBody className="absolute right-0 z-10 bg-white p-2 shadow-lg max-w-xs">
@@ -144,7 +144,7 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userID, userRole, accessT
</List>
</AccordionBody>
</Accordion>
</div>
</div> */}
</div>
);
}