From 8e99accef5c139682d62a8935aef6b1f5d5fa89d Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 21 Feb 2025 19:27:18 -0800 Subject: [PATCH] (UI) Edit Model flow improvements (#8729) * 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 * show edit / delete button on root of table * clean up columns * working edit model flow * decent working model edit page * fix edit model * show created at and created by * ui easy model edit flow * clean up columns * ui clean up updated at --- .../components/model_dashboard/columns.tsx | 72 +++- .../src/components/model_dashboard/table.tsx | 144 ++++--- .../src/components/model_dashboard/types.ts | 1 + .../src/components/model_info_view.tsx | 391 +++++++++++++----- 4 files changed, 430 insertions(+), 178 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx b/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx index a432d87e8a1..61048fa3b55 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/columns.tsx @@ -1,9 +1,9 @@ import { ColumnDef } from "@tanstack/react-table"; -import { Button, Badge } from "@tremor/react"; +import { Button, Badge, Icon } 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 { TrashIcon, PencilIcon, PencilAltIcon } from "@heroicons/react/outline"; import DeleteModelButton from "../delete_model_button"; export const columns = ( @@ -13,6 +13,7 @@ export const columns = ( getDisplayModelName: (model: any) => string, handleEditClick: (model: any) => void, handleRefreshClick: () => void, + setEditModel: (edit: boolean) => void, ): ColumnDef[] => [ { header: "Model ID", @@ -36,11 +37,18 @@ export const columns = ( }, }, { - header: "Model Name", + header: "Public Model Name", accessorKey: "model_name", - cell: ({ row }) => ( -

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

- ), + cell: ({ row }) => { + const displayName = getDisplayModelName(row.original) || "-"; + return ( + +

+ {displayName.length > 20 ? displayName.slice(0, 20) + "..." : displayName} +

+
+ ); + }, }, { header: "Provider", @@ -100,6 +108,19 @@ export const columns = ( ); }, }, + { + header: "Updated At", + accessorKey: "model_info.updated_at", + sortingFn: "datetime", + cell: ({ row }) => { + const model = row.original; + return ( + + {model.model_info.updated_at ? new Date(model.model_info.updated_at).toLocaleDateString() : "-"} + + ); + }, + }, { header: "Created By", accessorKey: "model_info.created_by", @@ -113,7 +134,11 @@ export const columns = ( }, }, { - header: "Input Cost (per 1M tokens)", + header: () => ( + + Input Cost + + ), accessorKey: "input_cost", cell: ({ row }) => { const model = row.original; @@ -125,7 +150,11 @@ export const columns = ( }, }, { - header: "Output Cost (per 1M tokens)", + header: () => ( + + Output Cost + + ), accessorKey: "output_cost", cell: ({ row }) => { const model = row.original; @@ -176,4 +205,31 @@ export const columns = ( ); }, }, + { + id: "actions", + header: "", + cell: ({ row }) => { + const model = row.original; + return ( +
+ { + setSelectedModelId(model.model_info.id); + setEditModel(true); + }} + /> + { + setSelectedModelId(model.model_info.id); + setEditModel(false); + }} + /> +
+ ); + }, + }, ]; \ 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 index e674d6a51bb..7499c9094cb 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/table.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/table.tsx @@ -46,75 +46,87 @@ export function ModelDataTable({ }); 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) => ( - +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + - {flexRender(cell.column.columnDef.cell, cell.getContext())} - +
+
+ {header.isPlaceholder ? null : ( + flexRender( + header.column.columnDef.header, + header.getContext() + ) + )} +
+ {header.id !== 'actions' && ( +
+ {header.column.getIsSorted() ? ( + { + asc: , + desc: + }[header.column.getIsSorted() as string] + ) : ( + + )} +
+ )} +
+
))}
- )) - ) : ( - - -
-

No models found

-
-
-
- )} - -
+ ))} + + + {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 index efcb5c8467d..04ecc1f0098 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/types.ts +++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts @@ -1,6 +1,7 @@ export interface ModelInfo { id: string; created_at: string; + updated_at: string; created_by: string; team_id: string; db_model: boolean; diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 0c21da3fa43..bc6a831d628 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -11,6 +11,8 @@ import { Grid, Badge, Button as TremorButton, + TextInput, + NumberInput, } from "@tremor/react"; import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; import { modelDeleteCall, modelUpdateCall } from "./networking"; @@ -43,12 +45,62 @@ export default function ModelInfoView({ setEditModalVisible, setSelectedModel }: ModelInfoViewProps) { - const [isEditing, setIsEditing] = useState(false); const [form] = Form.useForm(); + const [localModelData, setLocalModelData] = useState(modelData); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDirty, setIsDirty] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isEditing, setIsEditing] = useState(false); const canEditModel = userRole === "Admin"; + const handleModelUpdate = async (values: any) => { + try { + if (!accessToken) return; + setIsSaving(true); + + const updateData = { + model_name: values.model_name, + litellm_params: { + ...localModelData.litellm_params, + model: values.litellm_model_name, + api_base: values.api_base, + custom_llm_provider: values.custom_llm_provider, + organization: values.organization, + tpm: values.tpm, + rpm: values.rpm, + max_retries: values.max_retries, + timeout: values.timeout, + stream_timeout: values.stream_timeout, + input_cost_per_token: values.input_cost / 1_000_000, + output_cost_per_token: values.output_cost / 1_000_000, + }, + model_info: { + id: modelId, + } + + }; + + await modelUpdateCall(accessToken, updateData); + + setLocalModelData({ + ...localModelData, + model_name: values.model_name, + litellm_model_name: values.litellm_model_name, + litellm_params: updateData.litellm_params + }); + + message.success("Model settings updated successfully"); + setIsDirty(false); + setIsEditing(false); + } catch (error) { + console.error("Error updating model:", error); + message.error("Failed to update model settings"); + } finally { + setIsSaving(false); + } + }; + if (!modelData) { return (
@@ -154,122 +206,253 @@ export default function ModelInfoView({ + {/* Audit info shown as a subtle banner below the overview */} +
+
+ + + + Created At {modelData.model_info.created_at + ? new Date(modelData.model_info.created_at).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric' + }) + : "Not Set"} +
+
+ + + + Created By {modelData.model_info.created_by || "Not Set"} +
+
+ {/* Settings Card */}
Model Settings - {(canEditModel && !isEditing) && ( - setIsEditing(true)} + className="flex items-center" > - Edit Settings + Edit Model )}
- - {isEditing ? ( - setIsEditing(false)} - model={modelData} - onSubmit={(data: FormData) => handleEditModelSubmit(data, accessToken, setEditModalVisible, setSelectedModel)} - /> - ) : ( +
setIsDirty(true)} + >
-
- Model ID -
{modelData.model_info.id}
-
- -
- Public Model Name -
{getDisplayModelName(modelData)}
+
+
+ Model Name + {isEditing ? ( + + + + ) : ( +
{localModelData.model_name}
+ )} +
+ +
+ LiteLLM Model Name + {isEditing ? ( + + + + ) : ( +
{localModelData.litellm_model_name}
+ )} +
+ +
+ Input Cost (per 1M tokens) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.input_cost_per_token + ? (localModelData.litellm_params.input_cost_per_token * 1_000_000).toFixed(4) + : modelData.input_cost * 1_000_000} +
+ )} +
+ +
+ Output Cost (per 1M tokens) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.output_cost_per_token + ? (localModelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) + : modelData.output_cost * 1_000_000} +
+ )} +
+ +
+ API Base + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.api_base || "Not Set"} +
+ )} +
+ +
+ Custom LLM Provider + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.custom_llm_provider || "Not Set"} +
+ )} +
+ +
+ Organization + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.organization || "Not Set"} +
+ )} +
+ +
+ TPM (Tokens per Minute) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.tpm || "Not Set"} +
+ )} +
+ +
+ RPM (Requests per Minute) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.rpm || "Not Set"} +
+ )} +
+ +
+ Max Retries + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.max_retries || "Not Set"} +
+ )} +
+ +
+ Timeout (seconds) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.timeout || "Not Set"} +
+ )} +
+ +
+ Stream Timeout (seconds) + {isEditing ? ( + + + + ) : ( +
+ {localModelData.litellm_params?.stream_timeout || "Not Set"} +
+ )} +
+ +
+ Team ID +
+ {modelData.model_info.team_id || "Not Set"} +
+
-
- LiteLLM Model Name -
{modelData.litellm_model_name}
-
- -
- Input Cost (per 1M tokens) -
{modelData.litellm_params?.input_cost_per_token ? (modelData.litellm_params.input_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}
-
- -
- Output Cost (per 1M tokens) -
{modelData.litellm_params?.output_cost_per_token ? (modelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) : "Not Set"}
-
- -
- API Base -
{modelData.litellm_params?.api_base || "Not Set"}
-
- -
- Custom LLM Provider -
{modelData.litellm_params?.custom_llm_provider || "Not Set"}
-
- -
- Model -
{modelData.litellm_params?.model || "Not Set"}
-
- -
- Organization -
{modelData.litellm_params?.organization || "Not Set"}
-
- -
- TPM (Tokens per Minute) -
{modelData.litellm_params?.tpm || "Not Set"}
-
- -
- RPM (Requests per Minute) -
{modelData.litellm_params?.rpm || "Not Set"}
-
- -
- Max Retries -
{modelData.litellm_params?.max_retries || "Not Set"}
-
- -
- Timeout (seconds) -
{modelData.litellm_params?.timeout || "Not Set"}
-
- -
- Stream Timeout (seconds) -
{modelData.litellm_params?.stream_timeout || "Not Set"}
-
- -
- Team ID -
{modelData.model_info.team_id || "Not Set"}
-
- -
- Created At -
{modelData.model_info.created_at ? new Date(modelData.model_info.created_at).toLocaleString() : "Not Set"}
-
- -
- Created By -
{modelData.model_info.created_by || "Not Set"}
-
- -
- LiteLLM Parameters -
-                      {JSON.stringify(modelData.cleanedLitellmParams, null, 2)}
-                    
-
+ {isEditing && ( +
+ { + form.resetFields(); + setIsDirty(false); + setIsEditing(false); + }} + > + Cancel + + form.submit()} + loading={isSaving} + > + Save Changes + +
+ )}
- )} +