mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
Merge pull request #3835 from BerriAI/litellm_view_spend_by_provider_ui
[Feat] Admin UI - View Spend Per Provider
This commit is contained in:
commit
e187cbdd51
3 changed files with 194 additions and 1 deletions
|
@ -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"],
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue