diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB.ts new file mode 100644 index 00000000000..d7b42eb2cc4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB.ts @@ -0,0 +1,59 @@ +import { useMutation, UseMutationResult } from "@tanstack/react-query"; +import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking"; +import useAuthorized from "../useAuthorized"; + +export interface StoreModelInDBParams { + store_model_in_db: boolean; +} + +export interface StoreModelInDBResponse { + message: string; +} + +const performStoreModelInDB = async ( + accessToken: string, + params: StoreModelInDBParams +): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl ? `${proxyBaseUrl}/config/update` : `/config/update`; + + const response = await fetch(url, { + method: "POST", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + general_settings: { + store_model_in_db: params.store_model_in_db, + }, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMessage = + errorData?.error?.message || errorData?.message || errorData?.detail || "Failed to update model storage settings"; + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; +}; + +export const useStoreModelInDB = (): UseMutationResult< + StoreModelInDBResponse, + Error, + StoreModelInDBParams +> => { + const { accessToken } = useAuthorized(); + + return useMutation({ + mutationFn: async (params: StoreModelInDBParams) => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return await performStoreModelInDB(accessToken, params); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx index e252f273316..36948630d8a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx @@ -5,10 +5,11 @@ import { Team } from "@/components/key_team_helpers/key_list"; import { AllModelsDataTable } from "@/components/model_dashboard/all_models_table"; import { columns } from "@/components/molecules/models/columns"; import { getDisplayModelName } from "@/components/view_model/model_name_display"; -import { InfoCircleOutlined } from "@ant-design/icons"; +import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons"; import { PaginationState, SortingState } from "@tanstack/react-table"; import { Grid, TabPanel } from "@tremor/react"; -import { Badge, Select, Skeleton, Space, Typography } from "antd"; +import { Badge, Button, Select, Skeleton, Space, Typography } from "antd"; +import ModelSettingsModal from "@/components/model_dashboard/ModelSettingsModal/ModelSettingsModal"; import debounce from "lodash/debounce"; import { useEffect, useMemo, useState } from "react"; import { useModelsInfo } from "../../hooks/models/useModels"; @@ -51,6 +52,7 @@ const AllModelsTab = ({ pageSize: 50, }); const [sorting, setSorting] = useState([]); + const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false); // Debounce search input const debouncedUpdateSearch = useMemo( @@ -326,62 +328,71 @@ const AllModelsTab = ({
{/* Search and Filter Controls */} -
- {/* Model Name Search */} -
- setModelNameSearch(e.target.value)} - /> - - +
+ {/* Model Name Search */} +
+ setModelNameSearch(e.target.value)} /> - + + + +
+ + {/* Filter Button */} + + + {/* Reset Filters Button */} +
- {/* Filter Button */} - - - {/* Reset Filters Button */} - + {/* Model Settings Button */} +
{/* Additional Filters */} @@ -505,6 +516,11 @@ const AllModelsTab = ({
+ setIsModelSettingsModalVisible(false)} + onSuccess={() => setIsModelSettingsModalVisible(false)} + /> ); }; diff --git a/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx new file mode 100644 index 00000000000..106f82585dd --- /dev/null +++ b/ui/litellm-dashboard/src/components/model_dashboard/ModelSettingsModal/ModelSettingsModal.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { ConfigType, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig"; +import { StoreModelInDBParams, useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB"; +import NotificationsManager from "@/components/molecules/notifications_manager"; +import { parseErrorMessage } from "@/components/shared/errorUtils"; +import { Button, Form, Modal, Skeleton, Space, Switch, Typography } from "antd"; +import React, { useEffect, useMemo } from "react"; + +interface ModelSettingsModalProps { + isVisible: boolean; + onCancel: () => void; + onSuccess?: () => void; +} + +const ModelSettingsModal: React.FC = ({ isVisible, onCancel, onSuccess }) => { + const [form] = Form.useForm(); + const { mutateAsync, isPending } = useStoreModelInDB(); + const { data: proxyConfigData, isLoading: isLoadingConfig, refetch } = useProxyConfig(ConfigType.GENERAL_SETTINGS); + + // Refetch config when modal opens to ensure we have the latest values + useEffect(() => { + if (isVisible) { + refetch(); + } + }, [isVisible, refetch]); + + // Compute initial values from fetched config data + const initialValues = useMemo(() => { + if (!proxyConfigData) { + return { + store_model_in_db: false, + }; + } + + const storeModelField = proxyConfigData.find(field => field.field_name === 'store_model_in_db'); + + return { + store_model_in_db: storeModelField?.field_value ?? false, + }; + }, [proxyConfigData]); + + const handleFormSubmit = async (formValues: StoreModelInDBParams) => { + try { + await mutateAsync(formValues, { + onSuccess: () => { + NotificationsManager.success("Model storage settings updated successfully"); + refetch(); + onSuccess?.(); + }, + onError: (error) => { + NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error)); + }, + }); + } catch (error) { + NotificationsManager.fromBackend("Failed to save model storage settings: " + parseErrorMessage(error)); + } + }; + + const handleCancel = () => { + form.resetFields(); + onCancel(); + }; + + return ( + Model Settings} + open={isVisible} + footer={ + + + + + } + onCancel={handleCancel} + > +
+ f.field_name === 'store_model_in_db')?.field_description || + "If enabled, models and config are stored in and loaded from the database." + } + valuePropName="checked" + > + {isLoadingConfig ? : } + +
+
+ ); +}; + +export default ModelSettingsModal;