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
+
+
+
+
+
+
+ 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,