diff --git a/litellm/proxy/analytics_endpoints/analytics_endpoints.py b/litellm/proxy/analytics_endpoints/analytics_endpoints.py new file mode 100644 index 00000000000..54ac8bc48fb --- /dev/null +++ b/litellm/proxy/analytics_endpoints/analytics_endpoints.py @@ -0,0 +1,109 @@ +#### Analytics Endpoints ##### +from datetime import datetime, timedelta, timezone +from typing import List, Optional + +import fastapi +from fastapi import APIRouter, Depends, Header, HTTPException, Request, status + +import litellm +from litellm._logging import verbose_proxy_logger +from litellm.proxy._types import * +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + +router = APIRouter() + + +@router.get( + "/global/activity/cache_hits", + tags=["Budget & Spend Tracking"], + dependencies=[Depends(user_api_key_auth)], + responses={ + 200: {"model": List[LiteLLM_SpendLogs]}, + }, + include_in_schema=False, +) +async def get_global_activity( + 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 number of cache hits, vs misses + + { + "daily_data": [ + const chartdata = [ + { + date: 'Jan 22', + cache_hits: 10, + llm_api_calls: 2000 + }, + { + date: 'Jan 23', + cache_hits: 10, + llm_api_calls: 12 + }, + ], + "sum_cache_hits": 20, + "sum_llm_api_calls": 2012 + } + """ + 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") + + from litellm.proxy.proxy_server import llm_router, prisma_client + + try: + if prisma_client is None: + raise ValueError( + f"Database not connected. Connect a database to your proxy - https://docs.litellm.ai/docs/simple_proxy#managing-auth---virtual-keys" + ) + + sql_query = """ + SELECT + CASE + WHEN vt."key_alias" IS NOT NULL THEN vt."key_alias" + ELSE 'Unnamed Key' + END AS api_key, + sl."call_type", + sl."model", + COUNT(*) AS total_rows, + SUM(CASE WHEN sl."cache_hit" = 'True' THEN 1 ELSE 0 END) AS cache_hit_true_rows, + SUM(CASE WHEN sl."cache_hit" = 'True' THEN sl."completion_tokens" ELSE 0 END) AS cached_completion_tokens, + SUM(CASE WHEN sl."cache_hit" != 'True' THEN sl."completion_tokens" ELSE 0 END) AS generated_completion_tokens + FROM "LiteLLM_SpendLogs" sl + LEFT JOIN "LiteLLM_VerificationToken" vt ON sl."api_key" = vt."token" + WHERE + sl."startTime" BETWEEN $1::date AND $2::date + interval '1 day' + GROUP BY + vt."key_alias", + sl."call_type", + sl."model" + """ + db_response = await prisma_client.db.query_raw( + sql_query, start_date_obj, end_date_obj + ) + + if db_response is None: + return [] + + return db_response + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={"error": str(e)}, + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6d19766ad03..3befed9eaa3 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -116,6 +116,9 @@ from litellm.exceptions import RejectedRequestError from litellm.integrations.slack_alerting import SlackAlerting, SlackAlertingArgs from litellm.llms.custom_httpx.httpx_handler import HTTPHandler from litellm.proxy._types import * +from litellm.proxy.analytics_endpoints.analytics_endpoints import ( + router as analytics_router, +) from litellm.proxy.auth.auth_checks import ( allowed_routes_check, common_checks, @@ -9139,3 +9142,4 @@ app.include_router(internal_user_router) app.include_router(team_router) app.include_router(spend_management_router) app.include_router(caching_router) +app.include_router(analytics_router) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 3b70ef0aca6..ed9c98df7c6 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -15,6 +15,7 @@ import APIRef from "@/components/api_ref"; import ChatUI from "@/components/chat_ui"; import Sidebar from "../components/leftnav"; import Usage from "../components/usage"; +import CacheDashboard from "@/components/cache_dashboard"; import { jwtDecode } from "jwt-decode"; import { Typography } from "antd"; @@ -221,6 +222,14 @@ const CreateKeyPage = () => { publicPage={false} premiumUser={premiumUser} /> + ) : page == "caching" ? ( + ) : ( { + if (!date) return undefined; + return date.toISOString().split('T')[0]; + }; + + +function valueFormatterNumbers(number: number) { +const formatter = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 0, + notation: 'compact', + compactDisplay: 'short', +}); + +return formatter.format(number); +} + +interface CachePageProps { + accessToken: string | null; + token: string | null; + userRole: string | null; + userID: string | null; + premiumUser: boolean; +} + +interface cacheDataItem { + api_key: string; + model: string; + cache_hit_true_rows: number; + cached_completion_tokens: number; + total_rows: number; + generated_completion_tokens: number; + call_type: string; + + // Add other properties as needed + } + + +interface uiData { + "name": string; + "LLM API requests": number; + "Cache hit": number; + "Cached Completion Tokens": number; + "Generated Completion Tokens": number; + +} + + + +const CacheDashboard: React.FC = ({ + accessToken, + token, + userRole, + userID, + premiumUser, +}) => { + const [filteredData, setFilteredData] = useState([]); + const [selectedApiKeys, setSelectedApiKeys] = useState([]); + const [selectedModels, setSelectedModels] = useState([]); + const [data, setData] = useState([]); + const [cachedResponses, setCachedResponses] = useState("0"); + const [cachedTokens, setCachedTokens] = useState("0"); + const [cacheHitRatio, setCacheHitRatio] = useState("0"); + + const [dateValue, setDateValue] = useState({ + from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + to: new Date(), + }); + + + useEffect(() => { + if (!accessToken || !dateValue) { + return; + } + const fetchData = async () => { + const response = await adminGlobalCacheActivity(accessToken, formatDateWithoutTZ(dateValue.from), formatDateWithoutTZ(dateValue.to)); + setData(response); + }; + fetchData(); + }, [accessToken]); + + const uniqueApiKeys = Array.from(new Set(data.map((item) => item?.api_key ?? ""))); + const uniqueModels = Array.from(new Set(data.map((item) => item?.model ?? ""))); + const uniqueCallTypes = Array.from(new Set(data.map((item) => item?.call_type ?? ""))); + + + const updateCachingData = async (startTime: Date | undefined, endTime: Date | undefined) => { + if (!startTime || !endTime || !accessToken) { + return; + } + + // the endTime put it to the last hour of the selected date + endTime.setHours(23, 59, 59, 999); + + // startTime put it to the first hour of the selected date + startTime.setHours(0, 0, 0, 0); + + let new_cache_data = await adminGlobalCacheActivity( + accessToken, + formatDateWithoutTZ(startTime), + formatDateWithoutTZ(endTime) + ) + + setData(new_cache_data); + + } + + useEffect(() => { + console.log("DATA IN CACHE DASHBOARD", data); + let newData: cacheDataItem[] = data; + if (selectedApiKeys.length > 0) { + newData = newData.filter((item) => selectedApiKeys.includes(item.api_key)); + } + + if (selectedModels.length > 0) { + newData = newData.filter((item) => selectedModels.includes(item.model)); + } + + /* + Data looks like this + [{"api_key":"147dba2181f28914eea90eb484926c293cdcf7f5b5c9c3dd6a004d9e0f9fdb21","call_type":"acompletion","model":"llama3-8b-8192","total_rows":13,"cache_hit_true_rows":0}, + {"api_key":"8c23f021d0535c2e59abb7d83d0e03ccfb8db1b90e231ff082949d95df419e86","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, + {"api_key":"88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b","call_type":"acompletion","model":"gpt-3.5-turbo","total_rows":19,"cache_hit_true_rows":0}, + {"api_key":"88dc28d0f030c55ed4ab77ed8faf098196cb1c05df778539800c9f1243fe6b4b","call_type":"aimage_generation","model":"","total_rows":3,"cache_hit_true_rows":0}, + {"api_key":"0ad4b3c03dcb6de0b5b8f761db798c6a8ae80be3fd1e2ea30c07ce6d5e3bf870","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, + {"api_key":"034224b36e9769bc50e2190634abc3f97cad789b17ca80ac43b82f46cd5579b3","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, + {"api_key":"4f9c71cce0a2bb9a0b62ce6f0ebb3245b682702a8851d26932fa7e3b8ebfc755","call_type":"","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0}, + */ + + // What data we need for bar chat + // ui_data = [ + // { + // name: "Call Type", + // Cache hit: 20, + // LLM API requests: 10, + // } + // ] + + console.log("before processed data in cache dashboard", newData); + + let llm_api_requests = 0; + let cache_hits = 0; + let cached_tokens = 0; + const processedData = newData.reduce((acc: uiData[], item) => { + console.log("Processing item:", item); + + if (!item.call_type) { + console.log("Item has no call_type:", item); + item.call_type = "Unknown"; + } + + + llm_api_requests += (item.total_rows || 0) - (item.cache_hit_true_rows || 0); + cache_hits += item.cache_hit_true_rows || 0; + cached_tokens += item.cached_completion_tokens || 0; + + const existingItem = acc.find(i => i.name === item.call_type); + if (existingItem) { + existingItem["LLM API requests"] += (item.total_rows || 0) - (item.cache_hit_true_rows || 0); + existingItem["Cache hit"] += item.cache_hit_true_rows || 0; + existingItem["Cached Completion Tokens"] += item.cached_completion_tokens || 0; + existingItem["Generated Completion Tokens"] += item.generated_completion_tokens || 0; + } else { + acc.push({ + name: item.call_type, + "LLM API requests": (item.total_rows || 0) - (item.cache_hit_true_rows || 0), + "Cache hit": item.cache_hit_true_rows || 0, + "Cached Completion Tokens": item.cached_completion_tokens || 0, + "Generated Completion Tokens": item.generated_completion_tokens || 0 + }); + } + return acc; + }, []); + + // set header cache statistics + setCachedResponses(valueFormatterNumbers(cache_hits)); + setCachedTokens(valueFormatterNumbers(cached_tokens)); + if (llm_api_requests > 0) { + let cache_hit_ratio = ((cache_hits / llm_api_requests) * 100).toFixed(2); + setCacheHitRatio(cache_hit_ratio); + } else { + setCacheHitRatio("0"); + } + + setFilteredData(processedData); + + console.log("PROCESSED DATA IN CACHE DASHBOARD", processedData); + + }, [selectedApiKeys, selectedModels, dateValue, data]); + + return ( + + + + + {uniqueApiKeys.map((key) => ( + + {key} + + ))} + + + + + {uniqueModels.map((model) => ( + + {model} + + ))} + + + + { + setDateValue(value); + updateCachingData(value.from, value.to); + }} + selectPlaceholder="Select date range" + /> + + + +
+ +

