diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx index 96d8d25407e..14f53dd28a0 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx @@ -1,7 +1,7 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import EntityUsage from "./EntityUsage"; import * as networking from "../../../networking"; +import EntityUsage from "./EntityUsage"; beforeAll(() => { if (typeof window !== "undefined" && !window.ResizeObserver) { @@ -269,4 +269,45 @@ describe("EntityUsage", () => { expect(screen.getByText("Total Spend")).toBeInTheDocument(); expect(screen.getAllByText("0")[0]).toBeInTheDocument(); }); + + it("should display Model Activity tab for non-agent entity types", async () => { + render(); + + await waitFor(() => { + expect(mockTagDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Model Activity")).toBeInTheDocument(); + }); + + it("should display Request / Token Consumption tab for agent entity type", async () => { + render(); + + await waitFor(() => { + expect(mockAgentDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Request / Token Consumption")).toBeInTheDocument(); + }); + + it("should display Top Models title for non-agent entity types", async () => { + render(); + + await waitFor(() => { + expect(mockTagDailyActivityCall).toHaveBeenCalled(); + }); + + const topModelsElements = screen.getAllByText("Top Models"); + expect(topModelsElements.length).toBeGreaterThan(0); + }); + + it("should display Top Agents title for agent entity type", async () => { + render(); + + await waitFor(() => { + expect(mockAgentDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Top Agents")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx index 3179ce25f80..3ccda5aa047 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx @@ -1,41 +1,41 @@ -import React, { useState, useEffect } from "react"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; import { BarChart, Card, - Title, - Text, - Grid, Col, DateRangePickerValue, + DonutChart, + Grid, + Subtitle, + Tab, + TabGroup, Table, - TableHead, - TableRow, - TableHeaderCell, TableBody, TableCell, - DonutChart, - TabPanel, - TabGroup, + TableHead, + TableHeaderCell, + TableRow, TabList, - Tab, + TabPanel, TabPanels, - Subtitle, + Text, + Title, } from "@tremor/react"; +import React, { useEffect, useState } from "react"; import { ActivityMetrics, processActivityData } from "../../../activity_metrics"; -import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from "../../types"; +import { UsageExportHeader } from "../../../EntityUsageExport"; +import type { EntityType } from "../../../EntityUsageExport/types"; import { + agentDailyActivityCall, + customerDailyActivityCall, organizationDailyActivityCall, tagDailyActivityCall, teamDailyActivityCall, - customerDailyActivityCall, - agentDailyActivityCall, } from "../../../networking"; -import TopKeyView from "./TopKeyView"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { valueFormatterSpend } from "../../utils/value_formatters"; import { getProviderLogoAndName } from "../../../provider_info_helpers"; -import { UsageExportHeader } from "../../../EntityUsageExport"; -import type { EntityType } from "../../../EntityUsageExport/types"; +import { BreakdownMetrics, DailyData, EntityMetricWithMetadata, KeyMetricWithMetadata, TagUsage } from "../../types"; +import { valueFormatterSpend } from "../../utils/value_formatters"; +import TopKeyView from "./TopKeyView"; import TopModelView from "./TopModelView"; interface EntityMetrics { @@ -385,7 +385,7 @@ const EntityUsage: React.FC = ({ Cost - Model Activity + {entityType === "agent" ? "Request / Token Consumption" : "Model Activity"} Key Activity @@ -583,7 +583,7 @@ const EntityUsage: React.FC = ({ {/* Top Models */} - Top Models + {entityType === "agent" ? "Top Agents" : "Top Models"} @@ -661,10 +661,10 @@ const EntityUsage: React.FC = ({ - + - + diff --git a/ui/litellm-dashboard/src/components/activity_metrics.test.tsx b/ui/litellm-dashboard/src/components/activity_metrics.test.tsx new file mode 100644 index 00000000000..04c2e4ec597 --- /dev/null +++ b/ui/litellm-dashboard/src/components/activity_metrics.test.tsx @@ -0,0 +1,101 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { beforeAll, describe, expect, it, vi } from "vitest"; +import { ActivityMetrics } from "./activity_metrics"; +import { ModelActivityData } from "./UsagePage/types"; + +beforeAll(() => { + if (typeof window !== "undefined" && !window.ResizeObserver) { + window.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + } as any; + } +}); + +vi.mock("@tremor/react", () => ({ + Card: ({ children }: { children: React.ReactNode }) =>
{children}
, + Grid: ({ children }: { children: React.ReactNode }) =>
{children}
, + Text: ({ children }: { children: React.ReactNode }) => {children}, + Title: ({ children }: { children: React.ReactNode }) =>

{children}

, + AreaChart: () =>
AreaChart
, + BarChart: () =>
BarChart
, +})); + +vi.mock("antd", () => { + const CollapseComponent = ({ children }: { children: React.ReactNode }) =>
{children}
; + CollapseComponent.Panel = ({ children, header }: { children: React.ReactNode; header: React.ReactNode }) => ( +
+
{header}
+
{children}
+
+ ); + return { + Collapse: CollapseComponent, + }; +}); + +vi.mock("./UsagePage/utils/value_formatters", () => ({ + valueFormatter: (value: number) => value.toString(), +})); + +vi.mock("./common_components/chartUtils", () => ({ + CustomTooltip: () => null, + CustomLegend: () =>
Legend
, +})); + +vi.mock("@/utils/dataUtils", () => ({ + formatNumberWithCommas: (value: number, decimals?: number) => { + return value.toFixed(decimals || 0); + }, +})); + +describe("ActivityMetrics", () => { + const mockModelMetrics: Record = { + "gpt-4": { + label: "GPT-4", + total_requests: 100, + total_successful_requests: 95, + total_failed_requests: 5, + total_tokens: 50000, + prompt_tokens: 30000, + completion_tokens: 20000, + total_spend: 100.5, + total_cache_read_input_tokens: 1000, + total_cache_creation_input_tokens: 500, + top_api_keys: [], + daily_data: [ + { + date: "2025-01-01", + metrics: { + prompt_tokens: 30000, + completion_tokens: 20000, + total_tokens: 50000, + api_requests: 100, + spend: 100.5, + successful_requests: 95, + failed_requests: 5, + cache_read_input_tokens: 1000, + cache_creation_input_tokens: 500, + }, + }, + ], + }, + }; + + it("should render", () => { + render(); + expect(screen.getByText("Overall Usage")).toBeInTheDocument(); + }); + + it("should display prompt caching metrics when hidePromptCachingMetrics is false", () => { + render(); + expect(screen.getByText("Prompt Caching Metrics")).toBeInTheDocument(); + }); + + it("should hide prompt caching metrics when hidePromptCachingMetrics is true", () => { + render(); + expect(screen.queryByText("Prompt Caching Metrics")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 8b1c03e92db..f8535b8a7e2 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -6,12 +6,22 @@ import { Collapse } from "antd"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { valueFormatter } from "./UsagePage/utils/value_formatters"; import { CustomTooltip, CustomLegend } from "./common_components/chartUtils"; +import { EntityType } from "./EntityUsageExport/types"; interface ActivityMetricsProps { modelMetrics: Record; + hidePromptCachingMetrics?: boolean; } -const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => { +const ModelSection = ({ + modelName, + metrics, + hidePromptCachingMetrics = false, +}: { + modelName: string; + metrics: ModelActivityData; + hidePromptCachingMetrics?: boolean; +}) => { return (
{/* Summary Cards */} @@ -138,35 +148,37 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode /> - -
- Prompt Caching Metrics - +
+ Prompt Caching Metrics + +
+
+ Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens + Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens +
+ -
-
- Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens - Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens -
- -
+ + )}
); }; -export const ActivityMetrics: React.FC = ({ modelMetrics }) => { +export const ActivityMetrics: React.FC = ({ modelMetrics, hidePromptCachingMetrics = false }) => { const modelNames = Object.keys(modelMetrics).sort((a, b) => { if (a === "") return 1; if (b === "") return -1; @@ -314,7 +326,11 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } } > - + ))} diff --git a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx index d9e70ef3336..4293657b71a 100644 --- a/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx +++ b/ui/litellm-dashboard/src/components/shared/advanced_date_picker.tsx @@ -1,7 +1,7 @@ -import React, { useCallback, useState, useRef, useEffect } from "react"; -import { DateRangePickerValue, Text, Button } from "@tremor/react"; import { CalendarOutlined, ClockCircleOutlined } from "@ant-design/icons"; +import { Button, DateRangePickerValue, Text } from "@tremor/react"; import moment from "moment"; +import React, { useCallback, useEffect, useRef, useState } from "react"; interface AdvancedDatePickerProps { value: DateRangePickerValue; @@ -299,7 +299,7 @@ const AdvancedDatePicker: React.FC = ({ {/* Dropdown panel */} {isOpen && ( -
+
{/* Left side - Relative time options */}