From 2b77e8c4dba4567676c46bc19717877908ce91c1 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Thu, 23 Jul 2026 15:38:42 -0700 Subject: [PATCH 1/2] fix(ui): keep cache leakage time range picker inline at narrow widths The card header used flex-wrap, so the date picker was the element that gave way when the row ran out of room; at higher browser zoom it dropped onto its own line under the description. Pin the picker with shrink-0 and let the title/description block shrink instead (min-w-0), so the copy wraps to a second line and the picker stays on the right. Below md the header stacks, since a 300px input plus its nowrap label leaves nothing usable beside it. --- .../cost-optimization/_components/CacheLeakageCard.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx index bd0ecea9483..4f1cfc49569 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CacheLeakageCard.tsx @@ -102,8 +102,8 @@ const CacheLeakageCard: React.FC = ({ activity }) => { -
-
+
+
Cache leakage by {dimension === "model" ? "model" : "virtual key"}

{subject} sending large volumes of uncached input with a low cache hit rate are likely missing prompt @@ -111,7 +111,9 @@ const CacheLeakageCard: React.FC = ({ activity }) => { {dimension === "model" ? " Limited to Anthropic (Claude) models, which support prompt caching." : ""}

- +
+ +
Date: Thu, 23 Jul 2026 14:55:12 -0700 Subject: [PATCH 2/2] feat(ui): show in the log drawer and session sidebar when an auto-router served a request The dashboard already receives the requested model name as model_group on every spend-log row, but LogEntry dropped the field, so nothing distinguished an auto-routed request from a direct one. Surface it precisely rather than by comparing requested against resolved: model_group differs from model for plain aliases and wildcard deployments too, so a bare mismatch tags almost every row and identifies nothing. The indication is driven instead by which deployments are auto-routers, resolved from every page of /v2/model/info and shared through context. The request drawer header names the router in a badge next to the provider; the session sidebar swaps the entry's leading icon. Rows that no auto-router served render exactly as before. --- ui/litellm-dashboard/eslint-suppressions.json | 2 +- .../hooks/models/useModels.test.ts | 162 ++++++++++++++++++ .../app/(dashboard)/hooks/models/useModels.ts | 60 +++++++ .../shared/table_cells/AutoRouterTag.test.tsx | 62 +++++++ .../shared/table_cells/AutoRouterTag.tsx | 54 ++++++ .../components/shared/table_cells/index.ts | 7 + .../LogDetailsDrawer/DrawerHeader.tsx | 5 + .../LogDetailsDrawer.test.tsx | 46 +++++ .../LogDetailsDrawer/LogDetailsDrawer.tsx | 21 ++- .../components/view_logs/RequestLogsPanel.tsx | 5 +- .../src/components/view_logs/columns.tsx | 1 + 11 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.test.tsx create mode 100644 ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 4fa5528aab8..790965c119e 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -4318,7 +4318,7 @@ }, "src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": { "no-nested-ternary": { - "count": 3 + "count": 2 }, "no-restricted-imports": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts index 0bcc37d1389..f83ebd2622a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts @@ -3,13 +3,17 @@ import { renderHook, waitFor } from "@testing-library/react"; import React, { ReactNode } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { + isAutoRouterDeployment, + selectAutoRouterModelGroups, useAllProxyModels, + useAutoRouterModelGroups, useInfiniteModelInfo, useModelHub, useModelsInfo, useSelectedTeamModels, useUserModels, type AllProxyModelsResponse, + type AutoRouterCandidateDeployment, type PaginatedModelInfoResponse, type ProxyModel, } from "./useModels"; @@ -918,3 +922,161 @@ describe("useInfiniteModelInfo", () => { expect(modelInfoCall).not.toHaveBeenCalled(); }); }); + +describe("isAutoRouterDeployment", () => { + const cases: [string, string | null | undefined, boolean][] = [ + ["base semantic auto-router", "auto_router/my_router", true], + ["complexity router", "auto_router/complexity_router", true], + ["adaptive router", "auto_router/adaptive_router", true], + ["quality router", "auto_router/quality_router", true], + ["plain provider alias", "anthropic/claude-haiku-4-5", false], + ["wildcard deployment", "openai/*", false], + ["name merely containing the prefix", "openai/auto_router/nope", false], + ["missing model", undefined, false], + ["null model", null, false], + ]; + + it.each(cases)("returns %s -> %s", (_label, litellmParamsModel, expected) => { + expect(isAutoRouterDeployment({ model_name: "some-group", litellm_params: { model: litellmParamsModel } })).toBe( + expected, + ); + }); + + it("returns false when litellm_params is absent", () => { + expect(isAutoRouterDeployment({ model_name: "some-group" })).toBe(false); + }); +}); + +describe("selectAutoRouterModelGroups", () => { + it("keeps only the public model_name of auto-router deployments", () => { + const deployments: AutoRouterCandidateDeployment[] = [ + { model_name: "smart-router", litellm_params: { model: "auto_router/complexity_router" } }, + { model_name: "claude-haiku", litellm_params: { model: "anthropic/claude-haiku-4-5" } }, + { model_name: "claude-sonnet", litellm_params: { model: "anthropic/claude-sonnet-4-5" } }, + { model_name: "cheap-router", litellm_params: { model: "auto_router/adaptive_router" } }, + ]; + + expect(selectAutoRouterModelGroups(deployments)).toEqual(new Set(["smart-router", "cheap-router"])); + }); + + it("drops auto-router deployments that have no public model_name", () => { + expect( + selectAutoRouterModelGroups([{ model_name: "", litellm_params: { model: "auto_router/complexity_router" } }]), + ).toEqual(new Set()); + }); + + it("returns an empty set for an empty model list", () => { + expect(selectAutoRouterModelGroups([])).toEqual(new Set()); + }); +}); + +describe("useAutoRouterModelGroups", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + vi.clearAllMocks(); + mockUseAuthorized.mockReturnValue({ + accessToken: "test-access-token", + userId: "test-user-id", + userRole: "Admin", + token: "test-token", + userEmail: "test@example.com", + premiumUser: false, + disabledPersonalKeyCreation: null, + showSSOBanner: false, + }); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("resolves the set of auto-router model groups from the deployment list", async () => { + (modelInfoCall as any).mockResolvedValue({ + data: [ + { model_name: "smart-router", litellm_params: { model: "auto_router/complexity_router" } }, + { model_name: "claude-haiku", litellm_params: { model: "anthropic/claude-haiku-4-5" } }, + ], + total_count: 2, + current_page: 1, + total_pages: 1, + size: 1000, + }); + + const { result } = renderHook(() => useAutoRouterModelGroups(), { wrapper }); + + await waitFor(() => expect(result.current.size).toBe(1)); + expect(result.current.has("smart-router")).toBe(true); + expect(result.current.has("claude-haiku")).toBe(false); + }); + + it("requests a single large page when the proxy reports only one page of deployments", async () => { + (modelInfoCall as any).mockResolvedValue({ + data: [], + total_count: 0, + current_page: 1, + total_pages: 1, + size: 1000, + }); + + renderHook(() => useAutoRouterModelGroups(), { wrapper }); + + await waitFor(() => expect(modelInfoCall).toHaveBeenCalled()); + expect(modelInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", 1, 1000); + expect(modelInfoCall).toHaveBeenCalledTimes(1); + }); + + it("follows total_pages so an auto-router past the first page is still found", async () => { + (modelInfoCall as any).mockImplementation((_t: string, _u: string, _r: string, page: number) => { + if (page === 1) { + return Promise.resolve({ + data: [{ model_name: "claude-haiku", litellm_params: { model: "anthropic/claude-haiku-4-5" } }], + total_count: 3, + current_page: 1, + total_pages: 3, + size: 1000, + }); + } + if (page === 2) { + return Promise.resolve({ + data: [{ model_name: "claude-sonnet", litellm_params: { model: "anthropic/claude-sonnet-4-5" } }], + total_count: 3, + current_page: 2, + total_pages: 3, + size: 1000, + }); + } + return Promise.resolve({ + data: [{ model_name: "late-router", litellm_params: { model: "auto_router/complexity_router" } }], + total_count: 3, + current_page: 3, + total_pages: 3, + size: 1000, + }); + }); + + const { result } = renderHook(() => useAutoRouterModelGroups(), { wrapper }); + + await waitFor(() => expect(result.current.size).toBe(1)); + expect(result.current.has("late-router")).toBe(true); + expect(modelInfoCall).toHaveBeenCalledTimes(3); + expect(modelInfoCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin", 3, 1000); + }); + + it("returns an empty set before the model list resolves", () => { + (modelInfoCall as any).mockReturnValue(new Promise(() => {})); + + const { result } = renderHook(() => useAutoRouterModelGroups(), { wrapper }); + + expect(result.current.size).toBe(0); + }); + + it("returns an empty set when the model list request fails", async () => { + (modelInfoCall as any).mockRejectedValue(new Error("boom")); + + const { result } = renderHook(() => useAutoRouterModelGroups(), { wrapper }); + + await waitFor(() => expect(modelInfoCall).toHaveBeenCalled()); + expect(result.current.size).toBe(0); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index 113d1616e62..ad5e3c91ec3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -24,6 +24,7 @@ export interface PaginatedModelInfoResponse { const modelKeys = createQueryKeys("models"); const modelHubKeys = createQueryKeys("modelHub"); +const autoRouterKeys = createQueryKeys("autoRouterModelGroups"); const allProxyModelsKeys = createQueryKeys("allProxyModels"); const selectedTeamModelsKeys = createQueryKeys("selectedTeamModels"); const infiniteModelKeys = createQueryKeys("infiniteModels"); @@ -59,6 +60,65 @@ export const useModelsInfo = ( }); }; +const AUTO_ROUTER_MODEL_PREFIX = "auto_router/"; +const AUTO_ROUTER_LOOKUP_PAGE_SIZE = 1000; +const NO_AUTO_ROUTERS: ReadonlySet = new Set(); + +export interface AutoRouterCandidateDeployment { + model_name?: string | null; + litellm_params?: { model?: string | null } | null; +} + +export const isAutoRouterDeployment = (deployment: AutoRouterCandidateDeployment): boolean => + Boolean(deployment?.litellm_params?.model?.startsWith(AUTO_ROUTER_MODEL_PREFIX)); + +export const selectAutoRouterModelGroups = (deployments: AutoRouterCandidateDeployment[]): ReadonlySet => + new Set( + deployments + .filter(isAutoRouterDeployment) + .map((deployment) => deployment.model_name) + .filter((modelName): modelName is string => Boolean(modelName)), + ); + +const fetchAllModelDeployments = async ( + accessToken: string, + userId: string, + userRole: string, +): Promise => { + const firstPage: PaginatedModelInfoResponse = await modelInfoCall( + accessToken, + userId, + userRole, + 1, + AUTO_ROUTER_LOOKUP_PAGE_SIZE, + ); + const totalPages = firstPage?.total_pages ?? 1; + const remainingPages = await Promise.all( + Array.from({ length: Math.max(0, totalPages - 1) }, (_unused, index) => + modelInfoCall(accessToken, userId, userRole, index + 2, AUTO_ROUTER_LOOKUP_PAGE_SIZE), + ), + ); + return [firstPage, ...remainingPages].flatMap( + (page: PaginatedModelInfoResponse) => page?.data ?? [], + ) as AutoRouterCandidateDeployment[]; +}; + +export const useAutoRouterModelGroups = (): ReadonlySet => { + const { accessToken, userId, userRole } = useAuthorized(); + const { data } = useQuery>({ + queryKey: autoRouterKeys.list({ + filters: { + ...(userId && { userId }), + ...(userRole && { userRole }), + }, + }), + queryFn: async () => await fetchAllModelDeployments(accessToken!, userId!, userRole!), + enabled: Boolean(accessToken && userId && userRole), + select: selectAutoRouterModelGroups, + }); + return data ?? NO_AUTO_ROUTERS; +}; + export const useModelHub = () => { const { accessToken } = useAuthorized(); return useQuery({ diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.test.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.test.tsx new file mode 100644 index 00000000000..6c00f16be42 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.test.tsx @@ -0,0 +1,62 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +import { AutoRouterModelGroupsProvider, AutoRouterTag } from "./AutoRouterTag"; + +vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ + useAutoRouterModelGroups: vi.fn(), +})); + +import { useAutoRouterModelGroups } from "@/app/(dashboard)/hooks/models/useModels"; + +const mockUseAutoRouterModelGroups = vi.mocked(useAutoRouterModelGroups); + +const renderInProvider = (ui: React.ReactNode) => + render({ui}); + +describe("AutoRouterTag", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("names the router that served the request", () => { + mockUseAutoRouterModelGroups.mockReturnValue(new Set(["smart-router"])); + + renderInProvider(); + + const tag = screen.getByTitle('Routed by auto-router "smart-router"'); + expect(tag).toHaveTextContent("smart-router"); + expect(tag.querySelector("svg")).not.toBeNull(); + }); + + it("does not tag a plain alias whose model group differs from the resolved model", () => { + mockUseAutoRouterModelGroups.mockReturnValue(new Set(["smart-router"])); + + renderInProvider(); + + expect(screen.queryByText("claude-haiku")).not.toBeInTheDocument(); + }); + + it("renders nothing while the model list is unavailable, so a routed row is never mislabelled", () => { + mockUseAutoRouterModelGroups.mockReturnValue(new Set()); + + const { container } = renderInProvider(); + + expect(container).toBeEmptyDOMElement(); + }); + + it.each([[undefined], [null], [""]])("renders nothing when the row carries no model group (%s)", (modelGroup) => { + mockUseAutoRouterModelGroups.mockReturnValue(new Set(["smart-router"])); + + const { container } = renderInProvider(); + + expect(container).toBeEmptyDOMElement(); + }); + + it("renders nothing outside a provider instead of requiring a QueryClient", () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + expect(mockUseAutoRouterModelGroups).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.tsx b/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.tsx new file mode 100644 index 00000000000..3cc833e11e4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/table_cells/AutoRouterTag.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { createContext, useContext, type ReactNode } from "react"; +import { Waypoints } from "lucide-react"; + +import { useAutoRouterModelGroups } from "@/app/(dashboard)/hooks/models/useModels"; +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/cva.config"; + +const NO_AUTO_ROUTERS: ReadonlySet = new Set(); + +const AutoRouterModelGroupsContext = createContext>(NO_AUTO_ROUTERS); + +export function AutoRouterModelGroupsProvider({ children }: { children: ReactNode }) { + const autoRouterModelGroups = useAutoRouterModelGroups(); + + return ( + + {children} + + ); +} + +export function useIsAutoRoutedModelGroup(modelGroup?: string | null): boolean { + const autoRouterModelGroups = useContext(AutoRouterModelGroupsContext); + + return Boolean(modelGroup) && autoRouterModelGroups.has(modelGroup as string); +} + +export function AutoRouterIcon({ size = 12, className }: { size?: number; className?: string }) { + return ; +} + +export interface AutoRouterTagProps { + modelGroup?: string | null; + className?: string; +} + +export function AutoRouterTag({ modelGroup, className }: AutoRouterTagProps) { + const isAutoRouted = useIsAutoRoutedModelGroup(modelGroup); + + if (!isAutoRouted) return null; + + return ( + + + {modelGroup} + + ); +} diff --git a/ui/litellm-dashboard/src/components/shared/table_cells/index.ts b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts index 9fdd04d169c..99b54f9aad0 100644 --- a/ui/litellm-dashboard/src/components/shared/table_cells/index.ts +++ b/ui/litellm-dashboard/src/components/shared/table_cells/index.ts @@ -1,3 +1,10 @@ +export { + AutoRouterTag, + AutoRouterIcon, + AutoRouterModelGroupsProvider, + useIsAutoRoutedModelGroup, + type AutoRouterTagProps, +} from "./AutoRouterTag"; export { CellTooltip } from "./cell_tooltip"; export { DateCell, formatCellDate, formatFullTimestamp, type DatePrecision } from "./date_cell"; export { IdCell, type IdCellVariant } from "./id_cell"; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx index 8c0b679a846..0bcb9050d7d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -2,6 +2,7 @@ import { Button, Space, Tag, Tooltip, Typography } from "antd"; import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; import moment from "moment"; import { LogEntry } from "../columns"; +import { AutoRouterTag } from "@/components/shared/table_cells"; import { getProviderLogoAndName } from "../../provider_info_helpers"; import { DRAWER_HEADER_PADDING, @@ -57,6 +58,7 @@ export function DrawerHeader({ {/* Row 0: Model + Provider with Logo */} @@ -80,10 +82,12 @@ export function DrawerHeader({ */ function ModelProviderSection({ model, + modelGroup, providerLogo, providerName, }: { model: string; + modelGroup?: string; providerLogo?: string; providerName?: string; }) { @@ -109,6 +113,7 @@ function ModelProviderSection({ {providerName} )} + ); diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx index 5a49cccec70..b913f7c8d21 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.test.tsx @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest"; import { LogDetailsDrawer } from "./LogDetailsDrawer"; import { sessionSpendLogsCall } from "../../networking"; import { LogEntry } from "../columns"; +import { AutoRouterModelGroupsProvider } from "@/components/shared/table_cells"; vi.mock("../../networking", () => ({ sessionSpendLogsCall: vi.fn(), @@ -22,6 +23,10 @@ vi.mock("./DrawerHeader", () => ({ DrawerHeader: () => null, })); +vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ + useAutoRouterModelGroups: vi.fn(() => new Set(["smart-router"])), +})); + const makeLog = (overrides: Partial): LogEntry => ({ request_id: "req", api_key: "", @@ -118,3 +123,44 @@ describe("LogDetailsDrawer session sidebar sorting", () => { await waitFor(() => expect(sidebarEventNames()).toEqual(["tool-early", "llm-late", "llm-early", "tool-late"])); }); }); + +describe("LogDetailsDrawer session sidebar auto-router icon", () => { + const routedSessionLogs = [ + makeLog({ request_id: "routed", model: "claude-opus-4-8", model_group: "smart-router" }), + makeLog({ request_id: "direct", model: "claude-haiku-4-5", model_group: "claude-haiku" }), + ]; + + const renderRoutedSession = () => { + vi.mocked(sessionSpendLogsCall).mockResolvedValue({ data: routedSessionLogs, total: 2, total_pages: 1 }); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + {}} logEntry={null} sessionId="session-1" accessToken="token" /> + + , + ); + }; + + const rowFor = (label: string): HTMLElement => { + const row = Array.from(document.body.querySelectorAll("button")).find((button) => + button.textContent?.includes(label), + ); + if (!row) throw new Error(`no sidebar row for ${label}`); + return row; + }; + + it("marks the auto-routed entry with the router icon and leaves a direct call on the default icon", async () => { + renderRoutedSession(); + + await waitFor(() => expect(screen.queryByText("claude-opus-4-8")).not.toBeNull()); + + const routedRow = rowFor("claude-opus-4-8"); + const directRow = rowFor("claude-haiku-4-5"); + + expect(routedRow.querySelector(".lucide-waypoints")).not.toBeNull(); + expect(routedRow.querySelector(".lucide-sparkles")).toBeNull(); + expect(directRow.querySelector(".lucide-sparkles")).not.toBeNull(); + expect(directRow.querySelector(".lucide-waypoints")).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index cc087a611b0..cdf8e0b1c60 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -3,6 +3,7 @@ import { Button, Drawer, Segmented } from "antd"; import { CheckOutlined, CopyOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons"; import { Bot, Sparkles, Wrench } from "lucide-react"; import { LogEntry } from "../columns"; +import { AutoRouterIcon, useIsAutoRoutedModelGroup } from "@/components/shared/table_cells"; import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants"; import { getEventDisplayName } from "../utils"; import { DrawerHeader } from "./DrawerHeader"; @@ -46,9 +47,17 @@ interface TraceEventRowProps { onClick: () => void; } +const TRACE_EVENT_ICON_CLASS = "text-slate-500 shrink-0"; + +function TraceEventIcon({ callType, isAutoRouted }: { callType: string; isAutoRouted: boolean }) { + if (MCP_CALL_TYPES.includes(callType)) return ; + if (AGENT_CALL_TYPES.includes(callType)) return ; + if (isAutoRouted) return ; + return ; +} + function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) { - const isMcp = MCP_CALL_TYPES.includes(row.call_type); - const isAgent = AGENT_CALL_TYPES.includes(row.call_type); + const isAutoRouted = useIsAutoRoutedModelGroup(row.model_group); const durationValue = row.request_duration_ms != null ? (row.request_duration_ms / 1000).toFixed(3) @@ -65,13 +74,7 @@ function TraceEventRow({ row, isSelected, onClick }: TraceEventRowProps) { onClick={onClick} >
- {isMcp ? ( - - ) : isAgent ? ( - - ) : ( - - )} + {getEventDisplayName(row.call_type, row.model)} diff --git a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx index 05044dda791..100b3422435 100644 --- a/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/RequestLogsPanel.tsx @@ -5,6 +5,7 @@ import type { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } fr import moment from "moment"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { AutoRouterModelGroupsProvider } from "@/components/shared/table_cells"; import { internalUserRoles } from "../../utils/roles"; import type { KeyResponse } from "../key_team_helpers/key_list"; import { keyInfoV1Call } from "../networking"; @@ -202,7 +203,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, } return ( - <> +

Request Logs

@@ -259,6 +260,6 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, onSelectLog={setSelectedLog} startTime={moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")} /> - +
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index a4b8015892a..d4f784bf165 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -16,6 +16,7 @@ export type LogEntry = { team_id: string; model: string; model_id: string; + model_group?: string; api_base?: string; call_type: string; spend: number;