mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(ui): show the Add Model picker once the model catalog loads after a provider is picked (#45426)
* fix(ui): show the Add Model picker once the model catalog loads after a provider is picked * fix(ui): show the Add Model picker when a name typed before the catalog loaded was cleared
This commit is contained in:
parent
406514fcaf
commit
b1eae8855e
5 changed files with 102 additions and 13 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { act, renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
|
||||
import userEvent, { PointerEventsCheckLevel } from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AddModelPanel from "./AddModelPanel";
|
||||
|
|
@ -64,6 +65,13 @@ vi.mock("@/app/(dashboard)/hooks/providers/useProviderFields", () => ({
|
|||
{ key: "api_base", label: "API Base", field_type: "text", required: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
provider: "Anthropic",
|
||||
provider_display_name: "Anthropic",
|
||||
litellm_provider: "anthropic",
|
||||
default_model_placeholder: "claude-3-opus",
|
||||
credential_fields: [{ key: "api_key", label: "API Key", field_type: "password", required: false }],
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
|
@ -74,6 +82,26 @@ vi.mock("@/components/vector_store_management/VectorStoreSelector", () => ({
|
|||
default: () => <div data-testid="vector-store-selector" />,
|
||||
}));
|
||||
|
||||
type Catalog = Record<string, { litellm_provider: string }>;
|
||||
|
||||
const createCatalogFeed = () => {
|
||||
let current: Catalog | undefined;
|
||||
const listeners = new Set<() => void>();
|
||||
const subscribe = (listener: () => void) => {
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
};
|
||||
return {
|
||||
useData: () => ({ data: useSyncExternalStore(subscribe, () => current) }),
|
||||
publish: (next: Catalog) => {
|
||||
current = next;
|
||||
listeners.forEach((listener) => listener());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const lastCreatedModel = () => modelCreateCall.mock.calls.at(-1)?.[1];
|
||||
|
||||
const PROXY_ADMIN = {
|
||||
|
|
@ -165,6 +193,70 @@ describe("AddModelPanel submit payload contract", () => {
|
|||
expect(screen.queryByText("openai-gpt-4o-deployment-id")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("offers the catalog models once the catalog arrives after the provider was picked", async () => {
|
||||
const catalog = createCatalogFeed();
|
||||
mockUseModelCostMap.mockImplementation(() => catalog.useData());
|
||||
const { user } = await setup();
|
||||
await user.click(screen.getByRole("combobox", { name: /provider/i }));
|
||||
await user.click(await screen.findByText("OpenAI"));
|
||||
expect(await screen.findByPlaceholderText("gpt-3.5-turbo")).toBeInTheDocument();
|
||||
|
||||
act(() => catalog.publish({ "gpt-4o-2024-08-06": { litellm_provider: "openai" } }));
|
||||
await user.click(await screen.findByPlaceholderText("Select models"));
|
||||
|
||||
expect(await screen.findByText("gpt-4o-2024-08-06")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps a model name typed before the catalog arrives instead of swapping the field under the user", async () => {
|
||||
const catalog = createCatalogFeed();
|
||||
mockUseModelCostMap.mockImplementation(() => catalog.useData());
|
||||
const { user } = await setup();
|
||||
await user.click(screen.getByRole("combobox", { name: /provider/i }));
|
||||
await user.click(await screen.findByText("OpenAI"));
|
||||
await user.type(await screen.findByPlaceholderText("gpt-3.5-turbo"), "my-fine-tune");
|
||||
|
||||
act(() => catalog.publish({ "gpt-4o-2024-08-06": { litellm_provider: "openai" } }));
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("gpt-3.5-turbo")).toHaveValue("my-fine-tune"));
|
||||
expect(screen.queryByPlaceholderText("Select models")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("offers the catalog models when a name typed before the catalog arrived was cleared again", async () => {
|
||||
const catalog = createCatalogFeed();
|
||||
mockUseModelCostMap.mockImplementation(() => catalog.useData());
|
||||
const { user } = await setup();
|
||||
await user.click(screen.getByRole("combobox", { name: /provider/i }));
|
||||
await user.click(await screen.findByText("OpenAI"));
|
||||
const typed = await screen.findByPlaceholderText("gpt-3.5-turbo");
|
||||
await user.type(typed, "my-fine-tune");
|
||||
await user.clear(typed);
|
||||
|
||||
act(() => catalog.publish({ "gpt-4o-2024-08-06": { litellm_provider: "openai" } }));
|
||||
await user.click(await screen.findByPlaceholderText("Select models"));
|
||||
|
||||
expect(await screen.findByText("gpt-4o-2024-08-06")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("swaps the offered models when the provider changes while the catalog stays the same", async () => {
|
||||
const catalog = createCatalogFeed();
|
||||
catalog.publish({
|
||||
"gpt-4o-2024-08-06": { litellm_provider: "openai" },
|
||||
"claude-sonnet-4-5": { litellm_provider: "anthropic" },
|
||||
});
|
||||
mockUseModelCostMap.mockImplementation(() => catalog.useData());
|
||||
const { user } = await setup();
|
||||
const provider = screen.getByRole("combobox", { name: /provider/i });
|
||||
await user.click(provider);
|
||||
await user.click(await screen.findByText("OpenAI"));
|
||||
await user.clear(provider);
|
||||
await user.type(provider, "Anthropic");
|
||||
await user.click(await screen.findByText("Anthropic"));
|
||||
await user.click(await screen.findByPlaceholderText("Select models"));
|
||||
|
||||
expect(await screen.findByText("claude-sonnet-4-5")).toBeInTheDocument();
|
||||
expect(screen.queryByText("gpt-4o-2024-08-06")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends only the always-mounted fields while Advanced Settings stays closed", async () => {
|
||||
const { fillRequired, submit } = await setup();
|
||||
await fillRequired();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm, useWatch } 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";
|
||||
|
|
@ -27,7 +27,11 @@ export default function AddModelPanel() {
|
|||
const { data: credentialsResponse } = useCredentials();
|
||||
const { data: teams } = useTeams();
|
||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(Providers.Anthropic);
|
||||
const [providerModels, setProviderModels] = useState<string[]>([]);
|
||||
const pickedProvider = useWatch({ control: form.control, name: "custom_llm_provider" });
|
||||
const providerModels = useMemo(
|
||||
() => (typeof pickedProvider === "string" ? getProviderModels(pickedProvider, modelCostMapData) : []),
|
||||
[pickedProvider, modelCostMapData],
|
||||
);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
|
||||
const refresh = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] });
|
||||
|
|
@ -57,9 +61,6 @@ export default function AddModelPanel() {
|
|||
selectedProvider={selectedProvider}
|
||||
setSelectedProvider={setSelectedProvider}
|
||||
providerModels={providerModels}
|
||||
setProviderModelsFn={(provider) =>
|
||||
setProviderModels(provider === null ? [] : getProviderModels(provider, modelCostMapData))
|
||||
}
|
||||
getPlaceholder={getPlaceholder}
|
||||
showAdvancedSettings={showAdvancedSettings}
|
||||
setShowAdvancedSettings={setShowAdvancedSettings}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,6 @@ const createTestProps = (userRole = "proxy_admin", userId = "user-1", isTeamAdmi
|
|||
mountedValues: () => projectMountedValues(registry, form.getValues),
|
||||
handleOk: vi.fn().mockResolvedValue(true),
|
||||
setSelectedProvider: vi.fn(),
|
||||
setProviderModelsFn: vi.fn(),
|
||||
getPlaceholder: vi.fn((provider: string) => `Enter ${provider} model name`),
|
||||
setShowAdvancedSettings: vi.fn(),
|
||||
selectedProvider: Providers.OpenAI,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ interface AddModelFormProps {
|
|||
selectedProvider: string | null;
|
||||
setSelectedProvider: (provider: string | null) => void;
|
||||
providerModels: string[];
|
||||
setProviderModelsFn: (provider: string | null) => void;
|
||||
getPlaceholder: (provider: string) => string;
|
||||
showAdvancedSettings: boolean;
|
||||
setShowAdvancedSettings: (show: boolean) => void;
|
||||
|
|
@ -76,7 +75,6 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
selectedProvider,
|
||||
setSelectedProvider,
|
||||
providerModels,
|
||||
setProviderModelsFn,
|
||||
getPlaceholder,
|
||||
showAdvancedSettings,
|
||||
setShowAdvancedSettings,
|
||||
|
|
@ -166,7 +164,6 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
|
|||
|
||||
const applyProviderSelection = (provider: string | null) => {
|
||||
setSelectedProvider(provider);
|
||||
setProviderModelsFn(provider);
|
||||
form.setValue("model", []);
|
||||
form.setValue("model_name", undefined);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -131,12 +131,12 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
) : providerModels.length > 0 ? (
|
||||
) : providerModels.length > 0 && !(typeof control.value === "string" && control.value !== "") ? (
|
||||
<MultiSelect
|
||||
id={control.id}
|
||||
placeholder="Select models"
|
||||
emptyText="No models found"
|
||||
value={(control.value as string[] | undefined) ?? []}
|
||||
value={Array.isArray(control.value) ? control.value : []}
|
||||
onValueChange={(value: string[]) => {
|
||||
control.onChange(value);
|
||||
handleModelChange(value);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue