diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index d558107c3f..07c1f69149 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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( "/global/spend/report", tags=["Budget & Spend Tracking"], diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 7540059c59..4c21e7e911 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -994,6 +994,46 @@ export const adminTopEndUsersCall = async ( 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) => { try { diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index 682c664c4f..b0fdafd504 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -3,7 +3,7 @@ import { BarChart, BarList, Card, Title, Table, TableHead, TableHeaderCell, Tabl import React, { useState, useEffect } from "react"; 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 { userSpendLogsCall, keyInfoCall, @@ -16,6 +16,7 @@ import { modelMetricsCall, modelAvailableCall, modelInfoCall, + adminspendByProvider, } from "./networking"; import { start } from "repl"; @@ -115,6 +116,7 @@ const UsagePage: React.FC = ({ const [topTagsData, setTopTagsData] = useState([]); const [uniqueTeamIds, setUniqueTeamIds] = useState([]); const [totalSpendPerTeam, setTotalSpendPerTeam] = useState([]); + const [spendByProvider, setSpendByProvider] = useState([]); const [selectedKeyID, setSelectedKeyID] = useState(""); const [dateValue, setDateValue] = useState({ from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), @@ -210,6 +212,12 @@ const UsagePage: React.FC = ({ if (userRole == "Admin" || userRole == "Admin Viewer") { const overall_spend = await adminSpendLogsCall(accessToken); 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 filtered_keys = top_keys.map((k: any) => ({ key: (k["key_alias"] || k["key_name"] || k["api_key"]).substring( @@ -373,6 +381,44 @@ const UsagePage: React.FC = ({ + + + ✨ Spend by Provider + + + + + + + + + Provider + Spend + + + + {spendByProvider.map((provider) => ( + + {provider.provider} + + {parseFloat(provider.spend.toFixed(2)) < 0.00001 + ? "less than 0.00" + : provider.spend.toFixed(2)} + + + ))} + +
+ +
+
+ @@ -382,6 +428,7 @@ const UsagePage: React.FC = ({ Total Spend Per Team