mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
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>
This commit is contained in:
parent
0beb2ffb81
commit
b5070408e7
6 changed files with 94 additions and 13 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ export const perSecondCostTiers = (modelInfo: Record<string, unknown> | 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)) };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(<ModelPricingSummary model={tokenPriced} />);
|
||||
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(
|
||||
<ModelPricingSummary
|
||||
model={{
|
||||
input_cost: "0.00" as unknown as number,
|
||||
output_cost: "0.00" as unknown as number,
|
||||
output_cost_per_second: 0.1,
|
||||
output_cost_per_second_tiers: [
|
||||
{ resolution: "1080p", cost: 0.12 },
|
||||
{ resolution: "4k", cost: 0.3 },
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ModelPricingSummary
|
||||
model={{
|
||||
input_cost: "0.60" as unknown as number,
|
||||
output_cost: "0.00" as unknown as number,
|
||||
output_cost_per_second: 0.015,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ModelPricingSummary model={{ input_cost: null as unknown as number, output_cost: null as unknown as number }} />,
|
||||
);
|
||||
expect(screen.getByText("-")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/\$/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -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 <p className="mt-2 text-sm text-muted-foreground">-</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue