From c8eddb7af6a8d4b339d4066f7900b9c75f55fcea Mon Sep 17 00:00:00 2001 From: mrinal Date: Wed, 21 Oct 2026 16:04:11 +0000 Subject: [PATCH] feat(ui): page the Spend Per Tag card through the team tags report Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../EntityUsage/EntityUsage.test.tsx | 95 +++++++------- .../EntityUsage/TeamTagSpendCard.tsx | 118 ++++++++++++++---- .../components/EntityUsage/entityFetchFns.ts | 14 ++- .../EntityUsage/teamTagSpend.test.ts | 70 +---------- .../components/EntityUsage/teamTagSpend.ts | 42 ------- .../components/UsagePage/dailyActivityApi.ts | 20 +-- .../networking.dailyActivity.test.ts | 27 ++-- .../src/components/networking.tsx | 17 ++- 8 files changed, 210 insertions(+), 193 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx index 912483ecbc7..3713dd1ea1e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.test.tsx @@ -12,6 +12,7 @@ import type { ModelActivityData, SpendMetrics, } from "@/components/UsagePage/types"; +import type { DailyActivityTagPageResponse } from "@/components/UsagePage/dailyActivityApi"; import EntityUsage from "./EntityUsage"; import { getGlobalTopKeys, getTopAPIKeys } from "./entityUsageAggregations"; @@ -34,6 +35,37 @@ const emptySpendMetrics: SpendMetrics = { cache_creation_input_tokens: 0, }; +const tagSpendRow = ( + tag: string, + spend: number, + apiRequests: number, +): DailyActivityTagPageResponse["tags"][number] => ({ + tag, + spend, + prompt_tokens: apiRequests * 10, + completion_tokens: apiRequests * 5, + total_tokens: apiRequests * 15, + api_requests: apiRequests, + successful_requests: apiRequests, + failed_requests: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, +}); + +const tagSpendPage = (offset: number): DailyActivityTagPageResponse => ({ + tags: + offset === 0 + ? [ + tagSpendRow("project:rag-v2", 0.25, 1), + tagSpendRow("cost-center:cc-42", 0.5, 2), + ...Array.from({ length: 48 }, (_, index) => tagSpendRow(`tag-${index}`, 0.01, 1)), + ] + : Array.from({ length: 10 }, (_, index) => tagSpendRow(`next-${index}`, 0.01, 1)), + total_tags: 60, + offset, + limit: 50, +}); + const createKeyMetrics = (spend: number, metadata: KeyMetadata): KeyMetricWithMetadata => ({ metrics: { ...emptySpendMetrics, spend }, metadata, @@ -68,6 +100,7 @@ vi.mock("@/components/networking", () => ({ dailyActivityKeyPageCall: vi.fn(), dailyActivityKeySearchCall: vi.fn(), dailyActivityModelTopKeysCall: vi.fn(), + dailyActivityTagPageCall: vi.fn(), dailyActivityExportCall: vi.fn(), })); @@ -165,6 +198,7 @@ vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({ describe("EntityUsage", () => { const mockDailyActivityAggregatedCall = vi.mocked(networking.dailyActivityAggregatedCall); const mockDailyActivityKeyPageCall = vi.mocked(networking.dailyActivityKeyPageCall); + const mockDailyActivityTagPageCall = vi.mocked(networking.dailyActivityTagPageCall); const mockTagDailyActivityCall = vi.fn(); const mockTeamDailyActivityCall = vi.fn(); const mockOrganizationDailyActivityCall = vi.fn(); @@ -476,6 +510,7 @@ describe("EntityUsage", () => { beforeEach(() => { mockDailyActivityAggregatedCall.mockReset(); mockDailyActivityKeyPageCall.mockReset(); + mockDailyActivityTagPageCall.mockReset(); const emptyKeyPage = { api_keys: [], total_api_keys: 0, @@ -483,6 +518,7 @@ describe("EntityUsage", () => { limit: 50, }; mockDailyActivityKeyPageCall.mockResolvedValue(emptyKeyPage); + mockDailyActivityTagPageCall.mockImplementation((request) => Promise.resolve(tagSpendPage(request.offset))); mockDailyActivityAggregatedCall.mockImplementation((entity, request) => ( entityMocks[entity] as unknown as ( @@ -610,44 +646,7 @@ describe("EntityUsage", () => { expect(screen.queryByText(/^team-user-spend:/)).not.toBeInTheDocument(); }); - const tagSpendFor = (tags: Record) => ({ - results: [ - { - date: "2025-01-01", - metrics: { ...emptySpendMetrics }, - breakdown: { - models: {}, - model_groups: {}, - mcp_servers: {}, - providers: {}, - api_keys: {}, - entities: Object.fromEntries( - Object.entries(tags).map(([tag, { spend, api_requests }]) => [ - tag, - { - metrics: { ...emptySpendMetrics, spend, api_requests, successful_requests: api_requests }, - metadata: {}, - api_key_breakdown: {}, - }, - ]), - ), - }, - }, - ], - metadata: { total_spend: 0, total_api_requests: 0 }, - }); - - it("shows one selected team's spend per tag from a group_by=tag request", async () => { - mockTeamDailyActivityCall.mockImplementation((request: { groupBy?: string }) => - Promise.resolve( - request.groupBy === "tag" - ? tagSpendFor({ - "project:rag-v2": { spend: 0.25, api_requests: 1 }, - "cost-center:cc-42": { spend: 0.5, api_requests: 2 }, - }) - : mockSpendData, - ), - ); + it("shows one selected team's tag page in API order and loads the next page", async () => { const user = userEvent.setup(); render(); expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument(); @@ -657,14 +656,22 @@ describe("EntityUsage", () => { const card = await screen.findByTestId("team-tag-spend-card"); expect(within(card).getByRole("heading", { name: "Spend Per Tag" })).toBeInTheDocument(); const rows = (await within(card).findAllByRole("row")).slice(1); - expect(rows.map((row) => within(row).getAllByRole("cell")[0].textContent)).toEqual([ - "cost-center:cc-42", + expect(rows.slice(0, 2).map((row) => within(row).getAllByRole("cell")[0].textContent)).toEqual([ "project:rag-v2", + "cost-center:cc-42", ]); - expect(within(rows[0]).getByText("$0.5000")).toBeInTheDocument(); - expect(mockTeamDailyActivityCall).toHaveBeenCalledWith( - expect.objectContaining({ entityIds: ["team-1"], groupBy: "tag" }), + expect(within(card).getByText("Showing 1-50 of 60")).toBeInTheDocument(); + expect(mockDailyActivityTagPageCall).toHaveBeenCalledWith( + expect.objectContaining({ teamId: "team-1", offset: 0, limit: 50 }), ); + + await user.click(within(card).getByRole("button", { name: "Go to next page" })); + await waitFor(() => + expect(mockDailyActivityTagPageCall).toHaveBeenCalledWith( + expect.objectContaining({ teamId: "team-1", offset: 50, limit: 50 }), + ), + ); + expect(await within(card).findByText("Showing 51-60 of 60")).toBeInTheDocument(); }); it("asks for tag spend only for exactly one team in the team view", async () => { @@ -702,7 +709,7 @@ describe("EntityUsage", () => { }); expect(screen.queryByTestId("team-tag-spend-card")).not.toBeInTheDocument(); - expect(mockTeamDailyActivityCall).not.toHaveBeenCalledWith(expect.objectContaining({ groupBy: "tag" })); + expect(mockDailyActivityTagPageCall).not.toHaveBeenCalled(); }); it("should render with organization entity type and call organization API", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx index 1c5e1589722..44c2760c0f5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamTagSpendCard.tsx @@ -1,16 +1,17 @@ import type { ColumnDef } from "@tanstack/react-table"; import { Tags } from "lucide-react"; -import React, { useMemo } from "react"; +import React, { useEffect, useMemo, useState } from "react"; -import { DataTable } from "@/components/shared/DataTable"; +import { DataTable, DataTablePagination } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; -import { toDailyData, type DailyActivityRequest } from "@/components/UsagePage/dailyActivityApi"; +import type { + DailyActivityTagPageResponse, + DailyActivityTagSpendRow, +} from "@/components/UsagePage/dailyActivityApi"; -import { useAggregatedDailyActivity } from "../../hooks/useAggregatedDailyActivity"; import { Panel } from "../overview/Primitives"; import { ENTITY_API } from "./entityFetchFns"; -import { teamTagSpendRows, type TeamTagSpendRow } from "./teamTagSpend"; interface TeamTagSpendCardProps { accessToken: string | null; @@ -19,9 +20,26 @@ interface TeamTagSpendCardProps { teamId: string; } -const QUIET_HEADER = { headerClassName: "font-normal" }; +interface TagPageState { + scope: object; + page: number; + response: DailyActivityTagPageResponse | null; + failed: boolean; +} -const columns: ColumnDef[] = [ +interface TeamTagSpendPage { + page: number; + loading: boolean; + failed: boolean; + rows: DailyActivityTagSpendRow[]; + totalTags: number; + onPageChange: (nextPage: number) => void; +} + +const QUIET_HEADER = { headerClassName: "font-normal" }; +const PAGE_SIZE = 50; + +const columns: ColumnDef[] = [ { header: "Tag", accessorKey: "tag", @@ -60,21 +78,68 @@ const columns: ColumnDef[] = [ }, ]; -const TeamTagSpendCard: React.FC = ({ accessToken, startTime, endTime, teamId }) => { - const tagSpendRequest: DailyActivityRequest = { - accessToken: accessToken as string, - startTime: startTime as Date, - endTime: endTime as Date, - entityIds: [teamId], - groupBy: "tag", - }; - const { data, loading, failed } = useAggregatedDailyActivity({ - fetch: () => ENTITY_API.team.aggregated(tagSpendRequest), - enabled: Boolean(accessToken && startTime && endTime), - deps: [accessToken, startTime, endTime, teamId], - }); - const rows = useMemo(() => teamTagSpendRows(toDailyData(data)), [data]); +const useTeamTagSpendPage = ({ + accessToken, + startTime, + endTime, + teamId, +}: TeamTagSpendCardProps): TeamTagSpendPage => { + const startTimestamp = startTime?.getTime(); + const endTimestamp = endTime?.getTime(); + const requestScope = useMemo( + () => ({ hasAccessToken: accessToken !== null, endTimestamp, startTimestamp, teamId }), + [accessToken, endTimestamp, startTimestamp, teamId], + ); + const [pageState, setPageState] = useState<{ scope: object; page: number }>({ scope: {}, page: 0 }); + const [tagPageState, setTagPageState] = useState(null); + const page = pageState.scope === requestScope ? pageState.page : 0; + const currentScopeState = tagPageState?.scope === requestScope ? tagPageState : null; + const currentPageState = currentScopeState?.page === page ? currentScopeState : null; + const enabled = Boolean(accessToken && startTimestamp !== undefined && endTimestamp !== undefined); + useEffect(() => { + if (!accessToken || startTimestamp === undefined || endTimestamp === undefined) return; + + const controller = new AbortController(); + const tagPageRequest = { + accessToken, + startTime: new Date(startTimestamp), + endTime: new Date(endTimestamp), + teamId, + offset: page * PAGE_SIZE, + limit: PAGE_SIZE, + signal: controller.signal, + }; + void ENTITY_API.team.tagPage(tagPageRequest) + .then((response) => { + if (controller.signal.aborted) return; + const nextPageState: TagPageState = { scope: requestScope, page, response, failed: false }; + setTagPageState(nextPageState); + }) + .catch((error: unknown) => { + if (controller.signal.aborted) return; + console.error("Team tag spend page request failed:", error); + setTagPageState((current) => ({ + scope: requestScope, + page, + response: current?.scope === requestScope ? current.response : null, + failed: true, + })); + }); + return () => controller.abort(); + }, [accessToken, endTimestamp, page, requestScope, startTimestamp, teamId]); + + const loading = enabled && currentPageState === null; + const failed = currentPageState?.failed ?? false; + const rows = failed ? [] : (currentPageState?.response?.tags ?? []); + const totalTags = currentScopeState?.response?.total_tags ?? 0; + const onPageChange = (nextPage: number) => setPageState({ scope: requestScope, page: nextPage }); + + return { page, loading, failed, rows, totalTags, onPageChange }; +}; + +const TeamTagSpendCard: React.FC = (props) => { + const { page, loading, failed, rows, totalTags, onPageChange } = useTeamTagSpendPage(props); return (

@@ -91,6 +156,17 @@ const TeamTagSpendCard: React.FC = ({ accessToken, startT } size="compact" /> + {totalTags > 0 && ( + undefined} + pageSizeOptions={[PAGE_SIZE]} + isLoading={loading} + /> + )} ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityFetchFns.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityFetchFns.ts index 6dc80ef4591..4cd5b89ae36 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityFetchFns.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityFetchFns.ts @@ -4,6 +4,7 @@ import { dailyActivityKeyPageCall, dailyActivityKeySearchCall, dailyActivityModelTopKeysCall, + dailyActivityTagPageCall, } from "@/components/networking"; import type { EntityType } from "@/components/EntityUsageExport/types"; import type { @@ -12,6 +13,8 @@ import type { DailyActivityKeySearchResponse, DailyActivityKeyPageResponse, DailyActivityRequest, + DailyActivityTagPageRequest, + DailyActivityTagPageResponse, ExportFormat, ExportType, ModelTopKeysResponse, @@ -30,6 +33,10 @@ export interface EntityApi { exportRows(req: DailyActivityRequest, exportType: ExportType, format: ExportFormat): Promise; } +export interface TeamEntityApi extends EntityApi { + tagPage(req: DailyActivityTagPageRequest): Promise; +} + const entityApi = ( entity: DailyActivityEntity, defaults?: Pick, @@ -42,9 +49,12 @@ const entityApi = ( exportRows: (req, exportType, format) => dailyActivityExportCall(entity, { ...defaults, ...req }, exportType, format), }); -export const ENTITY_API: Record = { +export const ENTITY_API: Record & { team: TeamEntityApi } = { tag: entityApi("tag"), - team: entityApi("team", { excludeEntityIds: ["litellm-dashboard"] }), + team: { + ...entityApi("team", { excludeEntityIds: ["litellm-dashboard"] }), + tagPage: dailyActivityTagPageCall, + }, organization: entityApi("organization"), customer: entityApi("customer"), agent: entityApi("agent"), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.test.ts index 2d497162287..329845b0f08 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.test.ts @@ -1,74 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { DailyData, MetricWithMetadata, SpendMetrics } from "@/components/UsagePage/types"; - -import { singleTeamId, teamTagSpendRows } from "./teamTagSpend"; - -const metrics = (spend: number, api_requests: number, failed_requests = 0): SpendMetrics => ({ - spend, - prompt_tokens: 10 * api_requests, - completion_tokens: 5 * api_requests, - total_tokens: 15 * api_requests, - api_requests, - successful_requests: api_requests - failed_requests, - failed_requests, - cache_read_input_tokens: 0, - cache_creation_input_tokens: 0, -}); - -const entity = (spend: number, api_requests: number, failed_requests = 0): MetricWithMetadata => ({ - metrics: metrics(spend, api_requests, failed_requests), - metadata: {}, - api_key_breakdown: {}, -}); - -const day = (date: string, entities: Record): DailyData => ({ - date, - metrics: metrics(0, 0), - breakdown: { models: {}, model_groups: {}, mcp_servers: {}, providers: {}, api_keys: {}, entities }, -}); - -describe("teamTagSpendRows", () => { - it("sums each tag across days and orders by spend, highest first", () => { - const rows = teamTagSpendRows([ - day("2026-10-18", { "cost-center:cc-42": entity(0.5, 2), "project:rag-v2": entity(0.25, 1) }), - day("2026-10-19", { "cost-center:cc-42": entity(0.25, 1, 1), "env:prod": entity(1, 4) }), - ]); - - expect(rows).toEqual([ - { tag: "env:prod", spend: 1, api_requests: 4, successful_requests: 4, failed_requests: 0, total_tokens: 60 }, - { - tag: "cost-center:cc-42", - spend: 0.75, - api_requests: 3, - successful_requests: 2, - failed_requests: 1, - total_tokens: 45, - }, - { - tag: "project:rag-v2", - spend: 0.25, - api_requests: 1, - successful_requests: 1, - failed_requests: 0, - total_tokens: 15, - }, - ]); - }); - - it("drops tags with no spend and breaks spend ties by tag name", () => { - const rows = teamTagSpendRows([ - day("2026-10-19", { "b-tag": entity(0.5, 1), "a-tag": entity(0.5, 1), "free-tag": entity(0, 3) }), - ]); - - expect(rows.map((row) => row.tag)).toEqual(["a-tag", "b-tag"]); - }); - - it("returns no rows when the team sent no tagged spend", () => { - expect(teamTagSpendRows([day("2026-10-19", {})])).toEqual([]); - expect(teamTagSpendRows([])).toEqual([]); - }); -}); +import { singleTeamId } from "./teamTagSpend"; describe("singleTeamId", () => { it("is the team only when exactly one is selected", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.ts index 8a6ed8171cc..c160bf77d0a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamTagSpend.ts @@ -1,44 +1,2 @@ -import type { DailyData } from "@/components/UsagePage/types"; - -export interface TeamTagSpendRow { - tag: string; - spend: number; - api_requests: number; - successful_requests: number; - failed_requests: number; - total_tokens: number; -} - -const emptyRow = (tag: string): TeamTagSpendRow => ({ - tag, - spend: 0, - api_requests: 0, - successful_requests: 0, - failed_requests: 0, - total_tokens: 0, -}); - -/** One row per tag summed across days, zero-spend tags dropped, highest spend first (ties by tag name). */ -export const teamTagSpendRows = (days: readonly DailyData[]): TeamTagSpendRow[] => { - const byTag = new Map(); - for (const day of days) { - for (const [tag, { metrics }] of Object.entries(day.breakdown.entities ?? {})) { - const row = byTag.get(tag) ?? emptyRow(tag); - const summed: TeamTagSpendRow = { - tag, - spend: row.spend + metrics.spend, - api_requests: row.api_requests + metrics.api_requests, - successful_requests: row.successful_requests + metrics.successful_requests, - failed_requests: row.failed_requests + metrics.failed_requests, - total_tokens: row.total_tokens + metrics.total_tokens, - }; - byTag.set(tag, summed); - } - } - return [...byTag.values()] - .filter((row) => row.spend > 0) - .sort((a, b) => b.spend - a.spend || a.tag.localeCompare(b.tag)); -}; - export const singleTeamId = (selectedTeamIds: readonly string[]): string | null => selectedTeamIds.length === 1 ? selectedTeamIds[0] : null; diff --git a/ui/litellm-dashboard/src/components/UsagePage/dailyActivityApi.ts b/ui/litellm-dashboard/src/components/UsagePage/dailyActivityApi.ts index ab1d0ebbc6d..53fe474c88a 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/dailyActivityApi.ts +++ b/ui/litellm-dashboard/src/components/UsagePage/dailyActivityApi.ts @@ -1,4 +1,4 @@ -import type { components, operations } from "@/lib/http/schema"; +import type { components } from "@/lib/http/schema"; import type { BreakdownMetrics, DailyData, KeyMetadata, KeyMetricWithMetadata, MetricWithMetadata } from "./types"; export type DailyActivityEntity = "user" | "team" | "tag" | "organization" | "customer" | "agent"; @@ -11,11 +11,23 @@ export type KeyActivityRow = components["schemas"]["KeyActivityRow"]; export type KeySpendActivityRow = components["schemas"]["KeySpendActivityRow"]; export type DailyActivityKeySearchResponse = components["schemas"]["DailyActivityKeySearchResponse"]; export type DailyActivityKeyPageResponse = components["schemas"]["DailyActivityKeyPageResponse"]; +export type DailyActivityTagPageResponse = components["schemas"]["DailyActivityTagPageResponse"]; +export type DailyActivityTagSpendRow = components["schemas"]["TagSpendRow"]; export type ModelTopKeysResponse = components["schemas"]["ModelTopKeysResponse"]; export type CacheLeakageKeysResponse = components["schemas"]["CacheLeakageKeysResponse"]; export type UserActivityRow = components["schemas"]["UserActivityRow"]; export type DailyActivityUserPageResponse = components["schemas"]["DailyActivityUserPageResponse"]; +export interface DailyActivityTagPageRequest { + accessToken: string; + startTime: Date; + endTime: Date; + teamId: string; + offset: number; + limit: number; + signal?: AbortSignal; +} + export interface DailyActivityRequest { accessToken: string; startTime: Date; @@ -26,14 +38,8 @@ export interface DailyActivityRequest { model?: string | null; includeCurrentUtcDay?: boolean; apiKeyLimit?: number; - groupBy?: TeamGroupBy; } -type TeamAggregatedQuery = NonNullable< - operations["get_team_daily_activity_aggregated_team_daily_activity_aggregated_get"]["parameters"]["query"] ->; -export type TeamGroupBy = NonNullable; - export const EMPTY_DAILY_ACTIVITY_METADATA: DailyActivityMetadata = { has_more: false, page: 1, diff --git a/ui/litellm-dashboard/src/components/networking.dailyActivity.test.ts b/ui/litellm-dashboard/src/components/networking.dailyActivity.test.ts index 44f5b1d3017..2461e3caa03 100644 --- a/ui/litellm-dashboard/src/components/networking.dailyActivity.test.ts +++ b/ui/litellm-dashboard/src/components/networking.dailyActivity.test.ts @@ -6,6 +6,7 @@ import { dailyActivityExportCall, dailyActivityKeySearchCall, dailyActivityModelTopKeysCall, + dailyActivityTagPageCall, } from "./networking"; import type { DailyActivityEntity, DailyActivityRequest } from "./UsagePage/dailyActivityApi"; @@ -62,6 +63,7 @@ describe("dailyActivityAggregatedCall", () => { expect(url.searchParams.has("timezone")).toBe(true); expect(url.searchParams.has("page")).toBe(false); expect(url.searchParams.has("page_size")).toBe(false); + expect(url.searchParams.has("group_by")).toBe(false); }); it("omits the entity filter when no ids are given", async () => { @@ -123,16 +125,27 @@ describe("dailyActivityAggregatedCall", () => { expect(requestedUrl(mockFetch).searchParams.get("include_current_utc_day")).toBe("true"); }); - it("sends group_by on team reports only, and only when set", async () => { + it("GETs the team tags page with team, offset, and limit", async () => { const mockFetch = captureFetch(); - await dailyActivityAggregatedCall("team", req({ entityIds: ["team-a"], groupBy: "tag" })); - await dailyActivityAggregatedCall("team", req({ entityIds: ["team-a"] })); - await dailyActivityAggregatedCall("tag", req({ entityIds: ["tag-a"], groupBy: "tag" })); + const request = { + accessToken: "sk-key", + startTime: start, + endTime: end, + teamId: "team-a", + offset: 50, + limit: 50, + }; + await dailyActivityTagPageCall(request); - expect(requestedUrl(mockFetch, 0).searchParams.get("group_by")).toBe("tag"); - expect(requestedUrl(mockFetch, 1).searchParams.has("group_by")).toBe(false); - expect(requestedUrl(mockFetch, 2).searchParams.has("group_by")).toBe(false); + const url = requestedUrl(mockFetch); + expect(url.pathname).toBe("/team/daily/activity/aggregated/tags"); + expect(url.searchParams.get("team_ids")).toBe("team-a"); + expect(url.searchParams.get("start_date")).toBe("2025-01-05"); + expect(url.searchParams.get("end_date")).toBe("2025-01-31"); + expect(url.searchParams.get("offset")).toBe("50"); + expect(url.searchParams.get("limit")).toBe("50"); + expect(url.searchParams.has("group_by")).toBe(false); }); it("sends api_key_limit only when provided", async () => { diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index df609dd43b5..6bb6db14a05 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -144,6 +144,8 @@ import type { DailyActivityKeyPageResponse, DailyActivityKeySearchResponse, DailyActivityRequest, + DailyActivityTagPageRequest, + DailyActivityTagPageResponse, DailyActivityUserPageResponse, ExportFormat, ExportType, @@ -1381,7 +1383,6 @@ const dailyActivityQuery = ( : {}), timezone: new Date().getTimezoneOffset().toString(), include_current_utc_day: entity === "user" && req.includeCurrentUtcDay ? "true" : undefined, - group_by: entity === "team" ? req.groupBy : undefined, ...extra, }; }; @@ -1406,6 +1407,20 @@ export const dailyActivityKeyPageCall = ( query: dailyActivityQuery(entity, req, { offset, limit }), }); +export const dailyActivityTagPageCall = (req: DailyActivityTagPageRequest): Promise => { + const dailyActivityRequest: DailyActivityRequest = { + accessToken: req.accessToken, + startTime: req.startTime, + endTime: req.endTime, + entityIds: [req.teamId], + }; + return apiClient.get(`/team/daily/activity/aggregated/tags`, { + accessToken: req.accessToken, + query: dailyActivityQuery("team", dailyActivityRequest, { offset: req.offset, limit: req.limit }), + ...(req.signal ? { signal: req.signal } : {}), + }); +}; + export const dailyActivityKeySearchCall = ( entity: DailyActivityEntity, req: DailyActivityRequest,