diff --git a/ui/litellm-dashboard/src/components/add_fallbacks.test.tsx b/ui/litellm-dashboard/src/components/add_fallbacks.test.tsx new file mode 100644 index 0000000000..102b55af8a --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_fallbacks.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import AddFallbacks from "./add_fallbacks"; + +vi.mock("./networking", () => ({ + setCallbacksCall: vi.fn(), +})); + +vi.mock("./playground/llm_calls/fetch_models", () => ({ + fetchAvailableModels: vi.fn(() => + Promise.resolve([ + { model_group: "gpt-4" }, + { model_group: "gpt-3.5-turbo" }, + { model_group: "claude-3-opus" }, + { model_group: "claude-3-sonnet" }, + ]), + ), +})); + +vi.mock("./molecules/notifications_manager", () => ({ + default: { + success: vi.fn(), + fromBackend: vi.fn(), + }, +})); + +describe("AddFallbacks", () => { + const mockAccessToken = "test-token"; + const mockRouterSettings = { fallbacks: [] }; + const mockSetRouterSettings = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should render the component", () => { + render( + , + ); + + expect(screen.getByRole("button", { name: /Add Fallbacks/i })).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/add_fallbacks.tsx b/ui/litellm-dashboard/src/components/add_fallbacks.tsx index 87cbc1a08c..97b020005a 100644 --- a/ui/litellm-dashboard/src/components/add_fallbacks.tsx +++ b/ui/litellm-dashboard/src/components/add_fallbacks.tsx @@ -2,13 +2,12 @@ * Modal to add fallbacks to the proxy router config */ -import React, { useState, useEffect } from "react"; import { Button } from "@tremor/react"; -import { SearchSelect, SearchSelectItem } from "@tremor/react"; -import { setCallbacksCall } from "./networking"; -import { Modal, Form } from "antd"; -import { fetchAvailableModels, ModelGroup } from "./playground/llm_calls/fetch_models"; +import { Form, Modal, Select } from "antd"; +import React, { useEffect, useState } from "react"; import NotificationManager from "./molecules/notifications_manager"; +import { setCallbacksCall } from "./networking"; +import { fetchAvailableModels, ModelGroup } from "./playground/llm_calls/fetch_models"; interface AddFallbacksProps { models?: string[]; @@ -134,10 +133,10 @@ const AddFallbacks: React.FC = ({ models, accessToken, router rules={[{ required: true, message: "Please select the primary model that needs fallbacks" }]} className="!mb-0" > - { + value={selectedModel || undefined} + onChange={(value: string) => { setSelectedModel(value); // Remove the selected model from fallbacks if it was selected const updatedFallbacks = selectedFallbacks.filter((model) => model !== value); @@ -145,15 +144,18 @@ const AddFallbacks: React.FC = ({ models, accessToken, router form.setFieldValue("models", updatedFallbacks); form.setFieldValue("model_name", value); }} + showSearch + allowClear + style={{ width: "100%" }} > {Array.from(new Set(modelInfo.map((option) => option.model_group))).map( (model: string, index: number) => ( - + {model} - + ), )} - +

This is the primary model that users will request

@@ -200,26 +202,29 @@ const AddFallbacks: React.FC = ({ models, accessToken, router )} {/* Model selector */} - { + value={undefined} + onChange={(value: string) => { if (value && !selectedFallbacks.includes(value)) { const newFallbacks = [...selectedFallbacks, value]; setSelectedFallbacks(newFallbacks); form.setFieldValue("models", newFallbacks); } }} + showSearch + allowClear + style={{ width: "100%" }} > {Array.from(new Set(modelInfo.map((option) => option.model_group))) .filter((data: string) => data !== selectedModel && !selectedFallbacks.includes(data)) .sort() .map((model: string) => ( - + {model} - + ))} - +

Order matters: Models will be tried in the order shown above (1st, 2nd, 3rd, etc.)