mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
[Feature] UI - Tag Usage Top Model Table View and Label Fix (#16249)
* Tag Usage Top Model Table View and Label Fix * Removed unused state
This commit is contained in:
parent
4d756a62d9
commit
2ed95e216c
5 changed files with 256 additions and 13 deletions
|
|
@ -29,6 +29,7 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
|
|||
import { valueFormatterSpend } from "./usage/utils/value_formatters";
|
||||
import { getProviderLogoAndName } from "./provider_info_helpers";
|
||||
import { UsageExportHeader } from "./EntityUsageExport";
|
||||
import TopModelView from "./top_model_view";
|
||||
|
||||
interface EntityMetrics {
|
||||
metrics: {
|
||||
|
|
@ -550,17 +551,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
<Col numColSpan={1}>
|
||||
<Card>
|
||||
<Title>Top Models</Title>
|
||||
<BarChart
|
||||
className="mt-4 h-40"
|
||||
data={getTopModels()}
|
||||
index="display_key"
|
||||
categories={["spend"]}
|
||||
colors={["cyan"]}
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
layout="vertical"
|
||||
yAxisWidth={200}
|
||||
showLegend={false}
|
||||
/>
|
||||
<TopModelView topModels={getTopModels()} />
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
|
|
|
|||
110
ui/litellm-dashboard/src/components/top_key_view.test.tsx
Normal file
110
ui/litellm-dashboard/src/components/top_key_view.test.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import TopKeyView from "./top_key_view";
|
||||
|
||||
describe("TopKeyView", () => {
|
||||
it("should render", () => {
|
||||
const { container } = render(
|
||||
<TopKeyView
|
||||
topKeys={[]}
|
||||
accessToken={null}
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={false}
|
||||
/>,
|
||||
);
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should have a table view button", () => {
|
||||
const { getByText } = render(
|
||||
<TopKeyView
|
||||
topKeys={[]}
|
||||
accessToken={null}
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={false}
|
||||
/>,
|
||||
);
|
||||
expect(getByText("Table View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have a chart view", () => {
|
||||
const { getByText } = render(
|
||||
<TopKeyView
|
||||
topKeys={[]}
|
||||
accessToken={null}
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={false}
|
||||
/>,
|
||||
);
|
||||
expect(getByText("Chart View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
["Key ID", "Key Alias", "Spend (USD)"].forEach((header) => {
|
||||
it(`should have a ${header} column`, () => {
|
||||
const { getByText } = render(
|
||||
<TopKeyView
|
||||
topKeys={[]}
|
||||
accessToken={null}
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={false}
|
||||
/>,
|
||||
);
|
||||
expect(getByText(header)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should have a Tags column when showTags is true", () => {
|
||||
const { getByText } = render(
|
||||
<TopKeyView
|
||||
topKeys={[]}
|
||||
accessToken={null}
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={true}
|
||||
/>,
|
||||
);
|
||||
expect(getByText("Tags")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the key's information on the table", () => {
|
||||
const { getByText } = render(
|
||||
<TopKeyView
|
||||
topKeys={[
|
||||
{
|
||||
api_key: "key-123",
|
||||
key_alias: "Test Key",
|
||||
spend: 100,
|
||||
tags: [
|
||||
{ tag: "tag-1", usage: 50 },
|
||||
{ tag: "tag-2", usage: 30 },
|
||||
],
|
||||
},
|
||||
]}
|
||||
accessToken="test-token"
|
||||
userID={null}
|
||||
userRole={null}
|
||||
teams={null}
|
||||
premiumUser={false}
|
||||
showTags={true}
|
||||
/>,
|
||||
);
|
||||
expect(getByText("Test Key")).toBeInTheDocument();
|
||||
expect(getByText(/tag-1/)).toBeInTheDocument();
|
||||
expect(getByText(/tag-2/)).toBeInTheDocument();
|
||||
expect(getByText("$100.00")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -216,9 +216,8 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({
|
|||
yAxisWidth={120}
|
||||
tickGap={5}
|
||||
layout="vertical"
|
||||
showXAxis={false}
|
||||
showLegend={false}
|
||||
valueFormatter={(value) => (value ? `$${formatNumberWithCommas(value, 2)}` : "No Key Alias")}
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
onValueChange={(item) => handleKeyClick(item)}
|
||||
showTooltip={true}
|
||||
customTooltip={(props) => {
|
||||
|
|
|
|||
48
ui/litellm-dashboard/src/components/top_model_view.test.tsx
Normal file
48
ui/litellm-dashboard/src/components/top_model_view.test.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import TopModelView from "./top_model_view";
|
||||
|
||||
describe("TopModelView", () => {
|
||||
it("should render", () => {
|
||||
const { container } = render(<TopModelView topModels={[]} />);
|
||||
expect(container).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should have a table view button", () => {
|
||||
const { getByText } = render(<TopModelView topModels={[]} />);
|
||||
expect(getByText("Table View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have a chart view", () => {
|
||||
const { getByText } = render(<TopModelView topModels={[]} />);
|
||||
expect(getByText("Chart View")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
["Model", "Spend (USD)", "Successful", "Failed", "Tokens"].forEach((header) => {
|
||||
it(`should have a ${header} column`, () => {
|
||||
const { getByText } = render(<TopModelView topModels={[]} />);
|
||||
expect(getByText(header)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show the model's information on the table", () => {
|
||||
const { getByText } = render(
|
||||
<TopModelView
|
||||
topModels={[
|
||||
{
|
||||
key: "gpt-4",
|
||||
spend: 150.5,
|
||||
successful_requests: 100,
|
||||
failed_requests: 5,
|
||||
tokens: 50000,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
expect(getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(getByText("$150.50")).toBeInTheDocument();
|
||||
expect(getByText("100")).toBeInTheDocument();
|
||||
expect(getByText("5")).toBeInTheDocument();
|
||||
expect(getByText("50,000")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
95
ui/litellm-dashboard/src/components/top_model_view.tsx
Normal file
95
ui/litellm-dashboard/src/components/top_model_view.tsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { BarChart } from "@tremor/react";
|
||||
import { formatNumberWithCommas } from "../utils/dataUtils";
|
||||
import { useState } from "react";
|
||||
import { DataTable } from "./view_logs/table";
|
||||
|
||||
interface TopModel {
|
||||
key: string;
|
||||
spend: number;
|
||||
successful_requests: number;
|
||||
failed_requests: number;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
interface TopModelViewProps {
|
||||
topModels: TopModel[];
|
||||
}
|
||||
|
||||
export default function TopModelView({ topModels }: TopModelViewProps) {
|
||||
const [modelViewMode, setModelViewMode] = useState<"chart" | "table">("table");
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "Model",
|
||||
accessorKey: "key",
|
||||
cell: (info: any) => info.getValue() || "-",
|
||||
},
|
||||
{
|
||||
header: "Spend (USD)",
|
||||
accessorKey: "spend",
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
return `$${formatNumberWithCommas(value, 2)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Successful",
|
||||
accessorKey: "successful_requests",
|
||||
cell: (info: any) => <span className="text-green-600">{info.getValue()?.toLocaleString() || 0}</span>,
|
||||
},
|
||||
{
|
||||
header: "Failed",
|
||||
accessorKey: "failed_requests",
|
||||
cell: (info: any) => <span className="text-red-600">{info.getValue()?.toLocaleString() || 0}</span>,
|
||||
},
|
||||
{
|
||||
header: "Tokens",
|
||||
accessorKey: "tokens",
|
||||
cell: (info: any) => info.getValue()?.toLocaleString() || 0,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex justify-end items-center">
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setModelViewMode("table")}
|
||||
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
|
||||
>
|
||||
Table View
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModelViewMode("chart")}
|
||||
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
|
||||
>
|
||||
Chart View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{modelViewMode === "chart" ? (
|
||||
<div className="relative">
|
||||
<BarChart
|
||||
className="mt-4 h-40"
|
||||
data={topModels}
|
||||
index="key"
|
||||
categories={["spend"]}
|
||||
colors={["cyan"]}
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
layout="vertical"
|
||||
yAxisWidth={200}
|
||||
showLegend={false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-auto">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={topModels}
|
||||
renderSubComponent={() => <></>}
|
||||
getRowCanExpand={() => false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue