Merge pull request #21516 from BerriAI/litellm_model_settings_modal

[Feature] UI - Models & Endpoints: Add Model Settings Modal
This commit is contained in:
yuneng-jiang 2026-02-18 21:02:31 -08:00 • committed by GitHub
commit a717f3624d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 687 additions and 55 deletions

View file

@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import React, { ReactNode } from "react";
import { useStoreModelInDB } from "./useStoreModelInDB";
vi.mock("@/components/networking", () => ({
getProxyBaseUrl: vi.fn(() => ""),
getGlobalLitellmHeaderName: vi.fn(() => "Authorization"),
}));
describe("useStoreModelInDB", () => {
let queryClient: QueryClient;
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
mutations: { retry: false },
},
});
fetchSpy = vi.fn();
global.fetch = fetchSpy;
});
afterEach(() => {
vi.restoreAllMocks();
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should send correct request body to /config/field/update", async () => {
fetchSpy.mockResolvedValue({
ok: true,
json: async () => ({ message: "Success" }),
});
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
result.current.mutate({ store_model_in_db: true });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(fetchSpy).toHaveBeenCalledWith(
"/config/field/update",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
field_name: "store_model_in_db",
field_value: true,
config_type: "general_settings",
}),
})
);
});
it("should handle setting store_model_in_db to false", async () => {
fetchSpy.mockResolvedValue({
ok: true,
json: async () => ({ message: "Success" }),
});
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
result.current.mutate({ store_model_in_db: false });
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(fetchSpy).toHaveBeenCalledWith(
"/config/field/update",
expect.objectContaining({
body: JSON.stringify({
field_name: "store_model_in_db",
field_value: false,
config_type: "general_settings",
}),
})
);
});
it("should throw error when access token is missing", async () => {
vi.spyOn(
await import("../useAuthorized"),
"default"
).mockReturnValue({
accessToken: null,
userRole: null,
userId: null,
token: null,
userEmail: null,
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
} as any);
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
result.current.mutate({ store_model_in_db: true });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error?.message).toBe("Access token is required");
expect(fetchSpy).not.toHaveBeenCalled();
});
it("should handle API error response", async () => {
fetchSpy.mockResolvedValue({
ok: false,
json: async () => ({ detail: "Unauthorized" }),
});
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
result.current.mutate({ store_model_in_db: true });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error?.message).toBe("Unauthorized");
});
it("should use fallback error message when API returns empty error", async () => {
fetchSpy.mockResolvedValue({
ok: false,
json: async () => ({}),
});
const { result } = renderHook(() => useStoreModelInDB(), { wrapper });
result.current.mutate({ store_model_in_db: true });
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error?.message).toBe("Failed to update model storage settings");
});
});

View file

@ -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<StoreModelInDBResponse> => {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl ? `${proxyBaseUrl}/config/field/update` : `/config/field/update`;
const response = await fetch(url, {
method: "POST",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
field_name: "store_model_in_db",
field_value: params.store_model_in_db,
config_type: "general_settings",
}),
});
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<StoreModelInDBResponse, Error, StoreModelInDBParams>({
mutationFn: async (params: StoreModelInDBParams) => {
if (!accessToken) {
throw new Error("Access token is required");
}
return await performStoreModelInDB(accessToken, params);
},
});
};

View file

