diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index b0a4d258ea8..c471355db68 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -793,16 +793,6 @@ "count": 1 } }, - "src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer.ts": { "prefer-const": { "count": 6 @@ -1740,7 +1730,7 @@ }, "src/components/add_model/AddModelForm.test.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "src/components/add_model/AddModelForm.tsx": { @@ -1751,7 +1741,7 @@ "count": 1 }, "no-restricted-imports": { - "count": 3 + "count": 2 } }, "src/components/add_model/ClassificationMethodConfig.tsx": { @@ -1803,9 +1793,6 @@ }, "no-restricted-imports": { "count": 3 - }, - "prefer-const": { - "count": 2 } }, "src/components/add_model/auto_router_connection_test.tsx": { @@ -1818,11 +1805,6 @@ "count": 1 } }, - "src/components/add_model/conditional_public_model_name.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/add_model/conditional_public_model_name.tsx": { "local/filename-pascal-case": { "count": 1 @@ -1847,11 +1829,6 @@ "count": 1 } }, - "src/components/add_model/litellm_model_name.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/add_model/litellm_model_name.tsx": { "local/filename-pascal-case": { "count": 1 @@ -1871,11 +1848,6 @@ "count": 2 } }, - "src/components/add_model/provider_specific_fields.test.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/add_model/provider_specific_fields.tsx": { "local/filename-pascal-case": { "count": 1 @@ -3011,4 +2983,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx index c22ff7ff460..b762f006261 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx @@ -361,4 +361,64 @@ describe("AddModelPanel validation gates", () => { expect(modelCreateCall).not.toHaveBeenCalled(); }); + + it("blocks the submit when LiteLLM Params is not valid JSON", async () => { + mockPtuEnabled.mockReturnValue(false); + const { user, openAdvanced, fillRequired, submitExpectingRejection } = await setup(); + await fillRequired(); + await openAdvanced(); + await user.type(screen.getByLabelText("LiteLLM Params"), "rpm: 7"); + await submitExpectingRejection("Please enter valid JSON"); + + expect(modelCreateCall).not.toHaveBeenCalled(); + }); +}); + +describe("AddModelPanel behaviours the removed Advanced Settings form instance never drove", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockPtuEnabled.mockReturnValue(false); + mockAuthorized.mockReturnValue(PROXY_ADMIN); + }); + + it("leaves LiteLLM Params untouched when pass through routes is switched on", async () => { + const { user, openAdvanced, fillRequired, submit } = await setup(); + await fillRequired(); + await openAdvanced(); + await user.click(screen.getByLabelText("Use in pass through routes")); + expect(screen.getByLabelText("LiteLLM Params")).toHaveValue(""); + + await submit(); + + expect(lastCreatedModel()).toStrictEqual({ + model_name: "gpt-4o", + litellm_params: { ...alwaysMounted, ...advancedOpenExtras, use_in_pass_through: true }, + model_info: { ...baseModelInfo }, + }); + }); + + it("keeps a typed cost when custom pricing is switched off and back on", async () => { + const { user, openAdvanced, fillRequired, submit } = await setup(); + await fillRequired(); + await openAdvanced(); + await user.click(screen.getByLabelText("Custom Pricing")); + await user.type(await screen.findByLabelText("Input Cost (per 1M tokens)"), "3"); + await user.click(screen.getByLabelText("Custom Pricing")); + await waitFor(() => expect(screen.queryByLabelText("Input Cost (per 1M tokens)")).not.toBeInTheDocument()); + await user.click(screen.getByLabelText("Custom Pricing")); + expect(await screen.findByLabelText("Input Cost (per 1M tokens)")).toHaveValue("3"); + + await submit(); + + expect(lastCreatedModel()).toStrictEqual({ + model_name: "gpt-4o", + litellm_params: { + ...alwaysMounted, + ...advancedOpenExtras, + input_cost_per_token: 0.000003, + cache_read_input_token_cost: 0.000003, + }, + model_info: { ...baseModelInfo }, + }); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx index 35be19531d5..59d4f95c038 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx @@ -1,21 +1,28 @@ "use client"; -import { Form } from "antd"; import { useState } from "react"; +import { useForm } from "react-hook-form"; import { useQueryClient } from "@tanstack/react-query"; import AddModelForm from "@/components/add_model/AddModelForm"; import { handleAddModelSubmit } from "@/components/add_model/handle_add_model_submit"; +import { + projectMountedValues, + useMountRegistry, + type MountedFormValues, +} from "@/components/common_components/MountedFormField"; import { Providers, getPlaceholder, getProviderModels } from "@/components/provider_info_helpers"; -import { toast } from "@/lib/toast"; import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"; import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials"; import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload"; +const INITIAL_VALUES: MountedFormValues = { litellm_credential_name: null }; + export default function AddModelPanel() { const { accessToken } = useAuthorized(); - const [form] = Form.useForm(); + const form = useForm({ mode: "onChange", defaultValues: INITIAL_VALUES }); + const registry = useMountRegistry(); const queryClient = useQueryClient(); const { data: modelCostMapData } = useModelCostMap(); const { data: credentialsResponse } = useCredentials(); @@ -26,28 +33,36 @@ export default function AddModelPanel() { const refresh = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] }); - const handleOk = async () => { - try { - const values = await form.validateFields(); - await handleAddModelSubmit(values, accessToken, form, refresh); - } catch (error: any) { - const errorMessages = - error.errorFields?.map((field: any) => `${field.name.join(".")}: ${field.errors.join(", ")}`).join(" | ") || - "Unknown validation error"; - toast.fromError(`Please fill in the following required fields: ${errorMessages}`); + const mountedValues = () => projectMountedValues(registry, form.getValues); + + const handleOk = async (): Promise => { + const isValid = await form.trigger(registry.mountedNames() as string[]); + if (!isValid) { + return false; } + await handleAddModelSubmit( + mountedValues(), + accessToken, + { resetFields: () => form.reset(INITIAL_VALUES) }, + refresh, + ); + return true; }; return ( setProviderModels(getProviderModels(provider, modelCostMapData))} getPlaceholder={getPlaceholder} - uploadProps={vertexCredentialsUploadProps(form)} + uploadProps={vertexCredentialsUploadProps({ + setFieldsValue: (values) => form.setValue("vertex_credentials", values.vertex_credentials), + })} showAdvancedSettings={showAdvancedSettings} setShowAdvancedSettings={setShowAdvancedSettings} teams={teams ?? null} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx index 6112e6bffbd..7251da7c3c3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx @@ -1,10 +1,17 @@ "use client"; -import { Form } from "antd"; +import { useForm } from "react-hook-form"; import CredentialsPanel from "@/components/model_add/CredentialsPanel"; +import type { MountedFormValues } from "@/components/common_components/MountedFormField"; import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload"; export default function LlmCredentialsPanel() { - const [form] = Form.useForm(); - return ; + const form = useForm(); + return ( + form.setValue("vertex_credentials", values.vertex_credentials), + })} + /> + ); } diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx index 8a99d9eae68..26bd9af94a8 100644 --- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.test.tsx @@ -1,11 +1,12 @@ import { renderHook, screen, waitFor, renderWithProviders } from "../../../tests/test-utils"; import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event"; -import { Form } from "antd"; import type { UploadProps } from "antd/es/upload"; import { describe, expect, it, vi } from "vitest"; import type { Team } from "../key_team_helpers/key_list"; import type { CredentialItem } from "../networking"; import { Providers } from "../provider_info_helpers"; +import { projectMountedValues, useMountRegistry, type MountedFormValues } from "../common_components/MountedFormField"; +import { useForm } from "react-hook-form"; import AddModelForm from "./AddModelForm"; vi.mock("../molecules/models/ProviderLogo", () => ({ @@ -131,8 +132,12 @@ const testTeam: Team = { }; const createTestProps = (userRole = "proxy_admin", userId = "user-1", isTeamAdmin = false) => { - const { result } = renderHook(() => Form.useForm()); - const [form] = result.current; + const { result } = renderHook(() => { + const form = useForm({ mode: "onChange" }); + const registry = useMountRegistry(); + return { form, registry }; + }); + const { form, registry } = result.current; const teams = [ { @@ -159,7 +164,9 @@ const createTestProps = (userRole = "proxy_admin", userId = "user-1", isTeamAdmi return { form, - handleOk: vi.fn(), + registry, + mountedValues: () => projectMountedValues(registry, form.getValues), + handleOk: vi.fn().mockResolvedValue(true), setSelectedProvider: vi.fn(), setProviderModelsFn: vi.fn(), getPlaceholder: vi.fn((provider: Providers) => `Enter ${provider} model name`), @@ -331,16 +338,7 @@ describe("AddModelForm", () => { await user.click(screen.getByLabelText("Cache Control Injection Points")); await waitFor(() => expect(screen.queryByText("Add Injection Point")).not.toBeInTheDocument()); }, - // AddModelPanel builds the wire payload from form.validateFields(), which reports exactly - // the mounted registered set. Reading the same instance the same way keeps this on the - // real payload path; a rejection still carries the same `values` object. - mountedValues: async (): Promise> => { - try { - return await props.form.validateFields(); - } catch (error) { - return (error as { values: Record }).values; - } - }, + mountedValues: async (): Promise> => props.mountedValues(), }; }; diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx index de159207078..4d339737ab0 100644 --- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx +++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx @@ -4,11 +4,20 @@ import { useTags } from "@/app/(dashboard)/hooks/tags/useTags"; import { all_admin_roles, isUserTeamAdminForAnyTeam } from "@/utils/roles"; import { modelCreationScope } from "@/utils/modelPermissions"; import { Switch } from "@/components/ui/switch"; -import type { FormInstance } from "antd"; -import { Select as AntdSelect, Button, Card, Col, Form, Modal, Row, Tooltip, Typography, Alert } from "antd"; +import { Field, FieldLabel } from "@/components/shared/form/field"; +import { Select as AntdSelect, Button, Card, Col, Modal, Row, Tooltip, Typography, Alert } from "antd"; import type { UploadProps } from "antd/es/upload"; import React, { useEffect, useMemo, useState } from "react"; +import { FormProvider, useWatch, type UseFormReturn } from "react-hook-form"; import TeamDropdown from "../common_components/team_dropdown"; +import { antdRequired } from "../common_components/antdFormRules"; +import { labelWithHint } from "@/components/shared/form/LabelWithHint"; +import { + MountedFormField, + MountedFormProvider, + type MountRegistry, + type MountedFormValues, +} from "../common_components/MountedFormField"; import type { Team } from "../key_team_helpers/key_list"; import { type CredentialItem, type ProviderCreateInfo, modelAvailableCall } from "../networking"; import { Providers } from "../provider_info_helpers"; @@ -22,8 +31,10 @@ import { TEST_MODES } from "./add_model_modes"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; interface AddModelFormProps { - form: FormInstance; // For the Add Model tab - handleOk: () => Promise; + form: UseFormReturn; // For the Add Model tab + registry: MountRegistry; + mountedValues: () => MountedFormValues; + handleOk: () => Promise; selectedProvider: Providers; setSelectedProvider: (provider: Providers) => void; providerModels: string[]; @@ -36,10 +47,20 @@ interface AddModelFormProps { credentials: CredentialItem[]; } +const connectionTestModelName = (values: MountedFormValues): string | undefined => { + const named = values.model_name || values.model; + if (Array.isArray(named)) { + return named.join(", "); + } + return typeof named === "string" ? named : undefined; +}; + const { Title, Link } = Typography; const AddModelForm: React.FC = ({ form, + registry, + mountedValues, handleOk, selectedProvider, setSelectedProvider, @@ -67,6 +88,7 @@ const AddModelForm: React.FC = ({ const { data: guardrailsData } = useGuardrails(); const guardrailsList = guardrailsData?.guardrails.map((g) => g.guardrail_name); const { data: tagsList } = useTags(); + const selectedCredentialName = useWatch({ control: form.control, name: "litellm_credential_name" }); const handleTestConnection = async () => { setIsTestingConnection(true); @@ -112,274 +134,302 @@ const AddModelForm: React.FC = ({ Add Model -
{ - await handleOk().then(() => { - setTeamAdminSelectedTeam(null); - }); - }} - onFinishFailed={(errorInfo) => {}} - labelCol={{ span: 10 }} - wrapperCol={{ span: 16 }} - labelAlign="left" - > - <> - {requiresTeamScope && ( - <> - - { - setTeamAdminSelectedTeam(value); - }} - /> - - {!teamAdminSelectedTeam && ( - - )} - - )} - {(isAdmin || (isTeamAdmin && teamAdminSelectedTeam)) && ( - <> - - { - 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; - - 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 + + + { + event.preventDefault(); + void handleOk().then((submitted) => { + if (submitted) { + setTeamAdminSelectedTeam(null); } - > - {({ getFieldValue }) => { - const credentialName = getFieldValue("litellm_credential_name"); - // Only show provider specific fields if no credentials selected - if (!credentialName) { - return ( - <> -
-
- OR -
-
- - - ); - } - return null; - }} -
-
-
- Additional Model Info Settings -
-
- {/* Team-only Model Switch - Only show for proxy admins, not team admins */} - {(isAdmin || !isTeamAdmin) && ( - - - - { - setIsTeamOnly(checked); - if (!checked) { - form.setFieldValue("team_id", undefined); - } - }} - disabled={!premiumUser} - aria-label="Team-BYOK Model" - /> - - - - )} - - {/* Conditional Team Selection */} - {isTeamOnly && !requiresTeamScope && ( - - - - )} - {isAdmin && ( + }); + }} + > + <> + {requiresTeamScope && ( <> - - ({ - value: group, - label: group, - }))} - maxTagCount="responsive" - allowClear + {(control) => ( + { + control.onChange(value); + setTeamAdminSelectedTeam(value); + }} + /> + )} + + {!teamAdminSelectedTeam && ( + - + )} )} - + {(isAdmin || (isTeamAdmin && teamAdminSelectedTeam)) && ( + <> + + {(control) => ( + { + control.onChange(value); + setSelectedProvider(value as Providers); + setProviderModelsFn(value as Providers); + form.setValue("model", []); + form.setValue("model_name", undefined); + }} + > + {providerMetadataErrorText && sortedProviderMetadata.length === 0 && ( + + {providerMetadataErrorText} + + )} + {sortedProviderMetadata.map((providerInfo) => { + const displayName = providerInfo.provider_display_name; + const providerKey = providerInfo.provider; + + return ( + +
+ + {displayName} +
+
+ ); + })} +
+ )} +
+ + + {/* Conditionally Render "Public Model Name" */} + + + {/* Select Mode */} + + {(control) => ( + { + control.onChange(value); + 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 + +
+ + + {(control) => ( + + (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) + } + value={control.value as string | null | undefined} + onChange={control.onChange} + onBlur={control.onBlur} + options={[ + { value: null, label: "None" }, + ...credentials.map((credential) => ({ + value: credential.credential_name, + label: credential.credential_name, + })), + ]} + allowClear + /> + )} + + + {/* Only show provider specific fields if no credentials selected */} + {!selectedCredentialName && ( + <> +
+
+ OR +
+
+ + + )} +
+
+ Additional Model Info Settings +
+
+ {/* Team-only Model Switch - Only show for proxy admins, not team admins */} + {(isAdmin || !isTeamAdmin) && ( + + + {labelWithHint( + "Team-BYOK Model", + "Only use this model + credential combination for this team. Useful when teams want to onboard their own OpenAI keys.", + )} + + + + { + setIsTeamOnly(checked); + if (!checked) { + form.setValue("team_id", undefined); + } + }} + disabled={!premiumUser} + aria-label="Team-BYOK Model" + /> + + + + )} + + {/* Conditional Team Selection */} + {isTeamOnly && !requiresTeamScope && ( + + {(control) => ( + + )} + + )} + {isAdmin && ( + <> + + {(control) => ( + ({ + value: group, + label: group, + }))} + maxTagCount="responsive" + allowClear + /> + )} + + + )} + + + )} +
+ + Need Help? + +
+ + +
+
- )} -
- - Need Help? - -
- - -
-
- - + + +
{/* Test Connection Results Modal */} @@ -408,10 +458,10 @@ const AddModelForm: React.FC = ({ { setIsResultModalVisible(false); setIsTestingConnection(false); diff --git a/ui/litellm-dashboard/src/components/add_model/advanced_settings.test.tsx b/ui/litellm-dashboard/src/components/add_model/advanced_settings.test.tsx index 4e5c5f25374..01e00d903aa 100644 --- a/ui/litellm-dashboard/src/components/add_model/advanced_settings.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/advanced_settings.test.tsx @@ -1,5 +1,6 @@ import { act, fireEvent, render, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { MountedFormHost } from "../../../tests/mounted-form-host"; import AdvancedSettings from "./advanced_settings"; const mockUsePtuCostAttributionEnabled = vi.fn(); @@ -12,13 +13,15 @@ const PTU_LABELS = ["PTU Count", "Calculated Cost per PTU / Hour (USD)", "PTU Ef const renderAdvancedSettings = () => render( - {}} - guardrailsList={[]} - tagsList={{}} - accessToken="test-token" - />, + + {}} + guardrailsList={[]} + tagsList={{}} + accessToken="test-token" + /> + , ); describe("AdvancedSettings", () => { diff --git a/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx b/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx index ac3288faf8b..140b4363327 100644 --- a/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx +++ b/ui/litellm-dashboard/src/components/add_model/advanced_settings.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Form, Switch, Select, Tooltip, DatePicker } from "antd"; +import { Switch, Select, Tooltip, DatePicker } from "antd"; import { ChevronDown } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Input } from "@/components/ui/input"; @@ -7,6 +7,9 @@ import { Row, Col, Typography } from "antd"; import TextArea from "antd/es/input/TextArea"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Team } from "../key_team_helpers/key_list"; +import { antdRules } from "../common_components/antdFormRules"; +import { labelWithHint } from "@/components/shared/form/LabelWithHint"; +import { MountedFormField } from "../common_components/MountedFormField"; import CacheControlInjectionPoints, { CACHE_CONTROL_LABEL, CACHE_CONTROL_TOOLTIP, @@ -39,6 +42,31 @@ interface AdvancedSettingsProps { accessToken: string; } +const USAGE_COST_FIELDS = [ + "input_cost_per_token", + "output_cost_per_token", + "cache_read_input_token_cost", + "cache_creation_input_token_cost", + "input_cost_per_second", +]; + +const REVALIDATED_WHEN_PTU_COUNT_CHANGES = [PTU_RATE_FIELD, PTU_START_FIELD, ...USAGE_COST_FIELDS]; + +const validateNumber = (_: unknown, value: unknown) => { + if (!value) { + return Promise.resolve(); + } + if (isNaN(Number(value)) || Number(value) < 0) { + return Promise.reject("Please enter a valid positive number"); + } + return Promise.resolve(); +}; + +const usageCostRules = { + deps: [PTU_COUNT_FIELD], + validate: antdRules({ validator: validateNumber }, ptuNoUsageCostRule(PTU_COUNT_FIELD)), +}; + const AdvancedSettings: React.FC = ({ showAdvancedSettings, setShowAdvancedSettings, @@ -47,95 +75,36 @@ const AdvancedSettings: React.FC = ({ tagsList, accessToken, }) => { - const [form] = Form.useForm(); const [customPricing, setCustomPricing] = React.useState(false); const [pricingModel, setPricingModel] = React.useState<"per_token" | "per_second">("per_token"); const [showCacheControl, setShowCacheControl] = React.useState(false); const ptuCostAttributionEnabled = usePtuCostAttributionEnabled(); - // Add validation function for numbers - const validateNumber = (_: any, value: string) => { - if (!value) { - return Promise.resolve(); - } - if (isNaN(Number(value)) || Number(value) < 0) { - return Promise.reject("Please enter a valid positive number"); - } - return Promise.resolve(); - }; - - // Handle custom pricing changes - const handleCustomPricingChange = (checked: boolean) => { - setCustomPricing(checked); - if (!checked) { - // Clear pricing fields when disabled - form.setFieldsValue({ - input_cost_per_token: undefined, - output_cost_per_token: undefined, - cache_read_input_token_cost: undefined, - cache_creation_input_token_cost: undefined, - input_cost_per_second: undefined, - }); - } - }; - - const handlePassThroughChange = (checked: boolean) => { - const currentParams = form.getFieldValue("litellm_extra_params"); - try { - let paramsObj = currentParams ? JSON.parse(currentParams) : {}; - if (checked) { - paramsObj.use_in_pass_through = true; - } else { - delete paramsObj.use_in_pass_through; - } - // Only set the field value if there are remaining parameters - if (Object.keys(paramsObj).length > 0) { - form.setFieldValue("litellm_extra_params", JSON.stringify(paramsObj, null, 2)); - } else { - form.setFieldValue("litellm_extra_params", ""); - } - } catch (error) { - // If JSON parsing fails, only create new object if checked is true - if (checked) { - form.setFieldValue("litellm_extra_params", JSON.stringify({ use_in_pass_through: true }, null, 2)); - } else { - form.setFieldValue("litellm_extra_params", ""); - } - } - }; - - const handleCacheControlChange = (checked: boolean) => { - setShowCacheControl(checked); - if (!checked) { - const currentParams = form.getFieldValue("litellm_extra_params"); - try { - let paramsObj = currentParams ? JSON.parse(currentParams) : {}; - delete paramsObj.cache_control_injection_points; - if (Object.keys(paramsObj).length > 0) { - form.setFieldValue("litellm_extra_params", JSON.stringify(paramsObj, null, 2)); - } else { - form.setFieldValue("litellm_extra_params", ""); - } - } catch (error) { - form.setFieldValue("litellm_extra_params", ""); - } - } - }; - return ( <> Advanced Settings - + -
- - - +
+ + {(control) => ( + { + control.onChange(checked); + setCustomPricing(checked); + }} + className="bg-gray-600" + /> + )} + - Attached Knowledge Bases (RAG){" "} @@ -151,18 +120,21 @@ const AdvancedSettings: React.FC = ({ } - name="vector_store_ids" className="mt-4" help="Select vector stores to attach. Requests to this model will automatically use these for RAG. Set up vector stores in Tools > Vector Stores." > - {}} - accessToken={accessToken} - placeholder="Select knowledge bases (optional)" - /> - + {(control) => ( + + )} + - Guardrails{" "} @@ -178,199 +150,331 @@ const AdvancedSettings: React.FC = ({ } - name="guardrails" className="mt-4" help="Select existing guardrails. Go to 'Guardrails' tab to create new guardrails." > - ({ value: name, label: name }))} + /> + )} + - - ({ + value: tag.name, + label: tag.name, + title: tag.description || tag.name, + }))} + /> + )} + {ptuCostAttributionEnabled && ( <> - - - + {(control) => ( + + )} + - - - + {(control) => ( + + )} + - - - + {(control) => ( + + )} + - - - + {(control) => ( + + )} + )} {customPricing && ( -
- - { + control.onChange(value); + setPricingModel(value); + }} + options={[ + { value: "per_token", label: "Per Million Tokens" }, + { value: "per_second", label: "Per Second" }, + ]} + /> + )} + {pricingModel === "per_token" ? ( <> - - - - ( + + )} + + - - - ( + + )} + + - - - ( + + )} + + - - + {(control) => ( + + )} + ) : ( - - - + {(control) => ( + + )} + )}
)} - Allow using these credentials in pass through routes.{" "} Learn more - - } + , + )} + className="mb-4 mt-4" > - - + {(control) => ( + + )} + - - - + {(control) => ( + { + control.onChange(checked); + setShowCacheControl(checked); + }} + className="bg-gray-600" + /> + )} + {showCacheControl && ( - - - + + {(control) => ( + ["value"]} + onChange={control.onChange} + /> + )} + )} - -