playground-test-fallbacks: Added fallback testing as a toggle to ChatUI. Purposely fails first call, then attempts to use fallbacks.

This commit is contained in:
Alejandro Tapia 2026-02-11 17:31:05 -08:00
parent aad90ede43
commit ca94095e24
6 changed files with 209 additions and 2 deletions

View file

@ -1,4 +1,4 @@
import { render, screen, waitFor } from "@testing-library/react";
import { act, 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";
@ -47,4 +47,55 @@ describe("AdditionalModelSettings", () => {
expect(temperatureSlider).not.toBeDisabled();
expect(maxTokensSlider).not.toBeDisabled();
});
it("should not show Simulate failure to test fallbacks when onMockTestFallbacksChange is not provided", () => {
render(<AdditionalModelSettings />);
expect(screen.queryByText(/Simulate failure to test fallbacks/i)).not.toBeInTheDocument();
});
it("should show and toggle Simulate failure to test fallbacks when callback is provided", async () => {
const user = userEvent.setup();
const onMockTestFallbacksChange = vi.fn();
let currentValue = false;
const handleChange = (value: boolean) => {
currentValue = value;
onMockTestFallbacksChange(value);
};
const { rerender } = render(
<AdditionalModelSettings
mockTestFallbacks={currentValue}
onMockTestFallbacksChange={handleChange}
/>,
);
const fallbacksCheckbox = screen.getByRole("checkbox", {
name: /Simulate failure to test fallbacks/i,
});
expect(fallbacksCheckbox).toBeInTheDocument();
expect(fallbacksCheckbox).not.toBeChecked();
await act(async () => {
await user.click(fallbacksCheckbox);
});
await waitFor(() => {
expect(onMockTestFallbacksChange).toHaveBeenCalledWith(true);
});
rerender(
<AdditionalModelSettings
mockTestFallbacks={currentValue}
onMockTestFallbacksChange={handleChange}
/>,
);
await act(async () => {
await user.click(screen.getByRole("checkbox", { name: /Simulate failure to test fallbacks/i }));
});
await waitFor(() => {
expect(onMockTestFallbacksChange).toHaveBeenCalledWith(false);
});
});
});

View file