@ -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<SortingState>([]);
const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false);
// Debounce search input
const debouncedUpdateSearch = useMemo(
@ -326,62 +328,71 @@ const AllModelsTab = ({
<div className="border-b px-6 py-4">
<div className="flex flex-col space-y-4">
{/* Search and Filter Controls */}
<div className="flex flex-wrap items-center gap-3">
{/* Model Name Search */}
<div className="relative w-64">
<input
type="text"
placeholder="Search model names..."
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
<div className="flex items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-3">
{/* Model Name Search */}
<div className="relative w-64">
<input
type="text"
placeholder="Search model names..."
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
/>
</svg>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</div>
{/* Filter Button */}
<button
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
Filters
</button>
{/* Reset Filters Button */}
<button
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
onClick={resetFilters}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Reset Filters
</button>
</div>
{/* Filter Button */}
<button
className={`px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2 ${showFilters ? "bg-gray-100" : ""}`}
onClick={() => setShowFilters(!showFilters)}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"
/>
</svg>
Filters
</button>
{/* Reset Filters Button */}
<button
className="px-3 py-2 text-sm border rounded-md hover:bg-gray-50 flex items-center gap-2"
onClick={resetFilters}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
Reset Filters
</button>
{/* Model Settings Button */}
<Button
icon={<SettingOutlined />}
onClick={() => setIsModelSettingsModalVisible(true)}
title="Model Settings"
/>
</div>
{/* Additional Filters */}
@ -505,6 +516,11 @@ const AllModelsTab = ({
</div>
</div>
</Grid>
<ModelSettingsModal
isVisible={isModelSettingsModalVisible}
onCancel={() => setIsModelSettingsModalVisible(false)}
onSuccess={() => setIsModelSettingsModalVisible(false)}
/>
</TabPanel>
);
};

View file

@ -0,0 +1,306 @@
import { useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import { useStoreModelInDB } from "@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../tests/test-utils";
import ModelSettingsModal from "./ModelSettingsModal";
vi.mock("@/app/(dashboard)/hooks/storeModelInDB/useStoreModelInDB");
vi.mock("@/app/(dashboard)/hooks/proxyConfig/useProxyConfig");
vi.mock("@/components/molecules/notifications_manager", () => ({
default: {
success: vi.fn(),
fromBackend: vi.fn(),
},
}));
vi.mock("@/components/shared/errorUtils", () => ({
parseErrorMessage: vi.fn(),
}));
const mockUseStoreModelInDB = vi.mocked(useStoreModelInDB);
const mockUseProxyConfig = vi.mocked(useProxyConfig);
const mockNotificationsManager = vi.mocked(NotificationsManager);
const mockParseErrorMessage = vi.mocked(parseErrorMessage);
describe("ModelSettingsModal", () => {
const mockOnCancel = vi.fn();
const mockOnSuccess = vi.fn();
const mockMutateAsync = vi.fn();
const mockRefetch = vi.fn();
const defaultProps = {
isVisible: true,
onCancel: mockOnCancel,
onSuccess: mockOnSuccess,
};
beforeEach(() => {
vi.clearAllMocks();
mockUseStoreModelInDB.mockReturnValue({
mutateAsync: mockMutateAsync,
isPending: false,
} as any);
mockUseProxyConfig.mockReturnValue({
data: [],
isLoading: false,
refetch: mockRefetch,
} as any);
mockParseErrorMessage.mockImplementation((error: any) => error?.message || String(error));
});
it("should render the modal", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(screen.getByRole("dialog")).toBeInTheDocument();
expect(screen.getByText("Model Settings")).toBeInTheDocument();
});
it("should render form field with initial values", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(screen.getByText("Store Model in DB")).toBeInTheDocument();
});
it("should render cancel and save buttons", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Save Settings" })).toBeInTheDocument();
});
it("should call onCancel when cancel button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const cancelButton = screen.getByRole("button", { name: "Cancel" });
await user.click(cancelButton);
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it("should call onCancel when modal close button is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const closeButton = screen.getByRole("button", { name: /close/i });
await user.click(closeButton);
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
it("should toggle store model switch", async () => {
const user = userEvent.setup();
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).not.toBeChecked();
await user.click(switchElement);
await waitFor(() => {
expect(switchElement).toBeChecked();
});
});
it("should submit form with store_model_in_db enabled", async () => {
const user = userEvent.setup();
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
return { message: "Success" };
});
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const switchElement = screen.getByRole("switch");
await user.click(switchElement);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockMutateAsync).toHaveBeenCalledWith(
{ store_model_in_db: true },
expect.any(Object)
);
});
});
it("should submit form with store_model_in_db disabled", async () => {
const user = userEvent.setup();
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
return { message: "Success" };
});
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockMutateAsync).toHaveBeenCalledWith(
{ store_model_in_db: false },
expect.any(Object)
);
});
});
it("should show success notification and call onSuccess on successful submission", async () => {
const user = userEvent.setup();
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
return { message: "Success" };
});
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Model storage settings updated successfully");
expect(mockRefetch).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalledTimes(1);
});
});
it("should show error notification when submission fails", async () => {
const user = userEvent.setup();
const error = new Error("Network error");
mockMutateAsync.mockRejectedValue(error);
mockParseErrorMessage.mockReturnValue("Network error");
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save model storage settings: Network error");
});
});
it("should show error notification from onError callback", async () => {
const user = userEvent.setup();
const error = new Error("Backend error");
mockMutateAsync.mockImplementation((params, options) => {
options?.onError?.(error);
return Promise.reject(error);
});
mockParseErrorMessage.mockReturnValue("Backend error");
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to save model storage settings: Backend error");
});
});
it("should disable cancel button when pending", () => {
mockUseStoreModelInDB.mockReturnValue({
mutateAsync: mockMutateAsync,
isPending: true,
} as any);
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const cancelButton = screen.getByRole("button", { name: "Cancel" });
expect(cancelButton).toBeDisabled();
});
it("should disable cancel button when loading config", () => {
mockUseProxyConfig.mockReturnValue({
data: undefined,
isLoading: true,
refetch: mockRefetch,
} as any);
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const cancelButton = screen.getByRole("button", { name: "Cancel" });
expect(cancelButton).toBeDisabled();
});
it("should show loading state on save button when pending", () => {
mockUseStoreModelInDB.mockReturnValue({
mutateAsync: mockMutateAsync,
isPending: true,
} as any);
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const saveButton = screen.getByRole("button", { name: /Saving/i });
expect(saveButton).toBeInTheDocument();
expect(saveButton.className).toContain("ant-btn-loading");
});
it("should not render modal when isVisible is false", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} isVisible={false} />);
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
it("should call refetch when modal opens", () => {
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(mockRefetch).toHaveBeenCalledTimes(1);
});
it("should render form with initial values from config data", () => {
mockUseProxyConfig.mockReturnValue({
data: [
{
field_name: "store_model_in_db",
field_type: "bool",
field_description: "Store model in DB",
field_value: true,
stored_in_db: true,
field_default_value: false,
},
],
isLoading: false,
refetch: mockRefetch,
} as any);
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
const switchElement = screen.getByRole("switch");
expect(switchElement).toBeChecked();
});
it("should show skeleton loader when config is loading", () => {
mockUseProxyConfig.mockReturnValue({
data: undefined,
isLoading: true,
refetch: mockRefetch,
} as any);
renderWithProviders(<ModelSettingsModal {...defaultProps} />);
expect(screen.queryByRole("switch")).not.toBeInTheDocument();
const skeletons = document.querySelectorAll(".ant-skeleton");
expect(skeletons.length).toBeGreaterThan(0);
});
it("should not call onSuccess when it is not provided", async () => {
const user = userEvent.setup();
mockMutateAsync.mockImplementation(async (params, options) => {
await Promise.resolve();
options?.onSuccess?.();
return { message: "Success" };
});
renderWithProviders(<ModelSettingsModal isVisible={true} onCancel={mockOnCancel} />);
const saveButton = screen.getByRole("button", { name: "Save Settings" });
await user.click(saveButton);
await waitFor(() => {
expect(mockNotificationsManager.success).toHaveBeenCalled();
});
});
});

View file

@ -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<ModelSettingsModalProps> = ({ 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 (
<Modal
title={<Typography.Title level={5}>Model Settings</Typography.Title>}
open={isVisible}
footer={
<Space>
<Button onClick={handleCancel} disabled={isPending || isLoadingConfig}>
Cancel
</Button>
<Button type="primary" loading={isPending} disabled={isLoadingConfig} onClick={() => form.submit()}>
{isPending ? "Saving..." : "Save Settings"}
</Button>
</Space>
}
onCancel={handleCancel}
>
<Form
key={proxyConfigData ? JSON.stringify(initialValues) : 'loading'}
form={form}
layout="horizontal"
onFinish={handleFormSubmit}
initialValues={initialValues}
>
<Form.Item
label="Store Model in DB"
name="store_model_in_db"
tooltip={
proxyConfigData?.find(f => 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 ? <Skeleton.Input active block /> : <Switch />}
</Form.Item>
</Form>
</Modal>
);
};
export default ModelSettingsModal;