From 16f663778d267b0fe9bd7983fa403c99f431f79e Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 30 Dec 2025 13:30:54 -0800 Subject: [PATCH] Add Model Form Refactor --- .../hooks/guardrails/useGuardrails.ts | 18 + .../src/app/(dashboard)/hooks/tags/useTags.ts | 16 + .../ModelsAndEndpointsView.tsx | 1 - .../src/components/add_model/AddModelForm.tsx | 386 +++++++++++++++++ .../components/add_model/add_model_tab.tsx | 407 +----------------- 5 files changed, 441 insertions(+), 387 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts create mode 100644 ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts new file mode 100644 index 00000000000..9786b7fa359 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/guardrails/useGuardrails.ts @@ -0,0 +1,18 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; +import { getGuardrailsList } from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const guardrailKeys = createQueryKeys("guardrails"); + +export const useGuardrails = (): UseQueryResult => { + const { accessToken, userId, userRole } = useAuthorized(); + return useQuery({ + queryKey: guardrailKeys.list({}), + queryFn: async () => { + const response = await getGuardrailsList(accessToken!); + return response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); + }, + enabled: Boolean(accessToken && userId && userRole), + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts new file mode 100644 index 00000000000..8f82502a74c --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/tags/useTags.ts @@ -0,0 +1,16 @@ +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; +import { tagListCall } from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { TagListResponse } from "@/components/tag_management/types"; + +const tagKeys = createQueryKeys("tags"); + +export const useTags = (): UseQueryResult => { + const { accessToken, userId, userRole } = useAuthorized(); + return useQuery({ + queryKey: tagKeys.list({}), + queryFn: async () => await tagListCall(accessToken!), + enabled: Boolean(accessToken && userId && userRole), + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index bf001c62126..315b41393d6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -679,7 +679,6 @@ const ModelsAndEndpointsView: React.FC = ({ credentials={credentialsList} accessToken={accessToken} userRole={userRole} - premiumUser={premiumUser} /> )} diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx new file mode 100644 index 00000000000..7ddc7840464 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx @@ -0,0 +1,386 @@ +import { useProviderFields } from "@/app/(dashboard)/hooks/providers/useProviderFields"; +import { useGuardrails } from "@/app/(dashboard)/hooks/guardrails/useGuardrails"; +import { useTags } from "@/app/(dashboard)/hooks/tags/useTags"; +import { all_admin_roles } from "@/utils/roles"; +import { Switch, Text } from "@tremor/react"; +import type { FormInstance } from "antd"; +import { Select as AntdSelect, Button, Card, Col, Form, Modal, Row, Tooltip, Typography } from "antd"; +import type { UploadProps } from "antd/es/upload"; +import React, { useEffect, useMemo, useState } from "react"; +import TeamDropdown from "../common_components/team_dropdown"; +import type { Team } from "../key_team_helpers/key_list"; +import { type CredentialItem, type ProviderCreateInfo, modelAvailableCall } from "../networking"; +import { Providers, providerLogoMap } from "../provider_info_helpers"; +import { ProviderLogo } from "../molecules/models/ProviderLogo"; +import AdvancedSettings from "./advanced_settings"; +import ConditionalPublicModelName from "./conditional_public_model_name"; +import LiteLLMModelNameField from "./litellm_model_name"; +import ConnectionErrorDisplay from "./model_connection_test"; +import ProviderSpecificFields from "./provider_specific_fields"; +import { TEST_MODES } from "./add_model_modes"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +interface AddModelFormProps { + form: FormInstance; // For the Add Model tab + handleOk: () => void; + selectedProvider: Providers; + setSelectedProvider: (provider: Providers) => void; + providerModels: string[]; + setProviderModelsFn: (provider: Providers) => void; + getPlaceholder: (provider: Providers) => string; + uploadProps: UploadProps; + showAdvancedSettings: boolean; + setShowAdvancedSettings: (show: boolean) => void; + teams: Team[] | null; + credentials: CredentialItem[]; +} + +const { Title, Link } = Typography; + +const AddModelForm: React.FC = ({ + form, + handleOk, + selectedProvider, + setSelectedProvider, + providerModels, + setProviderModelsFn, + getPlaceholder, + uploadProps, + showAdvancedSettings, + setShowAdvancedSettings, + teams, + credentials, +}) => { + const [testMode, setTestMode] = useState("chat"); + const [isResultModalVisible, setIsResultModalVisible] = useState(false); + const [isTestingConnection, setIsTestingConnection] = useState(false); + // Using a unique ID to force the ConnectionErrorDisplay to remount and run a fresh test + const [connectionTestId, setConnectionTestId] = useState(""); + + const { accessToken, userRole, premiumUser } = useAuthorized(); + const { + data: providerMetadata, + isLoading: isProviderMetadataLoading, + error: providerMetadataError, + } = useProviderFields(); + const { data: guardrailsList, isLoading: isGuardrailsLoading, error: guardrailsError } = useGuardrails(); + const { data: tagsList, isLoading: isTagsLoading, error: tagsError } = useTags(); + + const handleTestConnection = async () => { + setIsTestingConnection(true); + setConnectionTestId(`test-${Date.now()}`); + setIsResultModalVisible(true); + }; + + const [isTeamOnly, setIsTeamOnly] = useState(false); + + const [modelAccessGroups, setModelAccessGroups] = useState([]); + + useEffect(() => { + const fetchModelAccessGroups = async () => { + const response = await modelAvailableCall(accessToken, "", "", false, null, true, true); + setModelAccessGroups(response["data"].map((model: any) => model["id"])); + }; + fetchModelAccessGroups(); + }, [accessToken]); + + const sortedProviderMetadata: ProviderCreateInfo[] = useMemo(() => { + if (!providerMetadata) { + return []; + } + return [...providerMetadata].sort((a, b) => a.provider_display_name.localeCompare(b.provider_display_name)); + }, [providerMetadata]); + + const providerMetadataErrorText = providerMetadataError + ? providerMetadataError instanceof Error + ? providerMetadataError.message + : "Failed to load providers" + : null; + + const isAdmin = all_admin_roles.includes(userRole); + + return ( + <> + Add Model + +
{ + console.log("🔥 Form onFinish triggered with values:", values); + handleOk(); + }} + onFinishFailed={(errorInfo) => { + console.log("💥 Form onFinishFailed triggered:", errorInfo); + }} + labelCol={{ span: 10 }} + wrapperCol={{ span: 16 }} + labelAlign="left" + > + <> + {/* Provider Selection */} + + { + setSelectedProvider(value as Providers); + setProviderModelsFn(value as Providers); + form.setFieldsValue({ + custom_llm_provider: value, + }); + form.setFieldsValue({ + model: [], + model_name: undefined, + }); + }} + > + {providerMetadataErrorText && sortedProviderMetadata.length === 0 && ( + + {providerMetadataErrorText} + + )} + {sortedProviderMetadata.map((providerInfo) => { + const displayName = providerInfo.provider_display_name; + const providerKey = providerInfo.provider; + const logoSrc = providerLogoMap[displayName] ?? ""; + + return ( + +
+ + {displayName} +
+
+ ); + })} +
+
+ + + {/* Conditionally Render "Public Model Name" */} + + + {/* Select Mode */} + + setTestMode(value)} + options={TEST_MODES} + /> + + + + + + Optional - LiteLLM endpoint to use when health checking this model{" "} + + Learn more + + + + + + {/* Credentials */} +
+ + Either select existing credentials OR enter new provider credentials below + +
+ + + (option?.label ?? "").toLowerCase().includes(input.toLowerCase())} + options={[ + { value: null, label: "None" }, + ...credentials.map((credential) => ({ + value: credential.credential_name, + label: credential.credential_name, + })), + ]} + allowClear + /> + + + + prevValues.litellm_credential_name !== currentValues.litellm_credential_name || + prevValues.provider !== currentValues.provider + } + > + {({ getFieldValue }) => { + const credentialName = getFieldValue("litellm_credential_name"); + console.log("🔑 Credential Name Changed:", credentialName); + // Only show provider specific fields if no credentials selected + if (!credentialName) { + return ( + <> +
+
+ OR +
+
+ + + ); + } + return null; + }} +
+
+
+ Additional Model Info Settings +
+
+ {/* Team-only Model Switch */} + + + { + setIsTeamOnly(checked); + if (!checked) { + form.setFieldValue("team_id", undefined); + } + }} + disabled={!premiumUser} + /> + + + + {/* Conditional Team Selection */} + {isTeamOnly && ( + + + + )} + {isAdmin && ( + <> + + ({ + value: group, + label: group, + }))} + maxTagCount="responsive" + allowClear + /> + + + )} + + +
+ + Need Help? + +
+ + +
+
+ + +
+ + {/* Test Connection Results Modal */} + { + setIsResultModalVisible(false); + setIsTestingConnection(false); + }} + footer={[ + , + ]} + width={700} + > + {/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */} + {isResultModalVisible && ( + { + setIsResultModalVisible(false); + setIsTestingConnection(false); + }} + onTestComplete={() => setIsTestingConnection(false)} + /> + )} + + + ); +}; + +export default AddModelForm; diff --git a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx index cfe00af5071..17ec1cbceee 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_model_tab.tsx @@ -1,34 +1,19 @@ -import { useProviderFields } from "@/app/(dashboard)/hooks/providers/useProviderFields"; -import { all_admin_roles } from "@/utils/roles"; -import { Switch, Tab, TabGroup, TabList, TabPanel, TabPanels, Text } from "@tremor/react"; +import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; import type { FormInstance } from "antd"; -import { Select as AntdSelect, Button, Card, Col, Form, Modal, Row, Tooltip, Typography } from "antd"; +import { Form } from "antd"; import type { UploadProps } from "antd/es/upload"; -import React, { useEffect, useMemo, useState } from "react"; -import TeamDropdown from "../common_components/team_dropdown"; +import React from "react"; import type { Team } from "../key_team_helpers/key_list"; -import { - type CredentialItem, - type ProviderCreateInfo, - getGuardrailsList, - modelAvailableCall, - tagListCall, -} from "../networking"; -import { Providers, providerLogoMap } from "../provider_info_helpers"; -import { ProviderLogo } from "../molecules/models/ProviderLogo"; -import { Tag } from "../tag_management/types"; +import { type CredentialItem } from "../networking"; +import { Providers } from "../provider_info_helpers"; import AddAutoRouterTab from "./add_auto_router_tab"; -import { TEST_MODES } from "./add_model_modes"; +import AddModelForm from "./AddModelForm"; import AdvancedSettings from "./advanced_settings"; -import ConditionalPublicModelName from "./conditional_public_model_name"; import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit"; -import LiteLLMModelNameField from "./litellm_model_name"; -import ConnectionErrorDisplay from "./model_connection_test"; -import ProviderSpecificFields from "./provider_specific_fields"; interface AddModelTabProps { form: FormInstance; // For the Add Model tab - handleOk: () => void; + handleOk: (values?: any) => void; selectedProvider: Providers; setSelectedProvider: (provider: Providers) => void; providerModels: string[]; @@ -41,11 +26,8 @@ interface AddModelTabProps { credentials: CredentialItem[]; accessToken: string; userRole: string; - premiumUser: boolean; } -const { Title, Link } = Typography; - const AddModelTab: React.FC = ({ form, handleOk, @@ -61,90 +43,9 @@ const AddModelTab: React.FC = ({ credentials, accessToken, userRole, - premiumUser, }) => { // Create separate form instance for auto router const [autoRouterForm] = Form.useForm(); - // State for test mode and connection testing - const [testMode, setTestMode] = useState("chat"); - const [isResultModalVisible, setIsResultModalVisible] = useState(false); - const [isTestingConnection, setIsTestingConnection] = useState(false); - const [guardrailsList, setGuardrailsList] = useState([]); - const [tagsList, setTagsList] = useState>({}); - // Using a unique ID to force the ConnectionErrorDisplay to remount and run a fresh test - const [connectionTestId, setConnectionTestId] = useState(""); - - // Provider metadata for driving the provider select from backend config - const { - data: providerMetadata, - isLoading: isProviderMetadataLoading, - error: providerMetadataError, - } = useProviderFields(); - - useEffect(() => { - const fetchGuardrails = async () => { - try { - const response = await getGuardrailsList(accessToken); - const guardrailNames = response.guardrails.map((g: { guardrail_name: string }) => g.guardrail_name); - setGuardrailsList(guardrailNames); - } catch (error) { - console.error("Failed to fetch guardrails:", error); - } - }; - - fetchGuardrails(); - }, [accessToken]); - - useEffect(() => { - const fetchTags = async () => { - try { - const response = await tagListCall(accessToken); - setTagsList(response); - } catch (error) { - console.error("Failed to fetch tags:", error); - } - }; - - fetchTags(); - }, [accessToken]); - - // Test connection when button is clicked - const handleTestConnection = async () => { - setIsTestingConnection(true); - // Generate a new test ID (using timestamp for uniqueness) - // This forces React to create a new instance of ConnectionErrorDisplay - setConnectionTestId(`test-${Date.now()}`); - // Show the modal with the fresh test - setIsResultModalVisible(true); - }; - - // State for team-only switch - const [isTeamOnly, setIsTeamOnly] = useState(false); - - const [modelAccessGroups, setModelAccessGroups] = useState([]); - - useEffect(() => { - const fetchModelAccessGroups = async () => { - const response = await modelAvailableCall(accessToken, "", "", false, null, true, true); - setModelAccessGroups(response["data"].map((model: any) => model["id"])); - }; - fetchModelAccessGroups(); - }, [accessToken]); - - const sortedProviderMetadata: ProviderCreateInfo[] = useMemo(() => { - if (!providerMetadata) { - return []; - } - return [...providerMetadata].sort((a, b) => a.provider_display_name.localeCompare(b.provider_display_name)); - }, [providerMetadata]); - - const providerMetadataErrorText = providerMetadataError - ? providerMetadataError instanceof Error - ? providerMetadataError.message - : "Failed to load providers" - : null; - - const isAdmin = all_admin_roles.includes(userRole); const handleAutoRouterOk = () => { autoRouterForm @@ -166,247 +67,20 @@ const AddModelTab: React.FC = ({ - Add Model - -
{ - console.log("🔥 Form onFinish triggered with values:", values); - handleOk(); - }} - onFinishFailed={(errorInfo) => { - console.log("💥 Form onFinishFailed triggered:", errorInfo); - }} - labelCol={{ span: 10 }} - wrapperCol={{ span: 16 }} - labelAlign="left" - > - <> - {/* Provider Selection */} - - { - setSelectedProvider(value as Providers); - setProviderModelsFn(value as Providers); - form.setFieldsValue({ - custom_llm_provider: value, - }); - form.setFieldsValue({ - model: [], - model_name: undefined, - }); - }} - > - {providerMetadataErrorText && sortedProviderMetadata.length === 0 && ( - - {providerMetadataErrorText} - - )} - {sortedProviderMetadata.map((providerInfo) => { - const displayName = providerInfo.provider_display_name; - const providerKey = providerInfo.provider; - const logoSrc = providerLogoMap[displayName] ?? ""; - - return ( - -
- - {displayName} -
-
- ); - })} -
-
- - - {/* Conditionally Render "Public Model Name" */} - - - {/* Select Mode */} - - setTestMode(value)} - options={TEST_MODES} - /> - - - - - - Optional - LiteLLM endpoint to use when health checking this model{" "} - - Learn more - - - - - - {/* Credentials */} -
- - Either select existing credentials OR enter new provider credentials below - -
- - - - (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) - } - options={[ - { value: null, label: "None" }, - ...credentials.map((credential) => ({ - value: credential.credential_name, - label: credential.credential_name, - })), - ]} - allowClear - /> - - - - prevValues.litellm_credential_name !== currentValues.litellm_credential_name || - prevValues.provider !== currentValues.provider - } - > - {({ getFieldValue }) => { - const credentialName = getFieldValue("litellm_credential_name"); - console.log("🔑 Credential Name Changed:", credentialName); - // Only show provider specific fields if no credentials selected - if (!credentialName) { - return ( - <> -
-
- OR -
-
- - - ); - } - return null; - }} -
-
-
- Additional Model Info Settings -
-
- {/* Team-only Model Switch */} - - - { - setIsTeamOnly(checked); - if (!checked) { - form.setFieldValue("team_id", undefined); - } - }} - disabled={!premiumUser} - /> - - - - {/* Conditional Team Selection */} - {isTeamOnly && ( - - - - )} - {isAdmin && ( - <> - - ({ - value: group, - label: group, - }))} - maxTagCount="responsive" - allowClear - /> - - - )} - - -
- - Need Help? - -
- - -
-
- - -
+
= ({
- - {/* Test Connection Results Modal */} - { - setIsResultModalVisible(false); - setIsTestingConnection(false); - }} - footer={[ - , - ]} - width={700} - > - {/* Only render the ConnectionErrorDisplay when modal is visible and we have a test ID */} - {isResultModalVisible && ( - { - setIsResultModalVisible(false); - setIsTestingConnection(false); - }} - onTestComplete={() => setIsTestingConnection(false)} - /> - )} - ); };