Merge pull request #3835 from BerriAI/litellm_view_spend_by_provider_ui

[Feat] Admin UI - View Spend Per Provider
This commit is contained in:
Ishaan Jaff 2024-05-25 09:46:29 -07:00 committed by GitHub
commit e187cbdd51
3 changed files with 194 additions and 1 deletions

View file

@ -5883,6 +5883,112 @@ async def view_spend_tags(
) )
@router.get(
"/global/spend/provider",
tags=["Budget & Spend Tracking"],
dependencies=[Depends(user_api_key_auth)],
include_in_schema=False,
responses={
200: {"model": List[LiteLLM_SpendLogs]},
},
)
async def get_global_spend_provider(
start_date: Optional[str] = fastapi.Query(
default=None,
description="Time from which to start viewing spend",
),
end_date: Optional[str] = fastapi.Query(
default=None,
description="Time till which to view spend",
),
):
"""
Get breakdown of spend per provider
[
{
"provider": "Azure OpenAI",
"spend": 20
},
{
"provider": "OpenAI",
"spend": 10
},
{
"provider": "VertexAI",
"spend": 30
}
]
"""
from collections import defaultdict
if start_date is None or end_date is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": "Please provide start_date and end_date"},
)
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d")
end_date_obj = datetime.strptime(end_date, "%Y-%m-%d")
global prisma_client, llm_router
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
model_id,
SUM(spend) AS spend
FROM "LiteLLM_SpendLogs"
WHERE "startTime" BETWEEN $1::date AND $2::date AND length(model_id) > 0
GROUP BY model_id
"""
db_response = await prisma_client.db.query_raw(
sql_query, start_date_obj, end_date_obj
)
if db_response is None:
return []
###################################
# Convert model_id -> to Provider #
###################################
# we use the in memory router for this
ui_response = []
provider_spend_mapping: defaultdict = defaultdict(int)
for row in db_response:
_model_id = row["model_id"]
_provider = "Unknown"
if llm_router is not None:
_deployment = llm_router.get_deployment(model_id=_model_id)
if _deployment is not None:
try:
_, _provider, _, _ = litellm.get_llm_provider(
model=_deployment.litellm_params.model,
custom_llm_provider=_deployment.litellm_params.custom_llm_provider,
api_base=_deployment.litellm_params.api_base,
litellm_params=_deployment.litellm_params,
)
provider_spend_mapping[_provider] += row["spend"]
except:
pass
for provider, spend in provider_spend_mapping.items():
ui_response.append({"provider": provider, "spend": spend})
return ui_response
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"error": str(e)},
)
@router.get( @router.get(
"/global/spend/report", "/global/spend/report",
tags=["Budget & Spend Tracking"], tags=["Budget & Spend Tracking"],

View file

@ -994,6 +994,46 @@ export const adminTopEndUsersCall = async (
throw error; throw error;
} }
}; };
export const adminspendByProvider = async (accessToken: String, keyToken: String | null, startTime: String | undefined, endTime: String | undefined) => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/global/spend/provider` : `/global/spend/provider`;
if (startTime && endTime) {
url += `?start_date=${startTime}&end_date=${endTime}`;
}
if (keyToken) {
url += `&api_key=${keyToken}`;
}
const requestOptions: {
method: string;
headers: {
Authorization: string;
};
} = {
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
const response = await fetch(url, requestOptions);
if (!response.ok) {
const errorData = await response.text();
message.error(errorData, 10);
throw new Error("Network response was not ok");
}
const data = await response.json();
console.log(data);
return data;
} catch (error) {
console.error("Failed to fetch spend data:", error);
throw error;
}
};
export const adminTopModelsCall = async (accessToken: String) => { export const adminTopModelsCall = async (accessToken: String) => {
try { try {

View file

@ -3,7 +3,7 @@ import { BarChart, BarList, Card, Title, Table, TableHead, TableHeaderCell, Tabl
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import ViewUserSpend from "./view_user_spend"; import ViewUserSpend from "./view_user_spend";
import { Grid, Col, Text, LineChart, TabPanel, TabPanels, TabGroup, TabList, Tab, Select, SelectItem, DateRangePicker, DateRangePickerValue } from "@tremor/react"; import { Grid, Col, Text, LineChart, TabPanel, TabPanels, TabGroup, TabList, Tab, Select, SelectItem, DateRangePicker, DateRangePickerValue, DonutChart} from "@tremor/react";
import { import {
userSpendLogsCall, userSpendLogsCall,
keyInfoCall, keyInfoCall,
@ -16,6 +16,7 @@ import {
modelMetricsCall, modelMetricsCall,
modelAvailableCall, modelAvailableCall,
modelInfoCall, modelInfoCall,
adminspendByProvider,
} from "./networking"; } from "./networking";
import { start } from "repl"; import { start } from "repl";
@ -115,6 +116,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
const [topTagsData, setTopTagsData] = useState<any[]>([]); const [topTagsData, setTopTagsData] = useState<any[]>([]);
const [uniqueTeamIds, setUniqueTeamIds] = useState<any[]>([]); const [uniqueTeamIds, setUniqueTeamIds] = useState<any[]>([]);
const [totalSpendPerTeam, setTotalSpendPerTeam] = useState<any[]>([]); const [totalSpendPerTeam, setTotalSpendPerTeam] = useState<any[]>([]);
const [spendByProvider, setSpendByProvider] = useState<any[]>([]);
const [selectedKeyID, setSelectedKeyID] = useState<string | null>(""); const [selectedKeyID, setSelectedKeyID] = useState<string | null>("");
const [dateValue, setDateValue] = useState<DateRangePickerValue>({ const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
@ -210,6 +212,12 @@ const UsagePage: React.FC<UsagePageProps> = ({
if (userRole == "Admin" || userRole == "Admin Viewer") { if (userRole == "Admin" || userRole == "Admin Viewer") {
const overall_spend = await adminSpendLogsCall(accessToken); const overall_spend = await adminSpendLogsCall(accessToken);
setKeySpendData(overall_spend); setKeySpendData(overall_spend);
const provider_spend = await adminspendByProvider(accessToken, token, startTime, endTime);
console.log("provider_spend", provider_spend);
setSpendByProvider(provider_spend);
const top_keys = await adminTopKeysCall(accessToken); const top_keys = await adminTopKeysCall(accessToken);
const filtered_keys = top_keys.map((k: any) => ({ const filtered_keys = top_keys.map((k: any) => ({
key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring( key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring(
@ -373,6 +381,44 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Col numColSpan={1}> <Col numColSpan={1}>
</Col> </Col>
<Col numColSpan={2}>
<Card className="mb-2">
<Title> Spend by Provider</Title>
<Grid numItems={2}>
<Col numColSpan={1}>
<DonutChart
className="mt-4 h-40"
variant="pie"
data={spendByProvider}
index="provider"
category="spend"
/>
</Col>
<Col numColSpan={1}>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{spendByProvider.map((provider) => (
<TableRow key={provider.provider}>
<TableCell>{provider.provider}</TableCell>
<TableCell>
{parseFloat(provider.spend.toFixed(2)) < 0.00001
? "less than 0.00"
: provider.spend.toFixed(2)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Col>
</Grid>
</Card>
</Col>
</Grid> </Grid>
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
@ -382,6 +428,7 @@ const UsagePage: React.FC<UsagePageProps> = ({
<Title>Total Spend Per Team</Title> <Title>Total Spend Per Team</Title>
<BarList <BarList
data={totalSpendPerTeam} data={totalSpendPerTeam}
/> />
</Card> </Card>
<Card> <Card>