+ Cache Hit Ratio +

+
+

+ {cacheHitRatio}% +

+ +
+
+ +

+ Cache Hits +

+
+

+ {cachedResponses} +

+ +
+
+ + +

+ Cached Tokens +

+
+

+ {cachedTokens} +

+ +
+
+ +
+ + Cache Hits vs API Requests + + + Cached Completion Tokens vs Generated Completion Tokens + + + +
+ + + + + + ); +}; + +export default CacheDashboard; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index aa774088596..1fafedf18b0 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -79,28 +79,32 @@ const Sidebar: React.FC = ({ Logging & Alerts ) : null} + {userRole == "Admin" ? ( + setPage("caching")}> + Caching + + ) : null} {userRole == "Admin" ? ( - setPage("budgets")}> + setPage("budgets")}> Budgets ) : null} - + {userRole == "Admin" ? ( - setPage("general-settings")}> + setPage("general-settings")}> Router Settings ) : null} - {userRole == "Admin" ? ( - setPage("admin-panel")}> + setPage("admin-panel")}> Admin ) : null} - setPage("api_ref")}> + setPage("api_ref")}> API Reference - setPage("model-hub")}> + setPage("model-hub")}> Model Hub diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index e5371f22e4b..9060c339e13 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1373,6 +1373,47 @@ export const adminGlobalActivity = async ( } }; +export const adminGlobalCacheActivity = async ( + accessToken: String, + startTime: String | undefined, + endTime: String | undefined +) => { + try { + let url = proxyBaseUrl + ? `${proxyBaseUrl}/global/activity/cache_hits` + : `/global/activity/cache_hits`; + + if (startTime && endTime) { + url += `?start_date=${startTime}&end_date=${endTime}`; + } + + 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(); + 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 adminGlobalActivityPerModel = async ( accessToken: String, startTime: String | undefined,