Migrate to antd + fix cost to 0 costs models

This commit is contained in:
yuneng-jiang
2026-02-07 17:28:55 -08:00
parent 5a084cef41
commit 6fad659ae2
4 changed files with 184 additions and 103 deletions
@@ -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>
)}
@@ -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();
});
});
@@ -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>
);