@ -10,6 +10,8 @@ interface AdditionalModelSettingsProps {
onTemperatureChange?: (value: number) => void;
onMaxTokensChange?: (value: number) => void;
onUseAdvancedParamsChange?: (value: boolean) => void;
mockTestFallbacks?: boolean;
onMockTestFallbacksChange?: (value: boolean) => void;
}
const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
@ -19,6 +21,8 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
onTemperatureChange,
onMaxTokensChange,
onUseAdvancedParamsChange,
mockTestFallbacks,
onMockTestFallbacksChange,
}) => {
const [internalUseAdvancedParams, setInternalUseAdvancedParams] = useState(false);
const useAdvancedParams =
@ -64,6 +68,20 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
<span className="font-medium">Use Advanced Parameters</span>
</Checkbox>
{onMockTestFallbacksChange && (
<Tooltip title="Causes the first request to fail so the router tries fallbacks (if configured). Use this to verify your fallback setup.">
<div className="flex items-center gap-1">
<Checkbox
checked={mockTestFallbacks ?? false}
onChange={(e) => onMockTestFallbacksChange(e.target.checked)}
>
<span className="font-medium">Simulate failure to test fallbacks</span>
</Checkbox>
<InfoCircleOutlined className="text-xs text-gray-400 cursor-help shrink-0" />
</div>
</Tooltip>
)}
<div className="space-y-4 transition-opacity duration-200" style={{ opacity: disabledOpacity }}>
<div>
<div className="flex items-center justify-between mb-2">

View file

@ -271,6 +271,70 @@ describe("ChatUI", () => {
});
});
it("should show Simulate failure to test fallbacks in Model Settings when chat endpoint is selected", async () => {
render(
<ChatUI
accessToken="1234567890"
token="1234567890"
userRole="user"
userID="1234567890"
disabledPersonalKeyCreation={false}
/>,
);
await waitFor(() => {
expect(screen.getByText("Test Key")).toBeInTheDocument();
});
// Model Settings button only appears when a chat model is selected; select "Model 1" first
const selectModelLabel = screen.getByText("Select Model");
const modelSelectContainer = selectModelLabel.closest("div");
const modelSelect = modelSelectContainer?.querySelector(".ant-select-selector");
expect(modelSelect).toBeTruthy();
await act(async () => {
fireEvent.mouseDown(modelSelect!);
});
await waitFor(() => {
expect(screen.getAllByText("Model 1").length).toBeGreaterThan(0);
});
// Ant Design Select options may not have role="option"; click the dropdown option by text
const model1Options = screen.getAllByText("Model 1");
await act(async () => {
fireEvent.click(model1Options[model1Options.length - 1]);
});
await waitFor(() => {
const modelSettingsButton = screen.getByTestId("model-settings-button");
expect(modelSettingsButton).toBeInTheDocument();
});
const modelSettingsButton = screen.getByTestId("model-settings-button");
await act(async () => {
fireEvent.click(modelSettingsButton);
});
await waitFor(() => {
expect(screen.getByText("Model Settings")).toBeInTheDocument();
expect(screen.getByText(/Simulate failure to test fallbacks/i)).toBeInTheDocument();
});
const fallbacksCheckbox = screen.getByRole("checkbox", {
name: /Simulate failure to test fallbacks/i,
});
expect(fallbacksCheckbox).not.toBeChecked();
await act(async () => {
fireEvent.click(fallbacksCheckbox);
});
await waitFor(() => {
expect(screen.getByRole("checkbox", { name: /Simulate failure to test fallbacks/i })).toBeChecked();
});
});
it("should show Fill button and populate customProxyBaseUrl when proxySettings.LITELLM_UI_API_DOC_BASE_URL is provided", async () => {
const testProxyUrl = "http://localhost:5000";

View file

@ -229,6 +229,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
const [temperature, setTemperature] = useState<number>(1.0);
const [maxTokens, setMaxTokens] = useState<number>(2048);
const [useAdvancedParams, setUseAdvancedParams] = useState<boolean>(false);
const [mockTestFallbacks, setMockTestFallbacks] = useState<boolean>(false);
// Code Interpreter state (using custom hook)
const codeInterpreter = useCodeInterpreter();
@ -982,6 +983,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
mcpServers,
mcpServerToolRestrictions,
handleMCPEvent,
mockTestFallbacks,
);
} else if (endpointType === EndpointType.IMAGE) {
// For image generation
@ -1401,6 +1403,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
onTemperatureChange={setTemperature}
onMaxTokensChange={setMaxTokens}
onUseAdvancedParamsChange={setUseAdvancedParams}
mockTestFallbacks={mockTestFallbacks}
onMockTestFallbacksChange={setMockTestFallbacks}
/>
}
title="Model Settings"
@ -1412,6 +1416,8 @@ const ChatUI: React.FC<ChatUIProps> = ({
size="small"
icon={<SettingOutlined />}
className="text-gray-500 hover:text-gray-700"
aria-label="Model Settings"
data-testid="model-settings-button"
/>
</Popover>
) : (
@ -2390,7 +2396,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
</Card>
<Modal
title="Generated Code"
visible={isGetCodeModalVisible}
open={isGetCodeModalVisible}
onCancel={() => setIsGetCodeModalVisible(false)}
footer={null}
width={800}

View file

@ -190,4 +190,70 @@ describe("chat_completion", () => {
expect(secondTool.require_approval).toBe("never");
expect(secondTool.allowed_tools).toEqual(["toolC"]);
});
it("should include mock_testing_fallbacks in request body when mockTestFallbacks is true", async () => {
await makeOpenAIChatCompletionRequest(
mockChatHistory,
mockUpdateUI,
"gpt-4",
"test-token",
undefined, // tags
undefined, // signal
undefined, // onReasoningContent
undefined, // onTimingData
undefined, // onUsageData
undefined, // traceId
undefined, // vector_store_ids
undefined, // guardrails
undefined, // policies
undefined, // selectedMCPServers
undefined, // onImageGenerated
undefined, // onSearchResults
undefined, // temperature
undefined, // max_tokens
undefined, // onTotalLatency
undefined, // customBaseUrl
undefined, // mcpServers
undefined, // mcpServerToolRestrictions
undefined, // onMCPEvent
true, // mockTestFallbacks
);
expect(mockCreate).toHaveBeenCalledTimes(1);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs.mock_testing_fallbacks).toBe(true);
});
it("should not include mock_testing_fallbacks in request body when mockTestFallbacks is false or undefined", async () => {
await makeOpenAIChatCompletionRequest(
mockChatHistory,
mockUpdateUI,
"gpt-4",
"test-token",
undefined, // tags
undefined, // signal
undefined, // onReasoningContent
undefined, // onTimingData
undefined, // onUsageData
undefined, // traceId
undefined, // vector_store_ids
undefined, // guardrails
undefined, // policies
undefined, // selectedMCPServers
undefined, // onImageGenerated
undefined, // onSearchResults
undefined, // temperature
undefined, // max_tokens
undefined, // onTotalLatency
undefined, // customBaseUrl
undefined, // mcpServers
undefined, // mcpServerToolRestrictions
undefined, // onMCPEvent
false, // mockTestFallbacks
);
expect(mockCreate).toHaveBeenCalledTimes(1);
const callArgs = mockCreate.mock.calls[0][0];
expect(callArgs).not.toHaveProperty("mock_testing_fallbacks");
});
});

View file

@ -30,6 +30,7 @@ export async function makeOpenAIChatCompletionRequest(
mcpServers?: MCPServer[],
mcpServerToolRestrictions?: Record<string, string[]>,
onMCPEvent?: (event: MCPEvent) => void,
mockTestFallbacks?: boolean,
) {
// base url should be the current base_url
const isLocal = process.env.NODE_ENV === "development";
@ -115,6 +116,7 @@ export async function makeOpenAIChatCompletionRequest(
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
...(temperature !== undefined ? { temperature } : {}),
...(max_tokens !== undefined ? { max_tokens } : {}),
...(mockTestFallbacks ? { mock_testing_fallbacks: true } : {}),
},
{ signal },
);