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",
From d197ca20fa1dcf98511d85898f9c691b42621b83 Mon Sep 17 00:00:00 2001
From: ryan
Date: Fri, 18 Sep 2026 16:13:28 +0000
Subject: [PATCH 4/5] fix(ui): type transformModelData output as ModelData so
the dashboard build typechecks
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../components/AllModelsTable.test.tsx | 14 +++---
.../utils/modelDataTransformer.ts | 43 +++++++++----------
.../src/components/ModelInfoEditForm.tsx | 2 +-
.../src/components/model_dashboard/types.ts | 9 ++--
.../src/components/model_info_view.tsx | 2 +-
.../models/ModelPricingSummary.test.tsx | 12 +++---
6 files changed, 40 insertions(+), 42 deletions(-)
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
index 1f18d9cb4e6..3c054831d99 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
@@ -11,8 +11,8 @@ const makeModel = (overrides: Partial = {}): ModelData =>
model_name: "gpt-4-public",
litellm_model_name: "openai/gpt-4",
provider: "openai",
- input_cost: 30 as unknown as number,
- output_cost: 60 as unknown as number,
+ input_cost: "30",
+ output_cost: "60",
max_tokens: 8192,
max_input_tokens: 8192,
litellm_params: { model: "openai/gpt-4" },
@@ -178,7 +178,7 @@ describe("AllModelsTable", () => {
rerender(
,
);
expect(screen.queryByText(/^\$/)).not.toBeInTheDocument();
@@ -190,8 +190,8 @@ describe("AllModelsTable", () => {
{...baseProps}
data={[
makeModel({
- input_cost: "0.00" as unknown as number,
- output_cost: "0.00" as unknown as number,
+ input_cost: "0.00",
+ output_cost: "0.00",
output_cost_per_second: 0.4,
}),
]}
@@ -205,8 +205,8 @@ describe("AllModelsTable", () => {
{...baseProps}
data={[
makeModel({
- input_cost: "0.60" as unknown as number,
- output_cost: "0.00" as unknown as number,
+ input_cost: "0.60",
+ output_cost: "0.00",
output_cost_per_second: 0.015,
}),
]}
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 ef76a506a18..d33b9f91e2f 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
@@ -1,4 +1,4 @@
-import { PerSecondCostTier } from "@/components/model_dashboard/types";
+import { LiteLLMParams, ModelData, ModelInfo, PerSecondCostTier } from "@/components/model_dashboard/types";
const PER_SECOND_TIER_KEY = /^output_cost_per_second_(.+)$/;
@@ -8,15 +8,11 @@ export const perSecondCostTiers = (modelInfo: Record | null | u
return resolution !== undefined && typeof value === "number" ? [{ resolution, cost: value }] : [];
});
-interface RawLitellmParams {
- model?: string;
- custom_llm_provider?: string;
- api_base?: string;
+interface RawLitellmParams extends LiteLLMParams {
output_cost_per_second?: number;
- [key: string]: unknown;
}
-interface RawModelInfo {
+interface RawModelInfo extends ModelInfo {
input_cost_per_token?: number | null;
output_cost_per_token?: number | null;
output_cost_per_second?: number;
@@ -25,14 +21,15 @@ interface RawModelInfo {
[key: string]: unknown;
}
-interface RawModel {
- litellm_params?: RawLitellmParams;
- model_info?: RawModelInfo;
+export interface RawModel {
+ model_name: string;
+ litellm_params: RawLitellmParams;
+ model_info: RawModelInfo;
[key: string]: unknown;
}
-const costPerMillionTokens = (costPerToken: number | null | undefined) =>
- costPerToken == null ? costPerToken : (Number(costPerToken) * 1000000).toFixed(2);
+const costPerMillionTokens = (costPerToken: number | null | undefined): string | null =>
+ costPerToken == null ? null : (Number(costPerToken) * 1000000).toFixed(2);
const resolveProvider = (
litellmModelName: string | null | undefined,
@@ -45,24 +42,24 @@ const resolveProvider = (
return splitModel.length === 1 ? getProviderFromModel(litellmModelName) : splitModel[0];
};
-const transformModel = (rawModel: RawModel, getProviderFromModel: (model: string) => string) => {
+const transformModel = (rawModel: RawModel, getProviderFromModel: (model: string) => string): ModelData => {
const model: RawModel = JSON.parse(JSON.stringify(rawModel));
const litellmParams = model.litellm_params;
const modelInfo = model.model_info;
return {
...model,
- provider: resolveProvider(litellmParams?.model, litellmParams?.custom_llm_provider, getProviderFromModel),
- input_cost: modelInfo ? costPerMillionTokens(modelInfo.input_cost_per_token) : null,
- output_cost: modelInfo ? costPerMillionTokens(modelInfo.output_cost_per_token) : null,
- output_cost_per_second: litellmParams?.output_cost_per_second ?? modelInfo?.output_cost_per_second ?? null,
+ provider: resolveProvider(litellmParams.model, litellmParams.custom_llm_provider, getProviderFromModel),
+ input_cost: costPerMillionTokens(modelInfo?.input_cost_per_token),
+ output_cost: costPerMillionTokens(modelInfo?.output_cost_per_token),
+ output_cost_per_second: litellmParams.output_cost_per_second ?? modelInfo?.output_cost_per_second ?? null,
output_cost_per_second_tiers: perSecondCostTiers(modelInfo),
- litellm_model_name: litellmParams?.model,
- max_tokens: modelInfo ? modelInfo.max_tokens : "Undefined",
- max_input_tokens: modelInfo ? modelInfo.max_input_tokens : "Undefined",
- api_base: litellmParams?.api_base,
+ litellm_model_name: litellmParams.model,
+ max_tokens: modelInfo?.max_tokens,
+ max_input_tokens: modelInfo?.max_input_tokens,
+ api_base: litellmParams.api_base,
cleanedLitellmParams: Object.fromEntries(
- Object.entries(litellmParams ?? {}).filter(([key]) => key !== "model" && key !== "api_base"),
+ Object.entries(litellmParams).filter(([key]) => key !== "model" && key !== "api_base"),
),
};
};
@@ -70,7 +67,7 @@ const transformModel = (rawModel: RawModel, getProviderFromModel: (model: string
export const transformModelData = (
rawModelData: { data?: RawModel[] | null } | null | undefined,
getProviderFromModel: (model: string) => string,
-) => {
+): { data: ModelData[] } => {
if (!rawModelData?.data) return { data: [] };
return { data: rawModelData.data.map((rawModel) => transformModel(rawModel, getProviderFromModel)) };
diff --git a/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx b/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx
index fb4d90b5ea2..4f0af75ef75 100644
--- a/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx
+++ b/ui/litellm-dashboard/src/components/ModelInfoEditForm.tsx
@@ -270,7 +270,7 @@ const displayCost = (localModelData: any, field: TouchedPricingField): string =>
interface ModelInfoEditFormProps {
localModelData: any;
- modelData: { model_info: { team_id?: string | null } & Record };
+ modelData: { model_info: { team_id?: string | null } };
teamAlias: string | null;
accessToken: string | null;
isEditing: boolean;
diff --git a/ui/litellm-dashboard/src/components/model_dashboard/types.ts b/ui/litellm-dashboard/src/components/model_dashboard/types.ts
index dd9a5b36058..f580e31a933 100644
--- a/ui/litellm-dashboard/src/components/model_dashboard/types.ts
+++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts
@@ -13,6 +13,7 @@ export interface ModelInfo {
access_groups: string[] | null;
blocked?: boolean;
team_public_model_name?: string;
+ key?: string;
}
export interface LiteLLMParams {
@@ -30,12 +31,12 @@ export interface ModelData {
model_name: string;
provider: string;
litellm_model_name: string;
- input_cost: number;
- output_cost: number;
+ input_cost: string | null;
+ output_cost: string | null;
output_cost_per_second?: number | null;
output_cost_per_second_tiers?: PerSecondCostTier[];
- max_tokens: number;
- max_input_tokens: number;
+ max_tokens?: number;
+ max_input_tokens?: number;
api_base?: string;
litellm_params: LiteLLMParams;
cleanedLitellmParams: Record;
diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx
index f4b6744b42e..ee3ac2ecbcb 100644
--- a/ui/litellm-dashboard/src/components/model_info_view.tsx
+++ b/ui/litellm-dashboard/src/components/model_info_view.tsx
@@ -365,7 +365,7 @@ export default function ModelInfoView({
// Parse the model_info from the form values
let updatedModelInfo;
try {
- updatedModelInfo = values.model_info ? JSON.parse(values.model_info) : modelData.model_info;
+ updatedModelInfo = values.model_info ? JSON.parse(values.model_info) : modelData?.model_info;
// Update access_groups from the form
if (values.model_access_group) {
updatedModelInfo = {
diff --git a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
index 5832f4cd17b..7cccea2a540 100644
--- a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
@@ -3,7 +3,7 @@ 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 };
+const tokenPriced = { input_cost: "1.50", output_cost: "2.00" };
describe("ModelPricingSummary", () => {
it("shows per-million-token rates for a token priced model", () => {
@@ -16,8 +16,8 @@ describe("ModelPricingSummary", () => {
render(
{
render(
,
@@ -49,7 +49,7 @@ describe("ModelPricingSummary", () => {
it("renders a dash when the model has no pricing at all", () => {
render(
- ,
+ ,
);
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByText(/\$/)).not.toBeInTheDocument();
From 4f86035a79c27d833c6e263f42574d4cd800b285 Mon Sep 17 00:00:00 2001
From: ryan
Date: Fri, 18 Sep 2026 16:16:28 +0000
Subject: [PATCH 5/5] style(ui): format pricing test fixtures with prettier
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../components/AllModelsTable.test.tsx | 7 +------
.../molecules/models/ModelPricingSummary.test.tsx | 4 +---
2 files changed, 2 insertions(+), 9 deletions(-)
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
index 3c054831d99..cc0169d745c 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx
@@ -175,12 +175,7 @@ describe("AllModelsTable", () => {
expect(screen.getByText("$30")).toBeInTheDocument();
expect(screen.getByText("$60")).toBeInTheDocument();
- rerender(
- ,
- );
+ rerender();
expect(screen.queryByText(/^\$/)).not.toBeInTheDocument();
});
diff --git a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
index 7cccea2a540..921a824e671 100644
--- a/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
+++ b/ui/litellm-dashboard/src/components/molecules/models/ModelPricingSummary.test.tsx
@@ -48,9 +48,7 @@ describe("ModelPricingSummary", () => {
});
it("renders a dash when the model has no pricing at all", () => {
- render(
- ,
- );
+ render();
expect(screen.getByText("-")).toBeInTheDocument();
expect(screen.queryByText(/\$/)).not.toBeInTheDocument();
});