diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.test.tsx index 1b443e98495..1c2cb426f23 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.test.tsx @@ -1,4 +1,4 @@ -import { act, render, screen, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import AdditionalModelSettings from "./AdditionalModelSettings"; @@ -128,4 +128,47 @@ describe("AdditionalModelSettings", () => { expect(onMockTestFallbacksChange).toHaveBeenCalledWith(false); }); }); + + it("should keep a half-typed decimal temperature instead of rewriting it", async () => { + const onTemperatureChange = vi.fn(); + + render(); + + const temperatureField = screen.getByLabelText("Temperature value") as HTMLInputElement; + fireEvent.change(temperatureField, { target: { value: "0." } }); + + expect(temperatureField.value).toBe("0."); + + fireEvent.change(temperatureField, { target: { value: "0.5" } }); + + expect(temperatureField.value).toBe("0.5"); + expect(onTemperatureChange).toHaveBeenLastCalledWith(0.5); + }); + + it("should let the max tokens field be cleared instead of snapping to a value", async () => { + const user = userEvent.setup(); + const onMaxTokensChange = vi.fn(); + + render(); + + const maxTokensField = screen.getByLabelText("Max tokens value"); + await user.clear(maxTokensField); + + expect((maxTokensField as HTMLInputElement).value).toBe(""); + }); + + it("should clamp an out-of-range temperature once the field is left", async () => { + const user = userEvent.setup(); + const onTemperatureChange = vi.fn(); + + render(); + + const temperatureField = screen.getByLabelText("Temperature value"); + await user.clear(temperatureField); + await user.type(temperatureField, "9"); + await user.tab(); + + expect((temperatureField as HTMLInputElement).value).toBe("2"); + expect(onTemperatureChange).toHaveBeenLastCalledWith(2); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx index 4deb7051954..af306e0eb2e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx @@ -42,6 +42,8 @@ const AdditionalModelSettings: React.FC = ({ externalUseAdvancedParams !== undefined ? externalUseAdvancedParams : internalUseAdvancedParams; const [localTemperature, setLocalTemperature] = useState(temperature); const [localMaxTokens, setLocalMaxTokens] = useState(maxTokens); + const [temperatureText, setTemperatureText] = useState(String(temperature)); + const [maxTokensText, setMaxTokensText] = useState(String(maxTokens)); const streamingId = useId(); const advancedId = useId(); @@ -51,24 +53,46 @@ const AdditionalModelSettings: React.FC = ({ useEffect(() => { setLocalTemperature(temperature); + setTemperatureText(String(temperature)); }, [temperature]); useEffect(() => { setLocalMaxTokens(maxTokens); + setMaxTokensText(String(maxTokens)); }, [maxTokens]); const handleTemperatureChange = (value: number) => { const newValue = clamp(Number.isFinite(value) ? value : 1.0, 0, 2); setLocalTemperature(newValue); + setTemperatureText(String(newValue)); onTemperatureChange?.(newValue); }; const handleMaxTokensChange = (value: number) => { const newValue = clamp(Number.isFinite(value) ? Math.round(value) : 1000, 1, 32768); setLocalMaxTokens(newValue); + setMaxTokensText(String(newValue)); onMaxTokensChange?.(newValue); }; + const handleTemperatureTyped = (raw: string) => { + setTemperatureText(raw); + const parsed = Number(raw); + if (raw.trim() !== "" && Number.isFinite(parsed) && parsed >= 0 && parsed <= 2) { + setLocalTemperature(parsed); + onTemperatureChange?.(parsed); + } + }; + + const handleMaxTokensTyped = (raw: string) => { + setMaxTokensText(raw); + const parsed = Number(raw); + if (raw.trim() !== "" && Number.isInteger(parsed) && parsed >= 1 && parsed <= 32768) { + setLocalMaxTokens(parsed); + onMaxTokensChange?.(parsed); + } + }; + const handleUseAdvancedParamsChange = (checked: boolean) => { if (onUseAdvancedParamsChange) { onUseAdvancedParamsChange(checked); @@ -175,14 +199,14 @@ const AdditionalModelSettings: React.FC = ({ handleTemperatureChange(Number(event.target.value))} + onChange={(event) => handleTemperatureTyped(event.target.value)} + onBlur={() => handleTemperatureChange(Number(temperatureText))} /> = ({ handleMaxTokensChange(Number(event.target.value))} + onChange={(event) => handleMaxTokensTyped(event.target.value)} + onBlur={() => handleMaxTokensChange(Number(maxTokensText))} /> { expect(screen.getByText("All MCP Servers")).toBeInTheDocument(); }); }); + + it("should keep the chosen endpoint when a model that endpoint can serve is picked", async () => { + (fetchModelsModule.fetchAvailableModels as ReturnType).mockResolvedValueOnce([ + { model_group: "ChatModel", mode: "chat" }, + ]); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByText("Test Key")).toBeInTheDocument(); + }); + + await selectComboboxOption("Select an endpoint", "/v1/responses"); + await selectComboboxOption("Select a Model", "ChatModel"); + + expect(screen.getByPlaceholderText("Select an endpoint")).toHaveValue("/v1/responses"); + }); + + it("should still switch endpoint when the picked model cannot be served by it", async () => { + (fetchModelsModule.fetchAvailableModels as ReturnType).mockResolvedValueOnce([ + { model_group: "SpeechModel", mode: "audio_speech" }, + ]); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByText("Test Key")).toBeInTheDocument(); + }); + + await selectComboboxOption("Select an endpoint", "/v1/responses"); + await selectComboboxOption("Select a Model", "SpeechModel"); + + expect(screen.getByPlaceholderText("Select an endpoint")).toHaveValue("/v1/audio/speech"); + }); + + it("should attach an audio file dropped on the transcription upload area", async () => { + render( + , + ); + + await waitFor(() => { + expect(screen.getByText("Test Key")).toBeInTheDocument(); + }); + + await selectComboboxOption("Select an endpoint", "/v1/audio/transcriptions"); + + const dropZone = (await screen.findByText("Click or drag audio file to upload")).closest("label"); + const file = new File(["clip"], "clip.wav", { type: "audio/wav" }); + fireEvent.drop(dropZone as HTMLElement, { dataTransfer: { files: [file] } }); + + expect(await screen.findByText("clip.wav")).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx index 384d10fd91b..f3d79dd3a2f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx @@ -1149,12 +1149,27 @@ const ChatUI: React.FC = ({ NotificationsManager.success("Chat history cleared."); }; + const currentEndpointServes = (mode: string): boolean => { + const modelEndpoint = getEndpointType(mode); + if ( + endpointType === EndpointType.RESPONSES || + endpointType === EndpointType.ANTHROPIC_MESSAGES || + endpointType === EndpointType.INTERACTIONS + ) { + return modelEndpoint === endpointType || modelEndpoint === EndpointType.CHAT; + } + if (endpointType === EndpointType.IMAGE_EDITS) { + return modelEndpoint === endpointType || modelEndpoint === EndpointType.IMAGE; + } + return modelEndpoint === endpointType; + }; + const onModelChange = (value: string) => { setSelectedModel(value); setShowCustomModelInput(value === "custom"); const model = modelInfo.find((option) => option.model_group === value); - if (model?.mode) { + if (model?.mode && !currentEndpointServes(model.mode)) { setEndpointType(getEndpointType(model.mode)); } }; @@ -1805,7 +1820,14 @@ const ChatUI: React.FC = ({ {endpointType === EndpointType.IMAGE_EDITS && (
{uploadedImages.length === 0 ? ( -