mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): migrate model_hub_table_columns to shadcn
- Same pattern as mcp_hub_table_columns: @tremor + antd + @ant-design/ icons \u2192 shadcn Badge/Button/Tooltip + lucide. Capabilities palette cycles through a 6-color categorical array (emerald/blue/purple/ orange/red/amber) kept as inline className strings; added to the raw-colors override list. - Public/Private/Mode/Provider badges map to shadcn default/outline variants with categorical color extensions where the hue matters. Gates: TS \u2713 | Build \u2713. Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
parent
1a987d9a90
commit
eaa075ee8e
2 changed files with 156 additions and 89 deletions
|
|
@ -36,7 +36,8 @@
|
|||
"src/components/model_group_alias_settings.tsx",
|
||||
"src/components/cache_dashboard.tsx",
|
||||
"src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx",
|
||||
"src/components/mcp_hub_table_columns.tsx"
|
||||
"src/components/mcp_hub_table_columns.tsx",
|
||||
"src/components/model_hub_table_columns.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"litellm-ui/no-banned-ui-imports": "off",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Button, Badge, Text } from "@tremor/react";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Copy, Info } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ModelHubData {
|
||||
model_group: string;
|
||||
|
|
@ -18,36 +25,70 @@ interface ModelHubData {
|
|||
supports_function_calling: boolean;
|
||||
supported_openai_params?: string[];
|
||||
is_public_model_group: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const formatCapabilityName = (key: string) => {
|
||||
return key
|
||||
const formatCapabilityName = (key: string) =>
|
||||
key
|
||||
.replace(/^supports_/, "")
|
||||
.split("_")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const getModelCapabilities = (model: ModelHubData) => {
|
||||
return Object.entries(model)
|
||||
const getModelCapabilities = (model: ModelHubData) =>
|
||||
Object.entries(model)
|
||||
.filter(([key, value]) => key.startsWith("supports_") && value === true)
|
||||
.map(([key]) => key);
|
||||
};
|
||||
|
||||
const formatCost = (cost: number) => {
|
||||
return `$${(cost * 1_000_000).toFixed(2)}`;
|
||||
};
|
||||
const formatCost = (cost: number) => `$${(cost * 1_000_000).toFixed(2)}`;
|
||||
|
||||
const formatTokens = (tokens: number) => {
|
||||
if (tokens >= 1_000_000) {
|
||||
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
} else if (tokens >= 1_000) {
|
||||
return `${(tokens / 1_000).toFixed(1)}K`;
|
||||
}
|
||||
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
||||
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
|
||||
return tokens.toString();
|
||||
};
|
||||
|
||||
// Categorical capability-color palette. Each index cycles through a set of
|
||||
// tags; added to the raw-colors eslintrc override since these are
|
||||
// categorical by design.
|
||||
const capabilityColors = [
|
||||
"bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300",
|
||||
"bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300",
|
||||
"bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300",
|
||||
"bg-orange-100 text-orange-800 dark:bg-orange-950/40 dark:text-orange-300",
|
||||
"bg-red-100 text-red-800 dark:bg-red-950/40 dark:text-red-300",
|
||||
"bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300",
|
||||
];
|
||||
|
||||
function CopyIconButton({
|
||||
value,
|
||||
onCopy,
|
||||
title,
|
||||
}: {
|
||||
value: string;
|
||||
onCopy: (text: string) => void;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCopy(value)}
|
||||
className="cursor-pointer text-muted-foreground hover:text-primary"
|
||||
aria-label={title}
|
||||
>
|
||||
<Copy size={12} />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{title}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export const modelHubColumns = (
|
||||
showModal: (model: ModelHubData) => void,
|
||||
copyToClipboard: (text: string) => void,
|
||||
|
|
@ -61,21 +102,20 @@ export const modelHubColumns = (
|
|||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium text-sm">{model.model_group}</Text>
|
||||
<Tooltip title="Copy model name">
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(model.model_group)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="font-medium text-sm">{model.model_group}</span>
|
||||
<CopyIconButton
|
||||
value={model.model_group}
|
||||
onCopy={copyToClipboard}
|
||||
title="Copy model name"
|
||||
/>
|
||||
</div>
|
||||
{/* Show provider on mobile when provider column is hidden */}
|
||||
<div className="md:hidden">
|
||||
<Text className="text-xs text-gray-600">{model.providers.join(", ")}</Text>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.providers.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -85,28 +125,32 @@ export const modelHubColumns = (
|
|||
header: "Provider",
|
||||
accessorKey: "providers",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const providersA = rowA.original.providers.join(", ");
|
||||
const providersB = rowB.original.providers.join(", ");
|
||||
return providersA.localeCompare(providersB);
|
||||
},
|
||||
sortingFn: (rowA, rowB) =>
|
||||
rowA.original.providers.join(", ").localeCompare(
|
||||
rowB.original.providers.join(", "),
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{model.providers.slice(0, 2).map((provider) => (
|
||||
<Tag key={provider} color="blue" className="text-xs">
|
||||
<Badge
|
||||
key={provider}
|
||||
variant="outline"
|
||||
className="text-xs bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300 border-blue-200 dark:border-blue-800"
|
||||
>
|
||||
{provider}
|
||||
</Tag>
|
||||
</Badge>
|
||||
))}
|
||||
{model.providers.length > 2 && <Text className="text-xs text-gray-500">+{model.providers.length - 2}</Text>}
|
||||
{model.providers.length > 2 && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
+{model.providers.length - 2}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden md:table-cell",
|
||||
},
|
||||
meta: { className: "hidden md:table-cell" },
|
||||
},
|
||||
{
|
||||
header: "Mode",
|
||||
|
|
@ -115,62 +159,77 @@ export const modelHubColumns = (
|
|||
sortingFn: "alphanumeric",
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return model.mode ? (
|
||||
<Badge color="green" size="sm">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300 border-emerald-200 dark:border-emerald-800"
|
||||
>
|
||||
{model.mode}
|
||||
</Badge>
|
||||
) : (
|
||||
<Text className="text-gray-500">-</Text>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
},
|
||||
{
|
||||
header: "Tokens",
|
||||
accessorKey: "max_input_tokens",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const tokensA = (rowA.original.max_input_tokens || 0) + (rowA.original.max_output_tokens || 0);
|
||||
const tokensB = (rowB.original.max_input_tokens || 0) + (rowB.original.max_output_tokens || 0);
|
||||
const tokensA =
|
||||
(rowA.original.max_input_tokens || 0) +
|
||||
(rowA.original.max_output_tokens || 0);
|
||||
const tokensB =
|
||||
(rowB.original.max_input_tokens || 0) +
|
||||
(rowB.original.max_output_tokens || 0);
|
||||
return tokensA - tokensB;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Text className="text-xs">
|
||||
{model.max_input_tokens ? formatTokens(model.max_input_tokens) : "-"} /{" "}
|
||||
{model.max_output_tokens ? formatTokens(model.max_output_tokens) : "-"}
|
||||
</Text>
|
||||
<span className="text-xs">
|
||||
{model.max_input_tokens
|
||||
? formatTokens(model.max_input_tokens)
|
||||
: "-"}{" "}
|
||||
/{" "}
|
||||
{model.max_output_tokens
|
||||
? formatTokens(model.max_output_tokens)
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden lg:table-cell",
|
||||
},
|
||||
meta: { className: "hidden lg:table-cell" },
|
||||
},
|
||||
{
|
||||
header: "Cost/1M",
|
||||
accessorKey: "input_cost_per_token",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const costA = (rowA.original.input_cost_per_token || 0) + (rowA.original.output_cost_per_token || 0);
|
||||
const costB = (rowB.original.input_cost_per_token || 0) + (rowB.original.output_cost_per_token || 0);
|
||||
const costA =
|
||||
(rowA.original.input_cost_per_token || 0) +
|
||||
(rowA.original.output_cost_per_token || 0);
|
||||
const costB =
|
||||
(rowB.original.input_cost_per_token || 0) +
|
||||
(rowB.original.output_cost_per_token || 0);
|
||||
return costA - costB;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<Text className="text-xs">{model.input_cost_per_token ? formatCost(model.input_cost_per_token) : "-"}</Text>
|
||||
<Text className="text-xs text-gray-500">
|
||||
{model.output_cost_per_token ? formatCost(model.output_cost_per_token) : "-"}
|
||||
</Text>
|
||||
<span className="text-xs block">
|
||||
{model.input_cost_per_token
|
||||
? formatCost(model.input_cost_per_token)
|
||||
: "-"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground block">
|
||||
{model.output_cost_per_token
|
||||
? formatCost(model.output_cost_per_token)
|
||||
: "-"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -182,15 +241,20 @@ export const modelHubColumns = (
|
|||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
const capabilities = getModelCapabilities(model);
|
||||
const colors = ["green", "blue", "purple", "orange", "red", "yellow"];
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{capabilities.length === 0 ? (
|
||||
<Text className="text-gray-500 text-xs">-</Text>
|
||||
<span className="text-muted-foreground text-xs">-</span>
|
||||
) : (
|
||||
capabilities.map((capability, index) => (
|
||||
<Badge key={capability} color={colors[index % colors.length]} size="xs">
|
||||
<Badge
|
||||
key={capability}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"text-xs",
|
||||
capabilityColors[index % capabilityColors.length],
|
||||
)}
|
||||
>
|
||||
{formatCapabilityName(capability)}
|
||||
</Badge>
|
||||
))
|
||||
|
|
@ -204,50 +268,52 @@ export const modelHubColumns = (
|
|||
accessorKey: "is_public_model_group",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const publicA = rowA.original.is_public_model_group === true ? 1 : 0;
|
||||
const publicB = rowB.original.is_public_model_group === true ? 1 : 0;
|
||||
const publicA =
|
||||
rowA.original.is_public_model_group === true ? 1 : 0;
|
||||
const publicB =
|
||||
rowB.original.is_public_model_group === true ? 1 : 0;
|
||||
return publicA - publicB;
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return model.is_public_model_group === true ? (
|
||||
<Badge color="green" size="xs">
|
||||
<Badge variant="default" className="text-xs">
|
||||
Yes
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="gray" size="xs">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
No
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: "hidden md:table-cell",
|
||||
},
|
||||
meta: { className: "hidden md:table-cell" },
|
||||
},
|
||||
{
|
||||
header: "Details",
|
||||
id: "details",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<Button size="xs" variant="secondary" onClick={() => showModal(model)} icon={InfoCircleOutlined}>
|
||||
<span className="hidden lg:inline">Details</span>
|
||||
<span className="lg:hidden">Info</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => showModal(row.original)}
|
||||
>
|
||||
<Info className="h-3 w-3" />
|
||||
<span className="hidden lg:inline">Details</span>
|
||||
<span className="lg:hidden">Info</span>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Filter out columns based on publicPage setting
|
||||
if (publicPage) {
|
||||
return allColumns.filter((column) => {
|
||||
// Remove the public column
|
||||
if ("accessorKey" in column && column.accessorKey === "is_public_model_group") return false;
|
||||
|
||||
if (
|
||||
"accessorKey" in column &&
|
||||
column.accessorKey === "is_public_model_group"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue