mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-10 22:24:51 +00:00
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:
+71
-20
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user