From 4e4a2e7ca209297837fc4c8e7bbb16ff8fb73e27 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 6 Dec 2025 16:14:33 -0800 Subject: [PATCH] Spend logs spend column enhancement --- .../src/components/new_usage.tsx | 2 +- .../src/components/view_logs/columns.tsx | 12 +++-- .../{tests => src}/utils/dataUtils.test.ts | 52 +++++++++++++++---- ui/litellm-dashboard/src/utils/dataUtils.ts | 18 ++++++- 4 files changed, 68 insertions(+), 16 deletions(-) rename ui/litellm-dashboard/{tests => src}/utils/dataUtils.test.ts (82%) diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index aa4b77b47a..9b6c7e48c5 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -424,7 +424,7 @@ const NewUsagePage: React.FC = ({
-
+
{all_admin_roles.includes(userRole || "") ? Global Usage : Your Usage} {all_admin_roles.includes(userRole || "") ? ( diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 08e8aaa9b0..2da1e83747 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -1,10 +1,10 @@ +import { getSpendString } from "@/utils/dataUtils"; import type { ColumnDef } from "@tanstack/react-table"; +import { Badge, Button } from "@tremor/react"; +import { Tooltip } from "antd"; import React, { useState } from "react"; import { getProviderLogoAndName } from "../provider_info_helpers"; -import { Tooltip } from "antd"; import { TimeCell } from "./time_cell"; -import { Button, Badge } from "@tremor/react"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; // Helper to get the appropriate logo URL const getLogoUrl = (row: LogEntry, provider: string) => { @@ -145,7 +145,11 @@ export const columns: ColumnDef[] = [ { header: "Cost", accessorKey: "spend", - cell: (info: any) => ${formatNumberWithCommas(info.getValue() || 0, 6)}, + cell: (info: any) => ( + + {getSpendString(info.getValue() || 0)} + + ), }, { header: "Duration (s)", diff --git a/ui/litellm-dashboard/tests/utils/dataUtils.test.ts b/ui/litellm-dashboard/src/utils/dataUtils.test.ts similarity index 82% rename from ui/litellm-dashboard/tests/utils/dataUtils.test.ts rename to ui/litellm-dashboard/src/utils/dataUtils.test.ts index c950cf87ce..24d9d7f1d8 100644 --- a/ui/litellm-dashboard/tests/utils/dataUtils.test.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import { copyToClipboard, formatNumberWithCommas, updateExistingKeys } from "../../src/utils/dataUtils"; +import { copyToClipboard, formatNumberWithCommas, getSpendString, updateExistingKeys } from "./dataUtils"; // Mock NotificationsManager vi.mock("../../src/components/molecules/notifications_manager", () => ({ @@ -10,7 +10,7 @@ vi.mock("../../src/components/molecules/notifications_manager", () => ({ })); // Import the mocked module -import NotificationsManager from "../../src/components/molecules/notifications_manager"; +import NotificationsManager from "../components/molecules/notifications_manager"; const mockNotificationsManager = vi.mocked(NotificationsManager); describe("dataUtils", () => { @@ -71,9 +71,41 @@ describe("dataUtils", () => { expect(formatNumberWithCommas(undefined)).toBe("-"); }); - it("should handle zero", () => { - expect(formatNumberWithCommas(0)).toBe("0"); - expect(formatNumberWithCommas(0, 2)).toBe("0.00"); + it("should handle zero and non-finite values", () => { + expect(formatNumberWithCommas(0)).toBe("-"); + expect(formatNumberWithCommas(0, 2)).toBe("-"); + expect(formatNumberWithCommas(Infinity)).toBe("-"); + expect(formatNumberWithCommas(Number.NaN)).toBe("-"); + }); + + it("should abbreviate large numbers", () => { + expect(formatNumberWithCommas(1_234_567, 1, true)).toBe("1.2M"); + expect(formatNumberWithCommas(12_345, 2, true)).toBe("12.35K"); + expect(formatNumberWithCommas(-1_200, 2, true)).toBe("-1.20K"); + }); + }); + + describe("getSpendString", () => { + it("should return '-' for null, undefined, zero, or non-finite", () => { + expect(getSpendString(null)).toBe("-"); + expect(getSpendString(undefined)).toBe("-"); + expect(getSpendString(0)).toBe("-"); + expect(getSpendString(Number.NaN)).toBe("-"); + }); + + it("should format spend with dollar sign", () => { + expect(getSpendString(1234.5, 2)).toBe("$1,234.50"); + expect(getSpendString(-2500, 0)).toBe("$-2,500"); + }); + + it("should return threshold string for very small values", () => { + expect(getSpendString(0.0000004, 6)).toBe("< $0.000001"); + expect(getSpendString(-0.0000004, 6)).toBe("< $0.000001"); + }); + + it("should respect custom decimals", () => { + expect(getSpendString(0.01234, 3)).toBe("$0.012"); + expect(getSpendString(999.9999, 1)).toBe("$1,000.0"); }); }); @@ -118,7 +150,7 @@ describe("dataUtils", () => { // Mock DOM methods const mockTextArea = { value: "", - style: {}, + style: {} as Record, setAttribute: vi.fn(), focus: vi.fn(), select: vi.fn(), @@ -149,7 +181,7 @@ describe("dataUtils", () => { // Mock DOM methods const mockTextArea = { value: "", - style: {}, + style: {} as Record, setAttribute: vi.fn(), focus: vi.fn(), select: vi.fn(), @@ -171,7 +203,7 @@ describe("dataUtils", () => { it("should set textarea properties correctly", async () => { const mockTextArea = { value: "", - style: {}, + style: {} as Record, setAttribute: vi.fn(), focus: vi.fn(), select: vi.fn(), @@ -213,7 +245,7 @@ describe("dataUtils", () => { it("should clean up textarea element after successful copy", async () => { const mockTextArea = { value: "", - style: {}, + style: {} as Record, setAttribute: vi.fn(), focus: vi.fn(), select: vi.fn(), @@ -237,7 +269,7 @@ describe("dataUtils", () => { document.execCommand = vi.fn().mockReturnValue(true); const mockTextArea = { value: "", - style: {}, + style: {} as Record, setAttribute: vi.fn(), focus: vi.fn(), select: vi.fn(), diff --git a/ui/litellm-dashboard/src/utils/dataUtils.ts b/ui/litellm-dashboard/src/utils/dataUtils.ts index e93f548667..f58ad07444 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.ts @@ -17,7 +17,7 @@ export const formatNumberWithCommas = ( decimals: number = 0, abbreviate: boolean = false, ): string => { - if (value === null || value === undefined || !Number.isFinite(value)) { + if (value === null || value === undefined || !Number.isFinite(value) || value === 0) { return "-"; } @@ -46,6 +46,22 @@ export const formatNumberWithCommas = ( return `${sign}${scaled.toLocaleString("en-US", opts)}${suffix}`; }; +export const getSpendString = (value: number | null | undefined, decimals: number = 6): string => { + if (value === null || value === undefined || !Number.isFinite(value) || value === 0) { + return "-"; + } + + const formatted = formatNumberWithCommas(value, decimals); + const numericFormatted = Number(formatted.replace(/,/g, "")); + + if (numericFormatted === 0) { + const threshold = (1 / 10 ** decimals).toFixed(decimals); + return `< $${threshold}`; + } + + return `$${formatted}`; +}; + export const copyToClipboard = async ( text: string | null | undefined, messageText: string = "Copied to clipboard",