diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index d4011f2e5db..9edd019ce87 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -61,6 +61,7 @@ vi.mock("@/components/networking", () => ({ dailyActivityModelTopKeysCall: vi.fn(), dailyActivityExportCall: vi.fn(), gatewayDailyActivityCall: vi.fn(), + requestErrorActivityCall: vi.fn(), tagListCall: vi.fn(), })); @@ -208,6 +209,7 @@ describe("UsagePage", () => { const mockDailyActivityKeyPageCall = vi.mocked(networking.dailyActivityKeyPageCall); const mockTagListCall = vi.mocked(networking.tagListCall); const mockGatewayDailyActivityCall = vi.mocked(networking.gatewayDailyActivityCall); + const mockRequestErrorActivityCall = vi.mocked(networking.requestErrorActivityCall); const mockUseCustomers = vi.mocked(useCustomers); const mockUseAgents = vi.mocked(useAgents); const mockUseAuthorized = vi.mocked(useAuthorized); @@ -370,11 +372,50 @@ describe("UsagePage", () => { // Counts deliberately unlike anything in mockSpendData: the gateway tile must be // readable as coming from /gateway/daily/activity and from nothing else. + const mockRequestErrorActivity = { + total_successful_requests: 900, + total_failed_requests: 100, + by_date: [ + { + date: "2025-01-01", + successful_requests: 900, + failed_requests: 100, + client_errors: 90, + server_errors: 10, + by_status_code: [ + { status_code: 429, failed_requests: 90 }, + { status_code: 500, failed_requests: 10 }, + ], + }, + ], + by_status_code: [ + { status_code: 429, failed_requests: 90 }, + { status_code: 500, failed_requests: 10 }, + ], + by_key: [ + { + id: "hash-1", + label: "prod-key", + api_requests: 400, + failed_requests: 100, + top_status_code: 429, + top_status_code_requests: 90, + }, + ], + by_team: [], + by_user: [], + by_model: [], + }; + const mockGatewayActivity = { total_successful_requests: 424242, total_failed_requests: 909, by_date: [{ date: "2025-01-01", successful_requests: 424242, failed_requests: 909 }], by_route: [{ category: "llm", route: "/chat/completions", successful_requests: 424242, failed_requests: 909 }], + by_status_code: [ + { status_code: 429, failed_requests: 3 }, + { status_code: 500, failed_requests: 2 }, + ], }; const defaultProps = { @@ -440,6 +481,8 @@ describe("UsagePage", () => { mockGatewayDailyActivityCall.mockClear(); mockUserDailyActivityAggregatedCall.mockResolvedValue(mockSpendData); mockGatewayDailyActivityCall.mockResolvedValue(mockGatewayActivity); + mockRequestErrorActivityCall.mockClear(); + mockRequestErrorActivityCall.mockResolvedValue(mockRequestErrorActivity); mockUseInfiniteUsers.mockReturnValue({ data: { pages: [ @@ -594,6 +637,8 @@ describe("UsagePage", () => { expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled(); }); expect(mockGatewayDailyActivityCall).not.toHaveBeenCalled(); + expect(mockRequestErrorActivityCall).not.toHaveBeenCalled(); + expect(screen.queryByRole("tab", { name: "Errors" })).not.toBeInTheDocument(); await waitFor(() => { expect(totalRequestsCell()).toHaveTextContent("1,500"); }); @@ -601,6 +646,24 @@ describe("UsagePage", () => { expect(screen.queryByTestId("gateway-requests-by-endpoint")).not.toBeInTheDocument(); }); + it("keeps failure analytics off the cost overview and on the Errors tab", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await waitFor(() => { + expect(mockRequestErrorActivityCall).toHaveBeenCalled(); + }); + expect(overview().queryByText(/Client errors \(4xx\)/)).not.toBeInTheDocument(); + expect(overview().queryByRole("button", { name: /Failed Requests/ })).not.toBeInTheDocument(); + + await user.click(screen.getByRole("tab", { name: "Errors" })); + const errorsTab = within(await screen.findByTestId("usage-errors-tab")); + expect(await errorsTab.findByText("10.0%")).toBeInTheDocument(); + expect(errorsTab.getByRole("list", { name: "Failed requests by status code" })).toHaveTextContent("429"); + const keys = errorsTab.getByRole("table", { name: "Virtual keys ranked by failed requests" }); + expect(within(keys).getAllByRole("row")[1]).toHaveTextContent("prod-key"); + }); + it("should display usage metrics and charts", async () => { renderWithProviders(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index e69f6f0afd8..5e9036520e6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -30,7 +30,7 @@ import EntityUsageExportModal from "@/components/EntityUsageExport"; import KeyActivityPanel from "@/components/UsagePage/components/KeyActivityPanel"; import { filterModelActivity } from "@/components/UsagePage/modelActivityFilter"; import { Team } from "@/components/key_team_helpers/key_list"; -import { gatewayDailyActivityCall, Organization, tagListCall } from "@/components/networking"; +import { gatewayDailyActivityCall, Organization, requestErrorActivityCall, tagListCall } from "@/components/networking"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; import { Tag } from "@/components/tag_management/types"; import UserAgentActivity from "@/components/user_agent_activity"; @@ -51,6 +51,8 @@ import { type FetchedGatewayActivity, type GatewayActivity, } from "./gatewayActivity"; +import ErrorsTab from "./errors/ErrorsTab"; +import type { RequestErrorActivity } from "./errors/errorsData"; import EndpointUsage from "./EndpointUsage/EndpointUsage"; import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage"; import ModelViewToggle, { ModelViewType } from "./ModelViewToggle"; @@ -230,6 +232,27 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const gatewayActivity = selectGatewayActivity(isAdmin, gatewayActivityData, currentGatewayRangeKey); + const [requestErrorData, setRequestErrorData] = useState | null>(null); + const requestErrorFetchIdRef = useRef(0); + useEffect(() => { + if (!isAdmin || !gatewayRequest) return; + const fetchId = ++requestErrorFetchIdRef.current; + requestErrorActivityCall(gatewayRequest.accessToken, gatewayRequest.startTime, gatewayRequest.endTime) + .then((data) => { + if (requestErrorFetchIdRef.current !== fetchId) return; + setRequestErrorData({ rangeKey: currentGatewayRangeKey, value: data as RequestErrorActivity }); + }) + .catch(() => { + if (requestErrorFetchIdRef.current !== fetchId) return; + setRequestErrorData({ rangeKey: currentGatewayRangeKey, value: null }); + }); + }, [isAdmin, gatewayRequest, currentGatewayRangeKey]); + const requestErrorsForRange = isAdmin ? requestErrorData : null; + const requestErrorsFailed = + requestErrorsForRange?.rangeKey === currentGatewayRangeKey && requestErrorsForRange.value === null; + const requestErrorActivity = + requestErrorsForRange?.rangeKey === currentGatewayRangeKey ? requestErrorsForRange.value : null; + const userSpendData = useMemo( () => ({ results: toDailyData(aggregatedRaw), @@ -364,6 +387,11 @@ const UsagePage: React.FC = ({ teams, organizations }) => { Endpoint Activity + {isAdmin && ( + + Errors + + )}
+
+ + + + ); +} + +function EmptyNote({ children }: { children: React.ReactNode }) { + return

{children}

; +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.test.ts new file mode 100644 index 00000000000..73ffcd5a6a5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it } from "vitest"; +import { + entityRows, + errorRate, + errorSeries, + rateByStatusRows, + errorSummary, + formatRate, + statusCodeLabel, + statusCodeRows, + type RequestErrorActivity, +} from "./errorsData"; + +const activity = (overrides: Partial = {}): RequestErrorActivity => ({ + total_successful_requests: 900, + total_failed_requests: 100, + by_date: [ + { + date: "2026-10-08", + successful_requests: 400, + failed_requests: 60, + client_errors: 50, + server_errors: 5, + by_status_code: [ + { status_code: 429, failed_requests: 40 }, + { status_code: 401, failed_requests: 10 }, + { status_code: 503, failed_requests: 5 }, + ], + }, + { + date: "2026-10-07", + successful_requests: 500, + failed_requests: 40, + client_errors: 40, + server_errors: 0, + by_status_code: [ + { status_code: 429, failed_requests: 30 }, + { status_code: 401, failed_requests: 10 }, + ], + }, + ], + by_status_code: [ + { status_code: 429, failed_requests: 70 }, + { status_code: 503, failed_requests: 5 }, + { status_code: 401, failed_requests: 20 }, + ], + by_key: [ + { + id: "hash-1", + label: "prod-key", + api_requests: 500, + failed_requests: 60, + top_status_code: 429, + top_status_code_requests: 55, + }, + { + id: "hash-2", + label: null, + api_requests: 0, + failed_requests: 0, + top_status_code: null, + top_status_code_requests: 0, + }, + ], + by_team: [], + by_user: [], + by_model: [ + { + id: "gpt-4o", + label: null, + api_requests: 1000, + failed_requests: 1, + top_status_code: 0, + top_status_code_requests: 1, + }, + ], + ...overrides, +}); + +describe("errorSummary", () => { + it("derives the rate and the status-class callouts from the edge totals", () => { + const expected = { + requests: 1000, + failed: 100, + rate: 10, + clientErrors: 90, + serverErrors: 5, + rateLimited: 70, + authFailures: 20, + }; + expect(errorSummary(activity())).toEqual(expected); + }); + + it("has no rate when nothing was served", () => { + const empty = activity({ total_successful_requests: 0, total_failed_requests: 0, by_status_code: [] }); + expect(errorSummary(empty).rate).toBeNull(); + }); +}); + +describe("errorSeries", () => { + it("orders days and stacks the top status codes, with the unrecorded remainder as Other", () => { + const series = errorSeries(activity()); + expect(series.codes).toEqual(["429", "401", "503", "Other"]); + expect(series.rows).toEqual([ + { + date: "2026-10-07", + requests: 540, + failed: 40, + rate: expect.closeTo(7.41, 2), + "429": 30, + "401": 10, + "503": 0, + Other: 0, + }, + { + date: "2026-10-08", + requests: 460, + failed: 60, + rate: expect.closeTo(13.04, 2), + "429": 40, + "401": 10, + "503": 5, + Other: 5, + }, + ]); + }); + + it("has no Other column when every failure has a recorded status", () => { + const complete = activity({ + by_date: [ + { + date: "2026-10-08", + successful_requests: 10, + failed_requests: 2, + client_errors: 2, + server_errors: 0, + by_status_code: [{ status_code: 429, failed_requests: 2 }], + }, + ], + by_status_code: [{ status_code: 429, failed_requests: 2 }], + }); + expect(errorSeries(complete).codes).toEqual(["429"]); + }); +}); + +describe("rateByStatusRows", () => { + it("turns each status code count into a share of that day's requests", () => { + const rows = rateByStatusRows(errorSeries(activity())); + expect(rows).toEqual([ + { + date: "2026-10-07", + requests: 540, + failed: 40, + rate: expect.closeTo(7.41, 2), + "429": expect.closeTo(5.56, 2), + "401": expect.closeTo(1.85, 2), + "503": 0, + Other: 0, + }, + { + date: "2026-10-08", + requests: 460, + failed: 60, + rate: expect.closeTo(13.04, 2), + "429": expect.closeTo(8.7, 2), + "401": expect.closeTo(2.17, 2), + "503": expect.closeTo(1.09, 2), + Other: expect.closeTo(1.09, 2), + }, + ]); + }); +}); + +describe("statusCodeRows", () => { + it("ranks codes by count, labels them and adds the unrecorded remainder", () => { + expect(statusCodeRows(activity())).toEqual([ + { status_code: 429, label: "Rate Limited", failed_requests: 70, share: 70 }, + { status_code: 401, label: "Unauthorized", failed_requests: 20, share: 20 }, + { status_code: 0, label: "Not recorded", failed_requests: 5, share: 5 }, + { status_code: 503, label: "Service Unavailable", failed_requests: 5, share: 5 }, + ]); + }); + + it("is empty when nothing failed", () => { + expect(statusCodeRows(activity({ total_failed_requests: 0, by_status_code: [] }))).toEqual([]); + }); +}); + +describe("entityRows", () => { + it("names rows by alias, computes the rate and describes the top status", () => { + expect(entityRows(activity(), "key")).toEqual([ + { id: "hash-1", name: "prod-key", requests: 500, failed: 60, rate: 12, topStatus: "429 · 55" }, + { id: "hash-2", name: "hash-2", requests: 0, failed: 0, rate: null, topStatus: null }, + ]); + }); + + it("switches source by kind and spells out an unrecorded top status", () => { + expect(entityRows(activity(), "team")).toEqual([]); + expect(entityRows(activity(), "model")[0].topStatus).toBe("No status · 1"); + }); +}); + +describe("rates", () => { + it.each([ + [0, 0, null], + [1, 1000, 0.1], + [50, 200, 25], + ])("errorRate(%s, %s)", (failed, total, expected) => { + expect(errorRate(failed, total)).toBe(expected); + }); + + it.each([ + [null, "—"], + [0, "0.0%"], + [0.04, "<0.1%"], + [12.345, "12.3%"], + ])("formatRate(%s)", (rate, expected) => { + expect(formatRate(rate)).toBe(expected); + }); +}); + +describe("statusCodeLabel", () => { + it.each([ + [0, "Not recorded"], + [401, "Unauthorized"], + [429, "Rate Limited"], + [503, "Service Unavailable"], + [418, "Client Error"], + [599, "Server Error"], + ] as const)("labels status %s", (code, label) => { + expect(statusCodeLabel(code)).toBe(label); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts new file mode 100644 index 00000000000..2374df0dd2c --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts @@ -0,0 +1,218 @@ +export interface RequestErrorDailyEntry { + date: string; + successful_requests: number; + failed_requests: number; + client_errors: number; + server_errors: number; + by_status_code: { status_code: number; failed_requests: number }[]; +} + +export interface RequestErrorEntityEntry { + id: string; + label: string | null; + api_requests: number; + failed_requests: number; + top_status_code: number | null; + top_status_code_requests: number; +} + +export interface RequestErrorActivity { + total_successful_requests: number; + total_failed_requests: number; + by_date: RequestErrorDailyEntry[]; + by_status_code: { status_code: number; failed_requests: number }[]; + by_key: RequestErrorEntityEntry[]; + by_team: RequestErrorEntityEntry[]; + by_user: RequestErrorEntityEntry[]; + by_model: RequestErrorEntityEntry[]; +} + +export const ERROR_ENTITY_KINDS = ["key", "team", "user", "model"] as const; +export type ErrorEntityKind = (typeof ERROR_ENTITY_KINDS)[number]; + +export const ERROR_ENTITY_LABEL: Record = { + key: { singular: "Virtual key", plural: "Virtual keys" }, + team: { singular: "Team", plural: "Teams" }, + user: { singular: "User", plural: "Users" }, + model: { singular: "Model", plural: "Models" }, +}; + +const STATUS_LABELS: Record = { + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 408: "Request Timeout", + 413: "Payload Too Large", + 422: "Unprocessable Entity", + 429: "Rate Limited", + 500: "Internal Server Error", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", +}; + +export const statusCodeLabel = (code: number): string => { + if (code === 0) return "Not recorded"; + const known = STATUS_LABELS[code]; + if (known) return known; + if (code >= 500) return "Server Error"; + if (code >= 400) return "Client Error"; + return "Error"; +}; + +export const errorRate = (failed: number, total: number): number | null => (total > 0 ? (failed / total) * 100 : null); + +export const formatRate = (rate: number | null): string => { + if (rate === null) return "—"; + if (rate > 0 && rate < 0.1) return "<0.1%"; + return `${rate.toFixed(1)}%`; +}; + +export interface ErrorSummary { + requests: number; + failed: number; + rate: number | null; + clientErrors: number; + serverErrors: number; + rateLimited: number; + authFailures: number; +} + +export const errorSummary = (activity: RequestErrorActivity): ErrorSummary => { + const requests = activity.total_successful_requests + activity.total_failed_requests; + const byCode = (predicate: (code: number) => boolean) => + activity.by_status_code + .filter((row) => predicate(row.status_code)) + .reduce((sum, row) => sum + row.failed_requests, 0); + return { + requests, + failed: activity.total_failed_requests, + rate: errorRate(activity.total_failed_requests, requests), + clientErrors: byCode((code) => code >= 400 && code <= 499), + serverErrors: byCode((code) => code >= 500 && code <= 599), + rateLimited: byCode((code) => code === 429), + authFailures: byCode((code) => code === 401 || code === 403), + }; +}; + +export interface ErrorSeriesRow extends Record { + date: string; + requests: number; + failed: number; + rate: number; +} + +export interface ErrorSeries { + rows: ErrorSeriesRow[]; + codes: string[]; +} + +export const ERROR_SERIES_MAX_CODES = 6; +export const ERROR_SERIES_OTHER = "Other"; + +const seriesKey = (code: number): string => (code === 0 ? "No status" : String(code)); + +export const errorSeries = (activity: RequestErrorActivity): ErrorSeries => { + const ranked = [...activity.by_status_code] + .filter((row) => row.failed_requests > 0) + .sort((a, b) => b.failed_requests - a.failed_requests || a.status_code - b.status_code); + const shown = ranked.slice(0, ERROR_SERIES_MAX_CODES).map((row) => row.status_code); + const codes = shown.map(seriesKey); + const needsOther = ranked.length > shown.length || activity.by_date.some(hasUnrecordedFailures); + const rows = [...activity.by_date] + .sort((a, b) => a.date.localeCompare(b.date)) + .map((day) => { + const requests = day.successful_requests + day.failed_requests; + const counts: Record = Object.fromEntries( + shown.map((code) => [ + seriesKey(code), + day.by_status_code.find((entry) => entry.status_code === code)?.failed_requests ?? 0, + ]), + ); + const recorded = Object.values(counts).reduce((sum, count) => sum + count, 0); + const other: Record = needsOther + ? { [ERROR_SERIES_OTHER]: Math.max(0, day.failed_requests - recorded) } + : {}; + return { + date: day.date, + requests, + failed: day.failed_requests, + rate: errorRate(day.failed_requests, requests) ?? 0, + ...counts, + ...other, + }; + }); + return { rows, codes: needsOther ? [...codes, ERROR_SERIES_OTHER] : codes }; +}; + +export type ErrorRateView = "total" | "status"; + +export const ERROR_RATE_VIEW_OPTIONS: readonly { value: ErrorRateView; label: string }[] = [ + { value: "total", label: "Total" }, + { value: "status", label: "By status code" }, +]; + +export const rateByStatusRows = (series: ErrorSeries): ErrorSeriesRow[] => + series.rows.map((row) => ({ + date: row.date, + requests: row.requests, + failed: row.failed, + rate: row.rate, + ...Object.fromEntries(series.codes.map((code) => [code, errorRate(Number(row[code] ?? 0), row.requests) ?? 0])), + })); + +const hasUnrecordedFailures = (day: RequestErrorDailyEntry): boolean => + day.failed_requests > day.by_status_code.reduce((sum, entry) => sum + entry.failed_requests, 0); + +export interface StatusCodeRow { + status_code: number; + label: string; + failed_requests: number; + share: number; +} + +export const statusCodeRows = (activity: RequestErrorActivity): StatusCodeRow[] => { + const recorded = activity.by_status_code.reduce((sum, row) => sum + row.failed_requests, 0); + const notRecorded = Math.max(0, activity.total_failed_requests - recorded); + const recordedRows = activity.by_status_code + .filter((row) => row.failed_requests > 0) + .map((row) => ({ + status_code: row.status_code, + label: statusCodeLabel(row.status_code), + failed_requests: row.failed_requests, + })); + const unrecordedRows = + notRecorded > 0 ? [{ status_code: 0, label: statusCodeLabel(0), failed_requests: notRecorded }] : []; + const total = activity.total_failed_requests || 1; + return [...recordedRows, ...unrecordedRows] + .sort((a, b) => b.failed_requests - a.failed_requests || a.status_code - b.status_code) + .map((row) => ({ ...row, share: (row.failed_requests / total) * 100 })); +}; + +export interface ErrorEntityRow { + id: string; + name: string; + requests: number; + failed: number; + rate: number | null; + topStatus: string | null; +} + +export const entityRows = (activity: RequestErrorActivity, kind: ErrorEntityKind): ErrorEntityRow[] => { + const source = { key: activity.by_key, team: activity.by_team, user: activity.by_user, model: activity.by_model }[ + kind + ]; + return source.map((entry) => ({ + id: entry.id, + name: entry.label ?? entry.id, + requests: entry.api_requests, + failed: entry.failed_requests, + rate: errorRate(entry.failed_requests, entry.api_requests), + topStatus: + entry.top_status_code === null + ? null + : `${entry.top_status_code === 0 ? "No status" : entry.top_status_code} · ${entry.top_status_code_requests.toLocaleString()}`, + })); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/gatewayActivity.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/gatewayActivity.ts index d527e8717f0..4362741bb7a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/gatewayActivity.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/gatewayActivity.ts @@ -15,6 +15,7 @@ export interface GatewayActivity { total_failed_requests: number; by_date: { date: string; successful_requests: number; failed_requests: number }[]; by_route: { category: string; route: string; successful_requests: number; failed_requests: number }[]; + by_status_code?: { status_code: number; failed_requests: number }[]; } /** A fetched result carrying the range key it was fetched for. */ @@ -32,11 +33,6 @@ export interface GatewayRouteBar extends Record { failed_requests: number; } -/** - * Identifies what a result was fetched for: the date range, plus any other - * input that changes the answer. The usage aggregate is scoped to a user, so - * two results covering the same dates still describe different numbers. - */ export const fetchedRangeKey = ( startTime: Date | null | undefined, endTime: Date | null | undefined, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 464a2ef45df..9603f85a3a3 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2465,6 +2465,27 @@ export const gatewayDailyActivityCall = async (accessToken: string, startTime: D } }; +export const requestErrorActivityCall = async (accessToken: string, startTime: Date, endTime: Date) => { + try { + const formatDate = (date: Date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + return await apiClient.get(`/gateway/errors/activity`, { + accessToken, + query: { + start_date: formatDate(startTime), + end_date: formatDate(endTime), + }, + }); + } catch (error) { + console.error("Failed to fetch request error activity:", error); + throw error; + } +}; + export const getPossibleUserRoles = async (accessToken: string) => { try { const data = (await apiClient.get(`/user/available_roles`, { accessToken })) as Record< diff --git a/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx b/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx index 2dc8747118d..ae653580ca5 100644 --- a/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx +++ b/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx @@ -23,6 +23,7 @@ export type LineChartProps> = { showTooltip?: boolean; customTooltip?: ChartTooltipComponent; connectNulls?: boolean; + showDots?: boolean; curveType?: LineChartCurveType; className?: string; style?: React.CSSProperties; @@ -42,6 +43,7 @@ export function LineChart>({ showTooltip = true, customTooltip, connectNulls = false, + showDots = false, curveType = "linear", className, style, @@ -88,7 +90,7 @@ export function LineChart>({ dataKey={category} stroke={fills[i]} strokeWidth={2} - dot={false} + dot={showDots} isAnimationActive={false} connectNulls={connectNulls} />