From 341a63cf8b6fc5a3f4585ec1ea32fe1aeb94dace Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 17 Feb 2025 17:57:56 -0800 Subject: [PATCH] (UI) Allow adding models for a Team (#8598) (#8601) * ui - use common team dropdown component * re-use team component * rename org field on add model * handle add model submit * working view model_id and team_id on root models page * cleaner * show all fields * working model info view * working team info selector * clean up team id --- ui/litellm-dashboard/src/app/page.tsx | 1 + .../add_model/advanced_settings.tsx | 12 + .../add_model/handle_add_model_submit.tsx | 3 + .../add_model/provider_specific_fields.tsx | 2 +- .../common_components/team_dropdown.tsx | 35 + .../src/components/create_key_button.tsx | 24 +- .../src/components/model_dashboard.tsx | 1679 +++++++++-------- .../src/components/model_info_view.tsx | 333 ++++ 8 files changed, 1276 insertions(+), 813 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx create mode 100644 ui/litellm-dashboard/src/components/model_info_view.tsx diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index d4f99161df..33d52bc41a 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -247,6 +247,7 @@ export default function CreateKeyPage() { modelData={modelData} setModelData={setModelData} premiumUser={premiumUser} + teams={teams} /> ) : page == "llm-playground" ? ( void; + teams?: Team[] | null; } const AdvancedSettings: React.FC = ({ showAdvancedSettings, setShowAdvancedSettings, + teams, }) => { const [form] = Form.useForm(); const [customPricing, setCustomPricing] = React.useState(false); @@ -87,6 +91,14 @@ const AdvancedSettings: React.FC = ({
+ + + + = ({ return ( <> {selectedProviderEnum === Providers.OpenAI && ( - + )} diff --git a/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx new file mode 100644 index 0000000000..313c0cd772 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/team_dropdown.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { Select } from "antd"; +import { Team } from "../key_team_helpers/key_list"; + +interface TeamDropdownProps { + teams?: Team[] | null; + value?: string; + onChange?: (value: string) => void; +} + +const TeamDropdown: React.FC = ({ teams, value, onChange }) => { + return ( + + ); +}; + +export default TeamDropdown; \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index 387e83d9f2..14c7fedce9 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -31,6 +31,7 @@ import { getGuardrailsList, } from "./networking"; import { Team } from "./key_team_helpers/key_list"; +import TeamDropdown from "./common_components/team_dropdown"; import { InfoCircleOutlined } from '@ant-design/icons'; import { Tooltip } from 'antd'; @@ -292,28 +293,7 @@ const CreateKey: React.FC = ({ initialValue={team ? team.team_id : null} className="mt-8" > - + = ({ keys, setModelData, premiumUser, + teams, }) => { const [pendingRequests, setPendingRequests] = useState([]); const [form] = Form.useForm(); @@ -182,7 +187,6 @@ const ModelDashboard: React.FC = ({ const [selectedProvider, setSelectedProvider] = useState(Providers.OpenAI); const [healthCheckResponse, setHealthCheckResponse] = useState(""); const [editModalVisible, setEditModalVisible] = useState(false); - const [infoModalVisible, setInfoModalVisible] = useState(false); const [selectedModel, setSelectedModel] = useState(null); const [availableModelGroups, setAvailableModelGroups] = useState< @@ -227,6 +231,12 @@ const ModelDashboard: React.FC = ({ // Add state for advanced settings visibility const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + // Add these state variables + const [selectedModelId, setSelectedModelId] = useState(null); + const [editModel, setEditModel] = useState(false); + + const [selectedTeamId, setSelectedTeamId] = useState(null); + const setProviderModelsFn = (provider: Providers) => { const _providerModels = getProviderModels(provider, modelMap); setProviderModels(_providerModels); @@ -1057,99 +1067,99 @@ const ModelDashboard: React.FC = ({ ); } + // If a team is selected, render TeamInfoView in full page layout + if (selectedTeamId) { + return ( +
+ setSelectedTeamId(null)} + accessToken={accessToken} + is_team_admin={userRole === "Admin"} + is_proxy_admin={userRole === "Proxy Admin"} + userModels={all_models_on_proxy} + editTeam={false} + /> +
+ ); + } + return (
- - -
- All Models - Add Model - -
/health Models
-
- Model Analytics - Model Retry Settings -
+ {selectedModelId ? ( + { + setSelectedModelId(null); + setEditModel(false); + }} + modelData={modelData.data.find((model: any) => model.model_info.id === selectedModelId)} + accessToken={accessToken} + userID={userID} + userRole={userRole} + /> + ) : ( + + +
+ All Models + Add Model + +
/health Models
+
+ Model Analytics + Model Retry Settings +
-
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - -
-
- - - -
- Filter by Public Model Name - -
- - - - - + {lastRefreshed && Last Refreshed: {lastRefreshed}} + + + + + + +
+ Filter by Public Model Name + +
+ +
+ + = ({ fontSize: "11px", }} > - API Base + Public Model Name - )} - - Input Price{" "} -

- /1M Tokens ($) -

-
- - Output Price{" "} -

- /1M Tokens ($) -

-
- - - {premiumUser ? ( - "Created At" - ) : ( - - {" "} - ✨ Created At - - )} - - - {premiumUser ? ( - "Created By" - ) : ( - - {" "} - ✨ Created By - - )} - - - Status - - -
-
- - {modelData.data - .filter( - (model: any) => - selectedModelGroup === "all" || - model.model_name === selectedModelGroup || - selectedModelGroup === null || - selectedModelGroup === undefined || - selectedModelGroup === "" - ) - .map((model: any, index: number) => ( - - + + Provider + + + LiteLLM Model + + {userRole === "Admin" && ( + -

{model.model_name || "-"}

-
- + )} + + 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) => ( + + +

{model.model_name || "-"}

+
+ +
+ + + +
+
+ = ({ )}

{model.provider || "-"}

-
- - - -
-                                  {model && model.litellm_model_name
-                                    ? model.litellm_model_name.slice(0, 20) + (model.litellm_model_name.length > 20 ? "..." : "")
-                                    : "-"}
-                                
-
+
- - {userRole === "Admin" && ( + + +
+                                    {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

+
+ )} +
= ({ wordBreak: "break-word", }} > - -
-                                  {model && model.api_base
-                                    ? model.api_base.slice(0, 20)
-                                    : "-"}
-                                
-
+ +
+ handleInfoClick(model)} + /> + + + handleEditClick(model)} + /> + + + + + + - )} - -
-                              {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.db_model ? ( - -

DB Model

-
- ) : ( - -

Config Model

-
- )} -
- - -
- handleInfoClick(model)} - /> - - - handleEditClick(model)} - /> - - - - - - - - - ))} - -
-
-
- - - Model Info - - {selectedModel && JSON.stringify(selectedModel, null, 2)} - - -
- - Add new model - -
- <> - {/* Provider Selection */} - - { - setSelectedProvider(value); - setProviderModelsFn(value); - form.setFieldsValue({ - model: [], - model_name: undefined - }); - }} + + ))} + + + + + + + + Add new model + + + <> + {/* Provider Selection */} + - {Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ( - -
- {`${providerEnum} { - // 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-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs'; - fallbackDiv.textContent = providerDisplayName.charAt(0); - parent.replaceChild(fallbackDiv, target); - } - }} - /> - {providerDisplayName} -
-
- ))} -
-
- { + setSelectedProvider(value); + setProviderModelsFn(value); + form.setFieldsValue({ + model: [], + model_name: undefined + }); + }} + > + {Object.entries(Providers).map(([providerEnum, providerDisplayName]) => ( + +
+ {`${providerEnum} { + // 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-5 h-5 rounded-full bg-gray-200 flex items-center justify-center text-xs'; + fallbackDiv.textContent = providerDisplayName.charAt(0); + parent.replaceChild(fallbackDiv, target); + } + }} + /> + {providerDisplayName} +
+
+ ))} + + + + + {/* Conditionally Render "Public Model Name" */} + + + - - {/* Conditionally Render "Public Model Name" */} - + + - + + + Need Help? + + + Add Model +
+ + + + + + + + `/health` will run a very small request through your models + configured on litellm + + + + {healthCheckResponse && ( +
{JSON.stringify(healthCheckResponse, null, 2)}
+ )} +
+
+ + + + Select Time Range + { + setDateValue(value); + updateModelMetrics( + selectedModelGroup, + value.from, + value.to + ); // Call updateModelMetrics with the new date range + }} /> - - - -
- - - Need Help? - - - Add Model -
- - - -
- - - - `/health` will run a very small request through your models - configured on litellm - - - - {healthCheckResponse && ( -
{JSON.stringify(healthCheckResponse, null, 2)}
- )} -
-
- - - - Select Time Range - { - setDateValue(value); - updateModelMetrics( - selectedModelGroup, - value.from, - value.to - ); // Call updateModelMetrics with the new date range + + + Select Model Group + + + + - - - 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 + - - - - - - - - +
+ Retry Policy for {selectedModelGroup} + + How many retries should be attempted based on the Exception + + {retry_policy_map && ( + + + {Object.entries(retry_policy_map).map( + ([exceptionType, retryPolicyKey], idx) => { + let retryCount = + modelGroupRetryPolicy?.[selectedModelGroup!]?.[ + retryPolicyKey + ]; + if (retryCount == null) { + retryCount = defaultRetry; + } - - - - - - 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)} + return ( + + + {exceptionType} + + + { + setModelGroupRetryPolicy( + (prevModelGroupRetryPolicy) => { + const prevRetryPolicy = + prevModelGroupRetryPolicy?.[ + selectedModelGroup! + ] ?? {}; + return { + ...(prevModelGroupRetryPolicy ?? {}), + [selectedModelGroup!]: { + ...prevRetryPolicy, + [retryPolicyKey!]: value, + }, + } as RetryPolicyObject; + } + ); + }} /> - - - - - -
- ))} - - } -
- -
- -
- Filter by Public Model Name - - -
- - Retry Policy for {selectedModelGroup} - - How many retries should be attempted based on the Exception - - {retry_policy_map && ( - - - {Object.entries(retry_policy_map).map( - ([exceptionType, retryPolicyKey], idx) => { - let retryCount = - modelGroupRetryPolicy?.[selectedModelGroup!]?.[ - retryPolicyKey - ]; - if (retryCount == null) { - retryCount = defaultRetry; + + + ); } - - return ( - - - - - ); - } - )} - -
- {exceptionType} - - { - setModelGroupRetryPolicy( - (prevModelGroupRetryPolicy) => { - const prevRetryPolicy = - prevModelGroupRetryPolicy?.[ - selectedModelGroup! - ] ?? {}; - return { - ...(prevModelGroupRetryPolicy ?? {}), - [selectedModelGroup!]: { - ...prevRetryPolicy, - [retryPolicyKey!]: value, - }, - } as RetryPolicyObject; - } - ); - }} - /> -
- )} - -
- - + )} + + + )} + + + + + )}
); }; diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx new file mode 100644 index 0000000000..ece453e041 --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -0,0 +1,333 @@ +import React, { useState, useEffect } from "react"; +import { + Card, + Title, + Text, + Tab, + TabList, + TabGroup, + TabPanel, + TabPanels, + Grid, + Badge, + Button as TremorButton, +} from "@tremor/react"; +import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; +import { modelDeleteCall, modelUpdateCall } from "./networking"; +import { Button, Form, Input, InputNumber, message, Select } from "antd"; +import EditModelModal from "./edit_model/edit_model_modal"; +import { getProviderLogoAndName } from "./provider_info_helpers"; + +interface ModelInfoViewProps { + modelId: string; + onClose: () => void; + modelData: any; + accessToken: string | null; + userID: string | null; + userRole: string | null; + editModel: boolean; +} + +export default function ModelInfoView({ + modelId, + onClose, + modelData, + accessToken, + userID, + userRole, + editModel +}: ModelInfoViewProps) { + const [isEditing, setIsEditing] = useState(false); + const [form] = Form.useForm(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const canEditModel = userRole === "Admin"; + + if (!modelData) { + return ( +
+ + Model not found +
+ ); + } + + const handleDelete = async () => { + try { + if (!accessToken) return; + await modelDeleteCall(accessToken, modelId); + message.success("Model deleted successfully"); + onClose(); + } catch (error) { + console.error("Error deleting the model:", error); + message.error("Failed to delete model"); + } + }; + + const handleModelUpdate = async (values: any) => { + try { + if (!accessToken) return; + + let payload = { + litellm_params: Object.keys(values.litellm_params).length > 0 ? values.litellm_params : undefined, + model_info: values.model_info ? { + id: values.model_info.id, + } : undefined, + }; + + await modelUpdateCall(accessToken, payload); + message.success("Model updated successfully"); + setIsEditing(false); + } catch (error) { + console.error("Error updating model:", error); + message.error("Failed to update model"); + } + }; + + return ( +
+
+
+ + {modelData.model_name} + {modelData.model_info.id} +
+ {canEditModel && ( +
+ setIsDeleteModalOpen(true)} + color="red" + > + Delete Model + +
+ )} +
+ + + + Overview + Raw JSON + + + + + {/* Overview Grid */} + + + Provider +
+ {modelData.provider && ( + {`${modelData.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 = modelData.provider?.charAt(0) || '-'; + parent.replaceChild(fallbackDiv, target); + } + }} + /> + )} + {modelData.provider || "Not Set"} +
+
+ + LiteLLM Model +
+                  {modelData.litellm_model_name || "Not Set"}
+                
+
+ + Pricing +
+ Input: ${modelData.input_cost}/1M tokens + Output: ${modelData.output_cost}/1M tokens +
+
+
+ + {/* Settings Card */} + +
+ Model Settings + {(canEditModel && !isEditing) && ( + setIsEditing(true)} + > + Edit Settings + + )} +
+ + {isEditing ? ( + setIsEditing(false)} + model={modelData} + onSubmit={handleModelUpdate} + /> + ) : ( +
+
+ Model ID +
{modelData.model_info.id}
+
+ +
+ Public Model Name +
{modelData.model_name}
+
+ +
+ 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)}
+                    
+
+
+ )} +
+
+ + + +
+                {JSON.stringify(modelData, null, 2)}
+              
+
+
+
+
+ + {/* Delete Confirmation Modal */} + {isDeleteModalOpen && ( +
+
+ + + + +
+
+
+
+

+ Delete Model +

+
+

+ Are you sure you want to delete this model? +

+
+
+
+
+
+ + +
+
+
+
+ )} +
+ ); +} \ No newline at end of file