From b5070408e7edd5211dadf89495db3f813178c905 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 18 Sep 2026 15:48:56 +0000 Subject: [PATCH] refactor(ui): move per-second cost formatter to dataUtils and type transformModelData input Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../components/ModelsTableColumns.tsx | 3 +- .../utils/modelDataTransformer.ts | 14 ++--- .../models/ModelPricingSummary.test.tsx | 57 +++++++++++++++++++ .../molecules/models/ModelPricingSummary.tsx | 10 +++- .../src/utils/dataUtils.test.ts | 20 ++++++- ui/litellm-dashboard/src/utils/dataUtils.ts | 3 + 6 files changed, 94 insertions(+), 13 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx index a8af629b29f..cbc31747688 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx @@ -3,7 +3,6 @@ import { ColumnDef } from "@tanstack/react-table"; import { Copy, Info, Loader2, Pencil, RefreshCw, Trash2 } from "lucide-react"; -import { formatPerSecondCost } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { ModelData } from "@/components/model_dashboard/types"; import { DataTableSortHeader } from "@/components/shared/DataTable"; @@ -13,7 +12,7 @@ import { Button } from "@/components/ui/button"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; import { Switch } from "@/components/ui/switch"; import { getDisplayModelName } from "@/components/view_model/model_name_display"; -import { copyToClipboard } from "@/utils/dataUtils"; +import { copyToClipboard, formatPerSecondCost } from "@/utils/dataUtils"; export const MODEL_ID_COLUMN_ID = "model_info_id"; export const MODEL_NAME_COLUMN_ID = "model_name"; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts index 5dce4cb8b65..ef76a506a18 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts @@ -8,9 +8,6 @@ export const perSecondCostTiers = (modelInfo: Record | null | u return resolution !== undefined && typeof value === "number" ? [{ resolution, cost: value }] : []; }); -export const formatPerSecondCost = (cost: number): string => - `$${cost.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 })}/s`; - interface RawLitellmParams { model?: string; custom_llm_provider?: string; @@ -50,8 +47,8 @@ const resolveProvider = ( const transformModel = (rawModel: RawModel, getProviderFromModel: (model: string) => string) => { const model: RawModel = JSON.parse(JSON.stringify(rawModel)); - const litellmParams = model?.litellm_params; - const modelInfo = model?.model_info; + const litellmParams = model.litellm_params; + const modelInfo = model.model_info; return { ...model, @@ -70,8 +67,11 @@ const transformModel = (rawModel: RawModel, getProviderFromModel: (model: string }; }; -export const transformModelData = (rawModelData: any, getProviderFromModel: (model: string) => string) => { +export const transformModelData = ( + rawModelData: { data?: RawModel[] | null } | null | undefined, + getProviderFromModel: (model: string) => string, +) => { if (!rawModelData?.data) return { data: [] }; - return { data: rawModelData.data.map((rawModel: RawModel) => transformModel(rawModel, getProviderFromModel)) }; + return { data: rawModelData.data.map((rawModel) => transformModel(rawModel, getProviderFromModel)) }; }; diff --git a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx new file mode 100644 index 00000000000..5832f4cd17b --- /dev/null +++ b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { ModelPricingSummary } from "./ModelPricingSummary"; + +const tokenPriced = { input_cost: "1.50" as unknown as number, output_cost: "2.00" as unknown as number }; + +describe("ModelPricingSummary", () => { + it("shows per-million-token rates for a token priced model", () => { + render(); + expect(screen.getByText("Input: $1.50/1M tokens")).toBeInTheDocument(); + expect(screen.getByText("Output: $2.00/1M tokens")).toBeInTheDocument(); + }); + + it("hides $0.00 token rates and shows per-second tiers for a per-second priced model", () => { + render( + , + ); + expect(screen.getByText("Output: $0.10/s")).toBeInTheDocument(); + expect(screen.getByText("Output (1080p): $0.12/s")).toBeInTheDocument(); + expect(screen.getByText("Output (4k): $0.30/s")).toBeInTheDocument(); + expect(screen.queryByText(/1M tokens/)).not.toBeInTheDocument(); + }); + + it("keeps a positive token rate next to the per-second rate", () => { + render( + , + ); + expect(screen.getByText("Input: $0.60/1M tokens")).toBeInTheDocument(); + expect(screen.getByText("Output: $0.015/s")).toBeInTheDocument(); + expect(screen.queryByText("Output: $0.00/1M tokens")).not.toBeInTheDocument(); + }); + + it("renders a dash when the model has no pricing at all", () => { + render( + , + ); + expect(screen.getByText("-")).toBeInTheDocument(); + expect(screen.queryByText(/\$/)).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.tsx b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.tsx index bf0c2b6806c..10b37c6100c 100644 --- a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.tsx +++ b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.tsx @@ -1,5 +1,5 @@ -import { formatPerSecondCost } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer"; import { ModelData } from "@/components/model_dashboard/types"; +import { formatPerSecondCost } from "@/utils/dataUtils"; type PricingFields = Pick< ModelData, @@ -9,8 +9,12 @@ type PricingFields = Pick< export function ModelPricingSummary({ model }: { model: PricingFields }) { const perSecond = model.output_cost_per_second; const hasPerSecond = perSecond != null; - const showInput = !hasPerSecond || Number(model.input_cost) > 0; - const showOutput = !hasPerSecond || Number(model.output_cost) > 0; + const showInput = model.input_cost != null && (!hasPerSecond || Number(model.input_cost) > 0); + const showOutput = model.output_cost != null && (!hasPerSecond || Number(model.output_cost) > 0); + + if (!showInput && !showOutput && !hasPerSecond) { + return

-

; + } return (
diff --git a/ui/litellm-dashboard/src/utils/dataUtils.test.ts b/ui/litellm-dashboard/src/utils/dataUtils.test.ts index f696780bc0a..af249ca601a 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.test.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.test.ts @@ -1,7 +1,13 @@ // @vitest-environment jsdom import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import { copyToClipboard, formatNumberWithCommas, getSpendString, updateExistingKeys } from "./dataUtils"; +import { + copyToClipboard, + formatNumberWithCommas, + formatPerSecondCost, + getSpendString, + updateExistingKeys, +} from "./dataUtils"; // Import the mocked module import { toast } from "@/lib/toast"; @@ -115,6 +121,18 @@ describe("dataUtils", () => { }); }); + describe("formatPerSecondCost", () => { + it("should keep at least two decimals and append the per-second unit", () => { + expect(formatPerSecondCost(0.4)).toBe("$0.40/s"); + expect(formatPerSecondCost(1)).toBe("$1.00/s"); + }); + + it("should show sub-cent rates without rounding them to zero", () => { + expect(formatPerSecondCost(0.015)).toBe("$0.015/s"); + expect(formatPerSecondCost(0.000025)).toBe("$0.000025/s"); + }); + }); + describe("copyToClipboard", () => { describe("when Clipboard API is available (HTTPS scenario)", () => { beforeEach(() => { diff --git a/ui/litellm-dashboard/src/utils/dataUtils.ts b/ui/litellm-dashboard/src/utils/dataUtils.ts index 514d13bf9f4..8908041a626 100644 --- a/ui/litellm-dashboard/src/utils/dataUtils.ts +++ b/ui/litellm-dashboard/src/utils/dataUtils.ts @@ -63,6 +63,9 @@ export const getSpendString = (value: number | null | undefined, decimals: numbe return `$${formatted}`; }; +export const formatPerSecondCost = (cost: number): string => + `$${cost.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 6 })}/s`; + export const copyToClipboard = async ( text: string | null | undefined, messageText: string = "Copied to clipboard",