mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-16 00:24:17 +00:00
Migrate to antd + fix cost to 0 costs models
This commit is contained in:
+108
-96
@@ -7,13 +7,14 @@ import { columns } from "@/components/molecules/models/columns";
|
||||
import { getDisplayModelName } from "@/components/view_model/model_name_display";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { PaginationState, SortingState } from "@tanstack/react-table";
|
||||
import { Grid, Select, SelectItem, TabPanel, Text } from "@tremor/react";
|
||||
import { Skeleton, Spin } from "antd";
|
||||
import { Grid, TabPanel } from "@tremor/react";
|
||||
import { Badge, Select, Skeleton, Space, Typography } from "antd";
|
||||
import debounce from "lodash/debounce";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useModelsInfo } from "../../hooks/models/useModels";
|
||||
import { transformModelData } from "../utils/modelDataTransformer";
|
||||
type ModelViewMode = "all" | "current_team";
|
||||
const { Text } = Typography;
|
||||
|
||||
interface AllModelsTabProps {
|
||||
selectedModelGroup: string | null;
|
||||
@@ -197,88 +198,95 @@ const AllModelsTab = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Text className="text-lg font-semibold text-gray-900">Current Team:</Text>
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active style={{ width: 320, height: 36 }} />
|
||||
) : (
|
||||
<Select
|
||||
className="w-80"
|
||||
defaultValue="personal"
|
||||
value={currentTeam === "personal" ? "personal" : currentTeam.team_id}
|
||||
onValueChange={(value) => {
|
||||
if (value === "personal") {
|
||||
setCurrentTeam("personal");
|
||||
// Reset to page 1 when team changes
|
||||
setCurrentPage(1);
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
} else {
|
||||
const team = teams?.find((t) => t.team_id === value);
|
||||
if (team) {
|
||||
setCurrentTeam(team);
|
||||
<div className="w-80">
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active block size="large" />
|
||||
) : (
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
size="large"
|
||||
defaultValue="personal"
|
||||
value={currentTeam === "personal" ? "personal" : currentTeam.team_id}
|
||||
onChange={(value) => {
|
||||
if (value === "personal") {
|
||||
setCurrentTeam("personal");
|
||||
// Reset to page 1 when team changes
|
||||
setCurrentPage(1);
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
} else {
|
||||
const team = teams?.find((t) => t.team_id === value);
|
||||
if (team) {
|
||||
setCurrentTeam(team);
|
||||
// Reset to page 1 when team changes
|
||||
setCurrentPage(1);
|
||||
setPagination((prev: PaginationState) => ({ ...prev, pageIndex: 0 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem value="personal">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="font-medium">Personal</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
{isLoadingTeams ? (
|
||||
<SelectItem value="loading">
|
||||
<div className="flex items-center gap-2">
|
||||
<Spin size="small" />
|
||||
<span className="font-medium text-gray-500">Loading teams...</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
) : (
|
||||
teams
|
||||
?.filter((team) => team.team_id)
|
||||
.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="font-medium">
|
||||
{team.team_alias
|
||||
? `${team.team_alias.slice(0, 30)}...`
|
||||
: `Team ${team.team_id.slice(0, 30)}...`}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
}}
|
||||
loading={isLoadingTeams}
|
||||
options={[
|
||||
{
|
||||
value: "personal",
|
||||
label: (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Badge color="blue" size="small" />
|
||||
<Text style={{ fontSize: 16 }}>Personal</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
...(teams
|
||||
?.filter((team) => team.team_id)
|
||||
.map((team) => ({
|
||||
value: team.team_id,
|
||||
label: (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Badge color="green" size="small" />
|
||||
<Text style={{ fontSize: 16 }}>
|
||||
{team.team_alias}
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Text className="text-lg font-semibold text-gray-900">View:</Text>
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active style={{ width: 256, height: 36 }} />
|
||||
) : (
|
||||
<Select
|
||||
className="w-64"
|
||||
defaultValue="current_team"
|
||||
value={modelViewMode}
|
||||
onValueChange={(value) => setModelViewMode(value as "current_team" | "all")}
|
||||
>
|
||||
<SelectItem value="current_team">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
|
||||
<span className="font-medium">Current Team Models</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span className="font-medium">All Available Models</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</Select>
|
||||
)}
|
||||
<div className="w-64">
|
||||
{isLoading ? (
|
||||
<Skeleton.Input active block size="large" />
|
||||
) : (
|
||||
<Select
|
||||
style={{ width: "100%" }}
|
||||
size="large"
|
||||
defaultValue="current_team"
|
||||
value={modelViewMode}
|
||||
onChange={(value) => setModelViewMode(value as "current_team" | "all")}
|
||||
options={[
|
||||
{
|
||||
value: "current_team",
|
||||
label: (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Badge color="purple" size="small" />
|
||||
<Text style={{ fontSize: 16 }}>Current Team Models</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "all",
|
||||
label: (
|
||||
<Space direction="horizontal" align="center">
|
||||
<Badge color="gray" size="small" />
|
||||
<Text style={{ fontSize: 16 }}>All Available Models</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -382,34 +390,38 @@ const AllModelsTab = ({
|
||||
{/* Model Name Filter */}
|
||||
<div className="w-64">
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedModelGroup ?? "all"}
|
||||
onValueChange={(value) => setSelectedModelGroup(value === "all" ? "all" : value)}
|
||||
onChange={(value) => setSelectedModelGroup(value === "all" ? "all" : value)}
|
||||
placeholder="Filter by Public Model Name"
|
||||
>
|
||||
<SelectItem value="all">All Models</SelectItem>
|
||||
<SelectItem value="wildcard">Wildcard Models (*)</SelectItem>
|
||||
{availableModelGroups.map((group, idx) => (
|
||||
<SelectItem key={idx} value={group}>
|
||||
{group}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
showSearch
|
||||
options={[
|
||||
{ value: "all", label: "All Models" },
|
||||
{ value: "wildcard", label: "Wildcard Models (*)" },
|
||||
...availableModelGroups.map((group, idx) => ({
|
||||
value: group,
|
||||
label: group,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Model Access Group Filter */}
|
||||
<div className="w-64">
|
||||
<Select
|
||||
className="w-full"
|
||||
value={selectedModelAccessGroupFilter ?? "all"}
|
||||
onValueChange={(value) => setSelectedModelAccessGroupFilter(value === "all" ? null : value)}
|
||||
onChange={(value) => setSelectedModelAccessGroupFilter(value === "all" ? null : value)}
|
||||
placeholder="Filter by Model Access Group"
|
||||
>
|
||||
<SelectItem value="all">All Model Access Groups</SelectItem>
|
||||
{availableModelAccessGroups.map((accessGroup, idx) => (
|
||||
<SelectItem key={idx} value={accessGroup}>
|
||||
{accessGroup}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
showSearch
|
||||
options={[
|
||||
{ value: "all", label: "All Model Access Groups" },
|
||||
...availableModelAccessGroups.map((accessGroup, idx) => ({
|
||||
value: accessGroup,
|
||||
label: accessGroup,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+69
@@ -50,4 +50,73 @@ describe("transformModelData", () => {
|
||||
const result = transformModelData(null, mockGetProviderFromModel);
|
||||
expect(result).toEqual({ data: [] });
|
||||
});
|
||||
|
||||
it("should handle zero cost models correctly", () => {
|
||||
const rawData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "gemini-2.5-flash",
|
||||
litellm_params: {
|
||||
model: "vertex_ai/gemini-2.5-flash",
|
||||
},
|
||||
model_info: {
|
||||
input_cost_per_token: 0.0,
|
||||
output_cost_per_token: 0.0,
|
||||
max_tokens: 65535,
|
||||
max_input_tokens: 1048576,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformModelData(rawData, mockGetProviderFromModel);
|
||||
|
||||
// Zero costs should be converted to "0.00" per 1M tokens, not left as 0 or null
|
||||
expect(result.data[0]).toHaveProperty("input_cost", "0.00");
|
||||
expect(result.data[0]).toHaveProperty("output_cost", "0.00");
|
||||
});
|
||||
|
||||
it("should handle null cost fields in model_info", () => {
|
||||
const rawData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "some-model",
|
||||
litellm_params: {
|
||||
model: "openai/some-model",
|
||||
},
|
||||
model_info: {
|
||||
input_cost_per_token: null,
|
||||
output_cost_per_token: null,
|
||||
max_tokens: 4096,
|
||||
max_input_tokens: 8192,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformModelData(rawData, mockGetProviderFromModel);
|
||||
|
||||
// Null costs should remain null (displayed as "-" in the UI)
|
||||
expect(result.data[0].input_cost).toBeNull();
|
||||
expect(result.data[0].output_cost).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle missing model_info", () => {
|
||||
const rawData = {
|
||||
data: [
|
||||
{
|
||||
model_name: "some-model",
|
||||
litellm_params: {
|
||||
model: "openai/some-model",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = transformModelData(rawData, mockGetProviderFromModel);
|
||||
|
||||
// Missing model_info should result in null costs
|
||||
expect(result.data[0].input_cost).toBeNull();
|
||||
expect(result.data[0].output_cost).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
+4
-4
@@ -15,8 +15,8 @@ export const transformModelData = (rawModelData: any, getProviderFromModel: (mod
|
||||
let model_info = curr_model?.model_info;
|
||||
|
||||
let provider = "";
|
||||
let input_cost = "Undefined";
|
||||
let output_cost = "Undefined";
|
||||
let input_cost: any = null;
|
||||
let output_cost: any = null;
|
||||
let max_tokens = "Undefined";
|
||||
let max_input_tokens = "Undefined";
|
||||
let cleanedLitellmParams = {};
|
||||
@@ -58,11 +58,11 @@ export const transformModelData = (rawModelData: any, getProviderFromModel: (mod
|
||||
transformedData[i].litellm_model_name = litellm_model_name;
|
||||
|
||||
// Convert Cost in terms of Cost per 1M tokens
|
||||
if (transformedData[i].input_cost) {
|
||||
if (transformedData[i].input_cost != null) {
|
||||
transformedData[i].input_cost = (Number(transformedData[i].input_cost) * 1000000).toFixed(2);
|
||||
}
|
||||
|
||||
if (transformedData[i].output_cost) {
|
||||
if (transformedData[i].output_cost != null) {
|
||||
transformedData[i].output_cost = (Number(transformedData[i].output_cost) * 1000000).toFixed(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ export const columns = (
|
||||
const outputCost = model.output_cost;
|
||||
|
||||
// If both costs are missing or undefined, show "-"
|
||||
if (!inputCost && !outputCost) {
|
||||
if (inputCost == null && outputCost == null) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
@@ -223,9 +223,9 @@ export const columns = (
|
||||
<Tooltip title="Cost per 1M tokens">
|
||||
<div className="flex flex-col min-w-0 w-full">
|
||||
{/* Input Cost - Primary */}
|
||||
{inputCost && <div className="text-xs font-medium text-gray-900 truncate">In: ${inputCost}</div>}
|
||||
{inputCost != null && <div className="text-xs font-medium text-gray-900 truncate">In: ${inputCost}</div>}
|
||||
{/* Output Cost - Secondary */}
|
||||
{outputCost && <div className="text-xs text-gray-500 truncate mt-0.5">Out: ${outputCost}</div>}
|
||||
{outputCost != null && <div className="text-xs text-gray-500 truncate mt-0.5">Out: ${outputCost}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user