From ad450b761696d71a25ea5033313fc966838258f6 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 21 Feb 2025 19:25:08 -0800 Subject: [PATCH] (Feat) - UI, Allow sorting models by Created_At and all other columns on the UI (#8725) * order models by created at * use existing table component on models page * sorting for created at * ui clean up models page * remove provider filter * fix columns sorting * decent switching * ui fix models page --- .../src/components/model_dashboard.tsx | 694 +----------------- .../components/model_dashboard/columns.tsx | 179 +++++ .../src/components/model_dashboard/table.tsx | 120 +++ .../src/components/model_dashboard/types.ts | 43 ++ 4 files changed, 361 insertions(+), 675 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/model_dashboard/columns.tsx create mode 100644 ui/litellm-dashboard/src/components/model_dashboard/table.tsx create mode 100644 ui/litellm-dashboard/src/components/model_dashboard/types.ts diff --git a/ui/litellm-dashboard/src/components/model_dashboard.tsx b/ui/litellm-dashboard/src/components/model_dashboard.tsx index 210dfbae3e2..454dc708ee8 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard.tsx @@ -108,6 +108,8 @@ import TeamInfoView from "./team/team_info"; import { Providers, provider_map, providerLogoMap, getProviderLogoAndName, getPlaceholder, getProviderModels } from "./provider_info_helpers"; import ModelInfoView from "./model_info_view"; import AddModelTab from "./add_model/add_model_tab"; +import { ModelDataTable } from "./model_dashboard/table"; +import { columns } from "./model_dashboard/columns"; interface ModelDashboardProps { accessToken: string | null; @@ -733,20 +735,6 @@ const ModelDashboard: React.FC = ({ } // when users click request access show pop up to allow them to request access - // sort modelData.data by provider alphabetically, check if provider exists and is not null / undefined - if (modelData.data && modelData.data.length > 0) { - modelData.data.sort((a: any, b: any) => { - if (a.provider && b.provider) { - return a.provider.localeCompare(b.provider); - } else if (a.provider && !b.provider) { - return -1; - } else if (!a.provider && b.provider) { - return 1; - } else { - return 0; - } - }); - } if (userRole && userRole == "Admin Viewer") { const { Title, Paragraph } = Typography; return ( @@ -1097,416 +1085,23 @@ const ModelDashboard: React.FC = ({ ))} - - - - - - Model ID - - - Public Model Name - - - Provider - - - LiteLLM Model - - {userRole === "Admin" && ( - - API Base - - )} - - Input Price{" "} -

- /1M Tokens ($) -

-
- - Output Price{" "} -

- /1M Tokens ($) -

-
- - - {premiumUser ? ( - "Created At" - ) : ( - - {" "} - ✨ Created At - - )} - - - {premiumUser ? ( - "Created By" - ) : ( - - {" "} - ✨ Created By - - )} - - - Team ID - - - Status - - -
-
- - {modelData.data - .filter( - (model: any) => - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - selectedModelGroup === null || - selectedModelGroup === undefined || - selectedModelGroup === "" - ) - .map((model: any, index: number) => ( - - -
- - - -
-
- -

{getDisplayModelName(model) || "-"}

-
- -
- {model.provider && ( - {`${model.provider} { - // Create a div with provider initial as fallback - const target = e.target as HTMLImageElement; - const parent = target.parentElement; - if (parent) { - const fallbackDiv = document.createElement('div'); - fallbackDiv.className = 'w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs'; - fallbackDiv.textContent = model.provider?.charAt(0) || '-'; - parent.replaceChild(fallbackDiv, target); - } - }} - /> - )} -

{model.provider || "-"}

-
-
- - - -
-                                    {model && model.litellm_model_name
-                                      ? model.litellm_model_name.slice(0, 20) + (model.litellm_model_name.length > 20 ? "..." : "")
-                                      : "-"}
-                                  
-
- -
- {userRole === "Admin" && ( - - -
-                                    {model && model.api_base
-                                      ? model.api_base.slice(0, 20)
-                                      : "-"}
-                                  
-
-
- )} - -
-                                {model.input_cost
-                                  ? model.input_cost
-                                  : model.litellm_params.input_cost_per_token != null && model.litellm_params.input_cost_per_token != undefined
-                                    ? (
-                                        Number(
-                                          model.litellm_params
-                                            .input_cost_per_token
-                                        ) * 1000000
-                                      ).toFixed(2)
-                                    : null}
-                              
-
- -
-                                {model.output_cost
-                                  ? model.output_cost
-                                  : model.litellm_params.output_cost_per_token
-                                    ? (
-                                        Number(
-                                          model.litellm_params
-                                            .output_cost_per_token
-                                        ) * 1000000
-                                      ).toFixed(2)
-                                    : null}
-                              
-
- -

- {premiumUser - ? formatCreatedAt( - model.model_info.created_at - ) || "-" - : "-"} -

-
- -

- {premiumUser - ? model.model_info.created_by || "-" - : "-"} -

-
- -
- {model.model_info.team_id ? ( - - - - ) : ( - "-" - )} - - -
-
- - {model.model_info.db_model ? ( - -

DB Model

-
- ) : ( - -

Config Model

-
- )} -
- - -
- handleEditClick(model)} - /> - - - - - - - - - ))} - -
-
+ + selectedModelGroup === "all" || + model.model_name === selectedModelGroup || + !selectedModelGroup + )} + isLoading={false} // Add loading state if needed + /> = ({ )} - - - - Select Time Range - { - setDateValue(value); - updateModelMetrics( - selectedModelGroup, - value.from, - value.to - ); // Call updateModelMetrics with the new date range - }} - /> - - - Select Model Group - - - - - - - - - - - - - - - - - Avg. Latency per Token - ✨ Time to first token - - - -

(seconds/token)

- - average Latency for successfull requests divided by - the total tokens - - {modelMetrics && modelMetricsCategories && ( - - )} -
- - - -
-
-
- - - - - - - Deployment - Success Responses - - Slow Responses

Success Responses taking 600+s

-
-
-
- - {slowResponsesData.map((metric, idx) => ( - - {metric.api_base} - {metric.total_count} - {metric.slow_count} - - ))} - -
-
- -
- - - - All Exceptions for {selectedModelGroup} - - - - - - - - - - All Up Rate Limit Errors (429) for {selectedModelGroup} - - - Num Rate Limit Errors { (globalExceptionData.sum_num_rate_limit_exceptions)} - console.log(v)} - /> - - - - - - - - - - - - - { - premiumUser ? ( - <> - {globalExceptionPerDeployment.map((globalActivity, index) => ( - - {globalActivity.api_base ? globalActivity.api_base : "Unknown API Base"} - - - Num Rate Limit Errors (429) {(globalActivity.sum_num_rate_limit_exceptions)} - console.log(v)} - /> - - - - - ))} - - ) : - <> - {globalExceptionPerDeployment && globalExceptionPerDeployment.length > 0 && - globalExceptionPerDeployment.slice(0, 1).map((globalActivity, index) => ( - - ✨ Rate Limit Errors by Deployment -

Upgrade to see exceptions for all deployments

- - - {globalActivity.api_base} - - - - Num Rate Limit Errors {(globalActivity.sum_num_rate_limit_exceptions)} - - console.log(v)} - /> - - - - - -
- ))} - - } -
- -
Filter by Public Model Name diff --git a/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx b/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx new file mode 100644 index 00000000000..a432d87e8a1 --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx @@ -0,0 +1,179 @@ +import { ColumnDef } from "@tanstack/react-table"; +import { Button, Badge } from "@tremor/react"; +import { Tooltip } from "antd"; +import { getProviderLogoAndName } from "../provider_info_helpers"; +import { ModelData } from "./types"; +import { TrashIcon, PencilIcon } from "@heroicons/react/outline"; +import DeleteModelButton from "../delete_model_button"; + +export const columns = ( + premiumUser: boolean, + setSelectedModelId: (id: string) => void, + setSelectedTeamId: (id: string) => void, + getDisplayModelName: (model: any) => string, + handleEditClick: (model: any) => void, + handleRefreshClick: () => void, +): ColumnDef[] => [ + { + header: "Model ID", + accessorKey: "model_info.id", + cell: ({ row }) => { + const model = row.original; + return ( +
+ + + +
+ ); + }, + }, + { + header: "Model Name", + accessorKey: "model_name", + cell: ({ row }) => ( +

{getDisplayModelName(row.original) || "-"}

+ ), + }, + { + header: "Provider", + accessorKey: "provider", + cell: ({ row }) => { + const model = row.original; + return ( +
+ {model.provider && ( + {`${model.provider} { + const target = e.target as HTMLImageElement; + const parent = target.parentElement; + if (parent) { + const fallbackDiv = document.createElement('div'); + fallbackDiv.className = 'w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs'; + fallbackDiv.textContent = model.provider?.charAt(0) || '-'; + parent.replaceChild(fallbackDiv, target); + } + }} + /> + )} +

{model.provider || "-"}

+
+ ); + }, + }, + { + header: "LiteLLM Model Name", + accessorKey: "litellm_model_name", + cell: ({ row }) => { + const model = row.original; + return ( + +
+            {model.litellm_model_name
+              ? model.litellm_model_name.slice(0, 20) + (model.litellm_model_name.length > 20 ? "..." : "")
+              : "-"}
+          
+
+ ); + }, + }, + { + header: "Created At", + accessorKey: "model_info.created_at", + sortingFn: "datetime", + cell: ({ row }) => { + const model = row.original; + return ( + + {model.model_info.created_at ? new Date(model.model_info.created_at).toLocaleDateString() : "-"} + + ); + }, + }, + { + header: "Created By", + accessorKey: "model_info.created_by", + cell: ({ row }) => { + const model = row.original; + return ( + + {model.model_info.created_by || "-"} + + ); + }, + }, + { + header: "Input Cost (per 1M tokens)", + accessorKey: "input_cost", + cell: ({ row }) => { + const model = row.original; + return ( +
+          {model.input_cost || "-"}
+        
+ ); + }, + }, + { + header: "Output Cost (per 1M tokens)", + accessorKey: "output_cost", + cell: ({ row }) => { + const model = row.original; + return ( +
+          {model.output_cost || "-"}
+        
+ ); + }, + }, + { + header: "Team ID", + accessorKey: "model_info.team_id", + cell: ({ row }) => { + const model = row.original; + return model.model_info.team_id ? ( +
+ + + +
+ ) : ( + "-" + ); + }, + }, + { + header: "Status", + accessorKey: "model_info.db_model", + cell: ({ row }) => { + const model = row.original; + return ( +
+ {model.model_info.db_model ? "DB Model" : "Config Model"} +
+ ); + }, + }, +]; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx new file mode 100644 index 00000000000..e674d6a51bb --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx @@ -0,0 +1,120 @@ +import { Fragment } from "react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getSortedRowModel, + SortingState, + useReactTable, +} from "@tanstack/react-table"; +import React from "react"; +import { + Table, + TableHead, + TableHeaderCell, + TableBody, + TableRow, + TableCell, +} from "@tremor/react"; +import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; + +interface ModelDataTableProps { + data: TData[]; + columns: ColumnDef[]; + isLoading?: boolean; +} + +export function ModelDataTable({ + data = [], + columns, + isLoading = false, +}: ModelDataTableProps) { + const [sorting, setSorting] = React.useState([ + { id: "model_info.created_at", desc: true } + ]); + + const table = useReactTable({ + data, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSorting: true, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+
+ {header.isPlaceholder ? null : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} +
+
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+
+
+ ))} +
+ ))} +
+ + {isLoading ? ( + + +
+

🚅 Loading models...

+
+
+
+ ) : table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + +
+

No models found

+
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/model_dashboard/types.ts b/ui/litellm-dashboard/src/components/model_dashboard/types.ts new file mode 100644 index 00000000000..efcb5c8467d --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts @@ -0,0 +1,43 @@ +export interface ModelInfo { + id: string; + created_at: string; + created_by: string; + team_id: string; + db_model: boolean; +} + +export interface LiteLLMParams { + model: string; + api_base?: string; + input_cost_per_token?: number; + output_cost_per_token?: number; + custom_llm_provider?: string; + [key: string]: any; +} + +export interface ModelData { + model_info: ModelInfo; + model_name: string; + provider: string; + litellm_model_name: string; + input_cost: number; + output_cost: number; + max_tokens: number; + max_input_tokens: number; + api_base?: string; + litellm_params: LiteLLMParams; + cleanedLitellmParams: Record; + accessToken?: string; +} + +export interface ModelDashboardProps { + accessToken: string; + token: string; + userRole: string; + userID: string; + modelData: { data: ModelData[] }; + keys: any[]; + setModelData: (data: any) => void; + premiumUser: boolean; + teams: any[]; +} \ No newline at end of file