diff --git a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx index b615a7945e..66ef4d8738 100644 --- a/ui/litellm-dashboard/src/components/callback_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/callback_info_helpers.tsx @@ -46,7 +46,7 @@ const asset_logos_folder = '/ui/assets/logos/'; interface CallbackInfo { logo: string; supports_key_team_logging: boolean; - dynamic_params: Record; + dynamic_params: Record; description: string | null; } @@ -86,7 +86,8 @@ export const callbackInfo: Record = { dynamic_params: { "langsmith_api_key": "password", "langsmith_project": "text", - "langsmith_base_url": "text" + "langsmith_base_url": "text", + "langsmith_sampling_rate": "number" }, description: "Langsmith Logging Integration" }, diff --git a/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx new file mode 100644 index 0000000000..8837784f96 --- /dev/null +++ b/ui/litellm-dashboard/src/components/team/LoggingSettings.test.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders, screen, fireEvent } from "../../../tests/test-utils"; +import LoggingSettings from './LoggingSettings'; + +describe('LoggingSettings', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('passes a number to updateCallbackVar when user inputs a number in NumericalInput', async () => { + const user = userEvent.setup(); + const mockOnChange = vi.fn(); + + // Create initial config with a callback that has number parameters (LangSmith has langsmith_sampling_rate) + const initialValue = [ + { + callback_name: 'langsmith', + callback_type: 'success', + callback_vars: {} + } + ]; + + renderWithProviders( + + ); + + // Find the numerical input for langsmith_sampling_rate + const numericalInput = screen.getByPlaceholderText('os.environ/LANGSMITH_SAMPLING_RATE'); + expect(numericalInput).toBeInTheDocument(); + + // Use fireEvent.change to directly set the value (more reliable for number inputs) + fireEvent.change(numericalInput, { target: { value: '0.75' } }); + + // Verify that onChange was called + expect(mockOnChange).toHaveBeenCalled(); + + // Get the last call to onChange + const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]; + const updatedConfig = lastCall[0]; + + // Verify the structure and that the value is stored as a string (as expected by the component) + expect(updatedConfig).toHaveLength(1); + expect(updatedConfig[0].callback_vars.langsmith_sampling_rate).toBe('0.75'); + }); + + it('displays number type indicator and validation hint for number parameters', () => { + const initialValue = [ + { + callback_name: 'langsmith', + callback_type: 'success', + callback_vars: {} + } + ]; + + renderWithProviders( + + ); + + // Check for the "Number" badge + expect(screen.getByText('Number')).toBeInTheDocument(); + + // Check for the validation hint + expect(screen.getByText('Value must be between 0 and 1')).toBeInTheDocument(); + + // Check that the input has the correct step attribute + const numericalInput = screen.getByPlaceholderText('os.environ/LANGSMITH_SAMPLING_RATE'); + expect(numericalInput).toHaveAttribute('step', '0.01'); + }); + + it('handles number input and text input independently', async () => { + const mockOnChange = vi.fn(); + + // Start with some existing values to simulate a more realistic scenario + const initialValue = [ + { + callback_name: 'langsmith', + callback_type: 'success', + callback_vars: { + langsmith_sampling_rate: '0.3', + langsmith_api_key: 'initial-key' + } + } + ]; + + renderWithProviders( + + ); + + // Find both number and text inputs + const numericalInput = screen.getByPlaceholderText('os.environ/LANGSMITH_SAMPLING_RATE'); + const textInput = screen.getByPlaceholderText('os.environ/LANGSMITH_API_KEY'); + + // Verify initial values are displayed + expect(numericalInput).toHaveValue(0.3); // NumberInput shows numeric value + expect(textInput).toHaveValue('initial-key'); + + // Change the numerical input + fireEvent.change(numericalInput, { target: { value: '0.5' } }); + + // Verify numerical input change was recorded and preserves other values + expect(mockOnChange).toHaveBeenCalled(); + let lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]; + let updatedConfig = lastCall[0]; + expect(updatedConfig[0].callback_vars.langsmith_sampling_rate).toBe('0.5'); + expect(updatedConfig[0].callback_vars.langsmith_api_key).toBe('initial-key'); // Should preserve existing value + + // Change the text input (this tests that text inputs work independently) + fireEvent.change(textInput, { target: { value: 'test-api-key' } }); + + // Verify text input change was also recorded + lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]; + updatedConfig = lastCall[0]; + expect(updatedConfig[0].callback_vars.langsmith_api_key).toBe('test-api-key'); + // The component preserves the original initial value since we're starting from initial state each time + expect(updatedConfig[0].callback_vars.langsmith_sampling_rate).toBe('0.3'); // Preserves initial value + }); + + it('correctly handles numerical input with decimal values', () => { + const mockOnChange = vi.fn(); + + const initialValue = [ + { + callback_name: 'langsmith', + callback_type: 'success', + callback_vars: {} + } + ]; + + renderWithProviders( + + ); + + const numericalInput = screen.getByPlaceholderText('os.environ/LANGSMITH_SAMPLING_RATE'); + + // Test various decimal values + const testValues = ['0.1', '0.25', '0.5', '0.75', '1.0']; + + testValues.forEach(value => { + fireEvent.change(numericalInput, { target: { value } }); + + const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]; + const updatedConfig = lastCall[0]; + expect(updatedConfig[0].callback_vars.langsmith_sampling_rate).toBe(value); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx b/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx index c81acefebd..f0b660d01d 100644 --- a/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx +++ b/ui/litellm-dashboard/src/components/team/LoggingSettings.tsx @@ -6,6 +6,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { Button, Card, TextInput } from '@tremor/react'; import { PlusIcon, TrashIcon, CogIcon, BanIcon } from '@heroicons/react/outline'; import { callbackInfo, Callbacks, callback_map, mapDisplayToInternalNames } from '../callback_info_helpers'; +import NumericalInput from '../shared/numerical_input'; const { Option } = Select; @@ -126,13 +127,33 @@ const LoggingSettings: React.FC = ({ Sensitive )} + {paramType === 'number' && ( + + Number + + )} - updateCallbackVar(configIndex, paramName, e.target.value)} - /> + {paramType === 'number' && ( + + Value must be between 0 and 1 + + )} + {paramType === 'number' ? ( + updateCallbackVar(configIndex, paramName, e.target.value)} + /> + ) : ( + updateCallbackVar(configIndex, paramName, e.target.value)} + /> + )} ))}