From 20cf663a1c8b1a8f1e49a0a980434c8efa952bf2 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sun, 19 Jul 2026 03:33:32 +0000 Subject: [PATCH] feat(cost-optimization): show recent optimized requests on dashboard Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/db/db_spend_update_writer.py | 17 +- .../cost_optimization_endpoints.py | 180 ++++++++++++++++++ litellm/proxy/proxy_server.py | 4 + litellm/proxy/spend_tracking/optimization.py | 29 +++ .../management_endpoints/cost_optimization.py | 26 +++ .../test_cost_optimization_endpoints.py | 83 ++++++++ .../_components/CostOptimizationView.test.tsx | 40 ++++ .../_components/CostOptimizationView.tsx | 174 ++++++++++++++++- .../src/components/networking.tsx | 49 +++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 94 +++++++++ 10 files changed, 680 insertions(+), 16 deletions(-) create mode 100644 litellm/proxy/management_endpoints/cost_optimization_endpoints.py create mode 100644 litellm/proxy/spend_tracking/optimization.py create mode 100644 litellm/types/proxy/management_endpoints/cost_optimization.py create mode 100644 tests/test_litellm/proxy/management_endpoints/test_cost_optimization_endpoints.py diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index 2262141f426..f582dfe832f 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -29,8 +29,8 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.caching import RedisCache from litellm.constants import ( - DB_SPEND_UPDATE_JOB_NAME, DB_DAILY_TAG_SPEND_UPDATE_JOB_NAME, + DB_SPEND_UPDATE_JOB_NAME, ) from litellm.litellm_core_utils.safe_json_loads import safe_json_loads from litellm.proxy._types import ( @@ -62,6 +62,7 @@ from litellm.proxy.route_llm_request import ROUTE_ENDPOINT_MAPPING from litellm.proxy.spend_tracking.compression_savings import ( extract_compression_saved_tokens, ) +from litellm.proxy.spend_tracking.optimization import extract_cache_read_tokens from litellm.proxy.spend_tracking.savings import compute_savings_spend from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error @@ -72,18 +73,6 @@ else: ProxyLogging = Any -def _extract_cache_read_tokens(usage_obj: dict) -> int: - """ - Anthropic: top-level cache_read_input_tokens field. - OpenAI-compatible (moonshotai, openai, deepseek, etc.): prompt_tokens_details.cached_tokens. - """ - explicit = usage_obj.get("cache_read_input_tokens", 0) or 0 - if explicit: - return int(explicit) - details = usage_obj.get("prompt_tokens_details") or {} - return int(details.get("cached_tokens", 0) or 0) - - def _extract_cache_creation_tokens(usage_obj: dict) -> int: """ Anthropic: top-level cache_creation_input_tokens field. @@ -1854,7 +1843,7 @@ class DBSpendUpdateWriter: if call_type: endpoint = ROUTE_ENDPOINT_MAPPING.get(call_type, None) - cache_read_input_tokens = _extract_cache_read_tokens(usage_obj) + cache_read_input_tokens = extract_cache_read_tokens({"usage_object": usage_obj}) compression_saved_tokens = extract_compression_saved_tokens(_metadata) savings_spend = compute_savings_spend( model=payload.get("model", None), diff --git a/litellm/proxy/management_endpoints/cost_optimization_endpoints.py b/litellm/proxy/management_endpoints/cost_optimization_endpoints.py new file mode 100644 index 00000000000..44af7822ef0 --- /dev/null +++ b/litellm/proxy/management_endpoints/cost_optimization_endpoints.py @@ -0,0 +1,180 @@ +import json +from collections.abc import Mapping +from datetime import datetime, timedelta, timezone +from typing import Awaitable, Callable, Literal, cast + +from fastapi import APIRouter, Depends, Query + +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.spend_tracking.compression_savings import extract_compression_saved_tokens +from litellm.proxy.spend_tracking.optimization import extract_cache_read_tokens +from litellm.proxy.spend_tracking.savings import compute_savings_spend +from litellm.types.proxy.management_endpoints.cost_optimization import ( + OptimizedRequestLog, + OptimizedRequestLogsResponse, +) + +router = APIRouter() + + +def _metadata_dict(value: object) -> Mapping[str, object]: + if isinstance(value, dict): + return cast(Mapping[str, object], value) # cast-ok: JSON metadata is validated as a mapping above + if isinstance(value, str): + try: + parsed = json.loads(value) + return ( + cast(Mapping[str, object], parsed) # cast-ok: parsed JSON is checked as a dict + if isinstance(parsed, dict) + else {} + ) + except (TypeError, ValueError): + return {} + return {} + + +def _nonnegative_int(value: object) -> int: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return 0 + return max(int(value), 0) + + +def build_optimized_request_log(row: Mapping[str, object]) -> OptimizedRequestLog | None: + metadata = _metadata_dict(row.get("metadata")) + cache_read_tokens = extract_cache_read_tokens(metadata) + tokens_saved = extract_compression_saved_tokens(metadata) + if cache_read_tokens <= 0 and tokens_saved <= 0: + return None + + model = str(row.get("model", "") or "") + provider_value = row.get("custom_llm_provider") + provider = str(provider_value) if provider_value is not None else None + savings_spend = compute_savings_spend( + model=model, + custom_llm_provider=provider, + compression_saved_tokens=tokens_saved, + cache_read_input_tokens=cache_read_tokens, + ) + compression_savings = float(savings_spend.compression) + caching_savings = float(savings_spend.prompt_caching) + if tokens_saved > 0 and cache_read_tokens > 0: + optimization_type: Literal["compression", "caching", "both"] = "both" + elif tokens_saved > 0: + optimization_type = "compression" + else: + optimization_type = "caching" + + spend_value = row.get("spend") + spend = float(spend_value) if isinstance(spend_value, (int, float)) else 0.0 + timestamp = row.get("startTime") + if isinstance(timestamp, datetime): + timestamp = timestamp.isoformat() + else: + timestamp = str(timestamp or "") + + return OptimizedRequestLog( + request_id=str(row.get("request_id", "")), + timestamp=timestamp, + model=str(model or ""), + total_tokens=_nonnegative_int(row.get("total_tokens")), + optimization_type=optimization_type, + spend=spend, + savings=compression_savings + caching_savings, + original_cost=spend + compression_savings + caching_savings, + compression_savings_spend=compression_savings, + prompt_caching_savings_spend=caching_savings, + tokens_saved=tokens_saved, + cache_read_tokens=cache_read_tokens, + ) + + +def _cache_read_filter_sql() -> str: + candidates = ( + "(metadata->'usage_object'->>'cache_read_input_tokens')", + "(metadata->'usage_object'->'prompt_tokens_details'->>'cached_tokens')", + "(metadata->'additional_usage_values'->>'cache_read_input_tokens')", + "(metadata->'additional_usage_values'->'prompt_tokens_details'->>'cached_tokens')", + ) + return " OR ".join(f"({candidate}) ~ '^[0-9]+$' AND ({candidate})::bigint > 0" for candidate in candidates) + + +def _optimized_where_sql() -> str: + return f"""( + (metadata->'compression_savings' IS NOT NULL + AND metadata->'compression_savings' <> 'null'::jsonb) + OR {_cache_read_filter_sql()} + )""" + + +def _parse_date(value: str | None, *, end: bool = False) -> datetime | None: + if not value: + return None + parsed = datetime.strptime(value, "%Y-%m-%d %H:%M:%S" if " " in value else "%Y-%m-%d") + parsed = parsed.replace(tzinfo=timezone.utc) + if end and " " not in value: + parsed += timedelta(days=1) + return parsed + + +@router.get( + "/cost_optimization/usage/logs", + tags=["Cost Optimization"], + dependencies=[Depends(user_api_key_auth)], + response_model=OptimizedRequestLogsResponse, +) +async def cost_optimization_usage_logs( + page: int = Query(default=1, ge=1), + page_size: int = Query(default=50, ge=1, le=100), + start_date: str | None = Query(default=None), + end_date: str | None = Query(default=None), +) -> OptimizedRequestLogsResponse: + from litellm.proxy.proxy_server import prisma_client + + if prisma_client is None: + return OptimizedRequestLogsResponse(logs=[], total=0, page=page, page_size=page_size, total_pages=0) + + conditions = [_optimized_where_sql()] + params: list[object] = [] + if start_date: + conditions.append('"startTime" >= $%d' % (len(params) + 1)) + params.append(_parse_date(start_date)) + if end_date: + conditions.append('"startTime" < $%d' % (len(params) + 1)) + params.append(_parse_date(end_date, end=True)) + where_sql = " AND ".join(conditions) + + query_raw: Callable[..., Awaitable[object]] = cast( # cast-ok: Prisma exposes query_raw dynamically + Callable[..., Awaitable[object]], prisma_client.db.query_raw + ) + total_rows = cast( # cast-ok: Prisma raw query returns rows with the selected columns + list[Mapping[str, object]], + await query_raw( + f'SELECT COUNT(*)::bigint AS total FROM "LiteLLM_SpendLogs" WHERE {where_sql}', + *params, + ), + ) + total_value = total_rows[0].get("total") if total_rows else 0 + total = int(total_value) if isinstance(total_value, (int, float)) else 0 + offset_placeholder = len(params) + 1 + limit_placeholder = len(params) + 2 + rows = cast( # cast-ok: Prisma raw query returns rows with the selected columns + list[Mapping[str, object]], + await query_raw( + f"""SELECT request_id, "startTime", model, custom_llm_provider, total_tokens, spend, metadata + FROM "LiteLLM_SpendLogs" + WHERE {where_sql} + ORDER BY "startTime" DESC + LIMIT ${limit_placeholder} OFFSET ${offset_placeholder}""", + *params, + page_size, + (page - 1) * page_size, + ), + ) + logs = [entry for row in rows if (entry := build_optimized_request_log(row)) is not None] + return OptimizedRequestLogsResponse( + logs=logs, + total=total, + page=page, + page_size=page_size, + total_pages=(total + page_size - 1) // page_size, + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 3b40abed19e..6104a3a756d 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -506,6 +506,9 @@ from litellm.proxy.spend_tracking.budget_reservation import get_budget_window_st from litellm.proxy.spend_tracking.spend_management_endpoints import ( router as spend_management_router, ) +from litellm.proxy.management_endpoints.cost_optimization_endpoints import ( + router as cost_optimization_router, +) from litellm.proxy.spend_tracking.spend_tracking_utils import get_logging_payload from litellm.proxy.types_utils.utils import get_instance_fn from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( @@ -16191,6 +16194,7 @@ app.include_router(ui_sso_router) app.include_router(organization_router) app.include_router(customer_router) app.include_router(spend_management_router) +app.include_router(cost_optimization_router) app.include_router(caching_router) app.include_router(analytics_router) app.include_router(callback_management_endpoints_router) diff --git a/litellm/proxy/spend_tracking/optimization.py b/litellm/proxy/spend_tracking/optimization.py new file mode 100644 index 00000000000..7fe4b0662b4 --- /dev/null +++ b/litellm/proxy/spend_tracking/optimization.py @@ -0,0 +1,29 @@ +from collections.abc import Mapping +from typing import cast + + +def _positive_int(value: object) -> int: + if isinstance(value, bool) or not isinstance(value, (int, float)): + return 0 + return max(int(value), 0) + + +def _extract_cache_read_tokens_from_usage(usage_obj: object) -> int: + if not isinstance(usage_obj, Mapping): + return 0 + typed_usage = cast(Mapping[str, object], usage_obj) # cast-ok: usage object is checked as a mapping above + explicit = _positive_int(typed_usage.get("cache_read_input_tokens")) + if explicit: + return explicit + details = typed_usage.get("prompt_tokens_details") + if isinstance(details, Mapping): + typed_details = cast(Mapping[str, object], details) # cast-ok: details is checked as a mapping above + return _positive_int(typed_details.get("cached_tokens")) + return 0 + + +def extract_cache_read_tokens(metadata: Mapping[str, object]) -> int: + return max( + _extract_cache_read_tokens_from_usage(metadata.get("usage_object")), + _extract_cache_read_tokens_from_usage(metadata.get("additional_usage_values")), + ) diff --git a/litellm/types/proxy/management_endpoints/cost_optimization.py b/litellm/types/proxy/management_endpoints/cost_optimization.py new file mode 100644 index 00000000000..747a05c6a75 --- /dev/null +++ b/litellm/types/proxy/management_endpoints/cost_optimization.py @@ -0,0 +1,26 @@ +from typing import List, Literal + +from pydantic import BaseModel + + +class OptimizedRequestLog(BaseModel): + request_id: str + timestamp: str + model: str + total_tokens: int + optimization_type: Literal["compression", "caching", "both"] + spend: float + savings: float + original_cost: float + compression_savings_spend: float + prompt_caching_savings_spend: float + tokens_saved: int + cache_read_tokens: int + + +class OptimizedRequestLogsResponse(BaseModel): + logs: List[OptimizedRequestLog] + total: int + page: int + page_size: int + total_pages: int diff --git a/tests/test_litellm/proxy/management_endpoints/test_cost_optimization_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_cost_optimization_endpoints.py new file mode 100644 index 00000000000..33bdaa81dbc --- /dev/null +++ b/tests/test_litellm/proxy/management_endpoints/test_cost_optimization_endpoints.py @@ -0,0 +1,83 @@ +from collections.abc import Mapping +from datetime import datetime, timezone +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from litellm.proxy.management_endpoints.cost_optimization_endpoints import ( + build_optimized_request_log, + cost_optimization_usage_logs, +) + + +def _row(metadata: Mapping[str, object], **overrides: object) -> dict[str, object]: + return { + "request_id": "req-1", + "startTime": datetime(2026, 7, 13, 12, 0, tzinfo=timezone.utc), + "model": "test-model", + "custom_llm_provider": "openai", + "total_tokens": 150, + "spend": 0.02, + "metadata": metadata, + **overrides, + } + + +def test_build_optimized_request_log_computes_both_savings(): + row = _row( + { + "compression_savings": {"tokens_saved": 100}, + "usage_object": {"cache_read_input_tokens": 50}, + } + ) + with patch( + "litellm.proxy.spend_tracking.savings._input_and_cache_read_cost", + return_value=(0.001, 0.0002), + ): + result = build_optimized_request_log(row) + + assert result is not None + assert result.optimization_type == "both" + assert result.tokens_saved == 100 + assert result.cache_read_tokens == 50 + assert result.compression_savings_spend == 0.1 + assert result.prompt_caching_savings_spend == 0.04 + assert result.savings == 0.14 + assert result.original_cost == 0.16 + + +def test_build_optimized_request_log_ignores_unoptimized_row(): + assert build_optimized_request_log(_row({"usage_object": {"prompt_tokens": 10}})) is None + + +@pytest.mark.asyncio +async def test_cost_optimization_usage_logs_returns_paginated_entries(): + db = MagicMock() + query_raw_mock = AsyncMock( + side_effect=[ + [{"total": 1}], + [_row({"compression_savings": {"tokens_saved": 25}})], + ] + ) + db.query_raw = query_raw_mock + prisma = MagicMock() + prisma.db = db + with ( + patch("litellm.proxy.proxy_server.prisma_client", prisma), + patch( + "litellm.proxy.spend_tracking.savings._input_and_cache_read_cost", + return_value=(0.001, 0.001), + ), + ): + result = await cost_optimization_usage_logs( + page=1, + page_size=50, + start_date="2026-07-01", + end_date="2026-07-31", + ) + + assert result.total == 1 + assert result.total_pages == 1 + assert result.logs[0].optimization_type == "compression" + assert result.logs[0].tokens_saved == 25 + assert query_raw_mock.await_count == 2 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx index 92426d3ce04..8a2ca8e5a05 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx @@ -4,6 +4,11 @@ import { describe, expect, it, vi } from "vitest"; import type { DailyData, SpendMetrics } from "@/components/UsagePage/types"; const mockUsePaginatedDailyActivity = vi.fn(); +const mockUseQuery = vi.fn(); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: (args: unknown) => mockUseQuery(args), +})); vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", () => ({ usePaginatedDailyActivity: (args: unknown) => mockUsePaginatedDailyActivity(args), @@ -11,6 +16,7 @@ vi.mock("@/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity", ( vi.mock("@/components/networking", () => ({ userDailyActivityCall: vi.fn(), + getCostOptimizationUsageLogs: vi.fn(), })); vi.mock("@/components/shared/advanced_date_picker", () => ({ @@ -57,6 +63,29 @@ const day = (date: string, metrics: Partial): DailyData => ({ const renderWith = (results: DailyData[]) => { mockUsePaginatedDailyActivity.mockReturnValue({ data: { results }, loading: false, isFetchingMore: false }); + mockUseQuery.mockReturnValue({ + data: { + logs: [ + { + request_id: "req-123456789", + timestamp: "2026-07-13T12:00:00Z", + model: "test-model", + total_tokens: 150, + optimization_type: "both", + spend: 0.02, + savings: 0.14, + original_cost: 0.16, + }, + ], + total: 1, + page: 1, + page_size: 50, + total_pages: 1, + }, + isLoading: false, + isFetching: false, + error: null, + }); return render(); }; @@ -106,4 +135,15 @@ describe("CostOptimizationView", () => { const slices = JSON.parse(getByTestId("donut-chart").getAttribute("data-slices") ?? "[]"); expect(slices).toEqual([{ driver: "Compression", usd: expect.closeTo(0.04, 5) }]); }); + + it("renders recent optimized requests with savings and optimization type", () => { + const { getByText } = renderWith([day("2026-07-12", { compression_savings_spend: 0.04 })]); + + expect(getByText("Recent Optimized Requests")).toBeInTheDocument(); + expect(getByText("req-123456789")).toBeInTheDocument(); + expect(getByText("Both")).toBeInTheDocument(); + expect(getByText("$0.1600")).toBeInTheDocument(); + expect(getByText("$0.0200")).toBeInTheDocument(); + expect(getByText("$0.1400")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx index e45e3e23f1d..358113dcb2b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx @@ -2,11 +2,17 @@ import React, { useMemo, useState } from "react"; import { PiggyBank } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; import { AreaChart, DonutChart } from "@/components/shared/charts"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { userDailyActivityCall } from "@/components/networking"; +import { + getCostOptimizationUsageLogs, + type OptimizedRequestLog, + type OptimizedRequestLogsResponse, + userDailyActivityCall, +} from "@/components/networking"; import { DailyData, SpendMetrics } from "@/components/UsagePage/types"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { all_admin_roles } from "@/utils/roles"; @@ -46,6 +52,112 @@ const SummaryCard = ({ label, value, hint }: { label: string; value: string; hin ); +const optimizationTypeLabel = (type: OptimizedRequestLog["optimization_type"]): string => { + if (type === "both") return "Both"; + if (type === "caching") return "Caching"; + return "Compression"; +}; + +const optimizationTypeClass = (type: OptimizedRequestLog["optimization_type"]): string => { + if (type === "both") return "bg-purple-50 text-purple-700 border-purple-200"; + if (type === "caching") return "bg-green-50 text-green-700 border-green-200"; + return "bg-blue-50 text-blue-700 border-blue-200"; +}; + +const optimizedLogsDescription = ( + loading: boolean, + fetching: boolean, + error: unknown, + data: OptimizedRequestLogsResponse | undefined, +): string => { + if (loading || fetching) return "Loading..."; + if (error) return "Failed to load optimized requests"; + if (data?.total) return `Showing ${data.logs.length} of ${data.total} requests`; + return "No optimized requests for this period"; +}; + +const OptimizedRequestsTable = ({ + data, + logsPage, + onPrevious, + onNext, +}: { + data: OptimizedRequestLogsResponse; + logsPage: number; + onPrevious: () => void; + onNext: () => void; +}) => ( + <> +
+ + + + + + + + + + + + + + + {data.logs.map((log) => ( + + + + + + + + + + + ))} + +
Request IDTimestampModelTokensTypeOriginal CostOptimized CostSavings
+ {log.request_id} + + {new Date(log.timestamp).toLocaleString()} + + {log.model} + {formatNumberWithCommas(log.total_tokens)} + + {optimizationTypeLabel(log.optimization_type)} + + {usd(log.original_cost)}{usd(log.spend)}{usd(log.savings)}
+
+ {data.total_pages > 1 && ( +
+

+ Page {data.page} of {data.total_pages} +

+
+ + +
+
+ )} + +); + const CostOptimizationView: React.FC = ({ accessToken, userId, userRole }) => { const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []); const initialTo = useMemo(() => new Date(), []); @@ -68,6 +180,32 @@ const CostOptimizationView: React.FC = ({ accessToken const cachingTotal = useMemo(() => results.reduce((sum, d) => sum + cachingOf(d.metrics), 0), [results]); const savedTokensTotal = useMemo(() => results.reduce((sum, d) => sum + savedTokensOf(d.metrics), 0), [results]); const totalSaved = compressionTotal + cachingTotal; + const [logsPage, setLogsPage] = useState(1); + const startDate = startTime ? startTime.toISOString().slice(0, 10) : ""; + const endDate = endTime ? endTime.toISOString().slice(0, 10) : ""; + const { + data: optimizedLogsData, + isLoading: optimizedLogsLoading, + isFetching: optimizedLogsFetching, + error: optimizedLogsError, + } = useQuery({ + queryKey: ["cost-optimization-usage-logs", startDate, endDate, logsPage], + queryFn: () => + getCostOptimizationUsageLogs({ + accessToken: accessToken!, + startDate, + endDate, + page: logsPage, + pageSize: 50, + }), + enabled: !!accessToken && !!startDate && !!endDate, + }); + const logsDescription = optimizedLogsDescription( + optimizedLogsLoading, + optimizedLogsFetching, + optimizedLogsError, + optimizedLogsData, + ); const overTime = useMemo( () => @@ -100,7 +238,13 @@ const CostOptimizationView: React.FC = ({ accessToken Money saved by prompt compression and prompt caching across your requests

- setDateValue(v)} /> + { + setDateValue(v); + setLogsPage(1); + }} + />
@@ -150,6 +294,32 @@ const CostOptimizationView: React.FC = ({ accessToken
+ + + + Recent Optimized Requests +

{logsDescription}

+
+ + {optimizedLogsLoading && ( +
Loading optimized requests...
+ )} + {!optimizedLogsLoading && optimizedLogsError && ( +
Failed to load optimized requests.
+ )} + {!optimizedLogsLoading && !optimizedLogsError && optimizedLogsData?.logs.length ? ( + setLogsPage((page) => Math.max(1, page - 1))} + onNext={() => setLogsPage((page) => Math.min(optimizedLogsData.total_pages, page + 1))} + /> + ) : null} + {!optimizedLogsLoading && !optimizedLogsError && !optimizedLogsData?.logs.length && ( +
No optimized requests to display.
+ )} +
+
); }; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index d44a491b840..df59f20d947 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1403,6 +1403,55 @@ export const userDailyActivityCall = async ( }); }; +export interface OptimizedRequestLog { + request_id: string; + timestamp: string; + model: string; + total_tokens: number; + optimization_type: "compression" | "caching" | "both"; + spend: number; + savings: number; + original_cost: number; + compression_savings_spend: number; + prompt_caching_savings_spend: number; + tokens_saved: number; + cache_read_tokens: number; +} + +export interface OptimizedRequestLogsResponse { + logs: OptimizedRequestLog[]; + total: number; + page: number; + page_size: number; + total_pages: number; +} + +export interface CostOptimizationUsageLogsArgs { + accessToken: string; + startDate: string; + endDate: string; + page?: number; + pageSize?: number; +} + +export const getCostOptimizationUsageLogs = async ({ + accessToken, + startDate, + endDate, + page = 1, + pageSize = 50, +}: CostOptimizationUsageLogsArgs): Promise => { + return apiClient.get("/cost_optimization/usage/logs", { + accessToken, + query: { + start_date: startDate, + end_date: endDate, + page, + page_size: pageSize, + }, + }); +}; + export const tagDailyActivityCall = async ( accessToken: string, startTime: Date, diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 2ceb75a06a9..8827da8eebe 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -2474,6 +2474,23 @@ export interface paths { patch?: never; trace?: never; }; + "/cost_optimization/usage/logs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Cost Optimization Usage Logs */ + get: operations["cost_optimization_usage_logs_cost_optimization_usage_logs_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/credentials": { parameters: { query?: never; @@ -28573,6 +28590,49 @@ export interface components { */ type: "openIdConnect"; }; + /** OptimizedRequestLog */ + OptimizedRequestLog: { + /** Cache Read Tokens */ + cache_read_tokens: number; + /** Compression Savings Spend */ + compression_savings_spend: number; + /** Model */ + model: string; + /** + * Optimization Type + * @enum {string} + */ + optimization_type: "compression" | "caching" | "both"; + /** Original Cost */ + original_cost: number; + /** Prompt Caching Savings Spend */ + prompt_caching_savings_spend: number; + /** Request Id */ + request_id: string; + /** Savings */ + savings: number; + /** Spend */ + spend: number; + /** Timestamp */ + timestamp: string; + /** Tokens Saved */ + tokens_saved: number; + /** Total Tokens */ + total_tokens: number; + }; + /** OptimizedRequestLogsResponse */ + OptimizedRequestLogsResponse: { + /** Logs */ + logs: components["schemas"]["OptimizedRequestLog"][]; + /** Page */ + page: number; + /** Page Size */ + page_size: number; + /** Total */ + total: number; + /** Total Pages */ + total_pages: number; + }; /** OrgMember */ OrgMember: { /** @@ -37642,6 +37702,40 @@ export interface operations { }; }; }; + cost_optimization_usage_logs_cost_optimization_usage_logs_get: { + parameters: { + query?: { + page?: number; + page_size?: number; + start_date?: string | null; + end_date?: string | null; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OptimizedRequestLogsResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; get_credentials_credentials_get: { parameters: { query?: never;