diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx
index 410758491fe..844ac9f44e2 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.integration.test.tsx
@@ -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: () =>
,
}));
+type Catalog = Record;
+
+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();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx
index 9dacabe506b..9fbd138bda4 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx
@@ -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(Providers.Anthropic);
- const [providerModels, setProviderModels] = useState([]);
+ 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}
diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx
index 12efc5beb7c..220d46647f6 100644
--- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.integration.test.tsx
@@ -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,
diff --git a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
index a416bf87602..b5b6c439dc2 100644
--- a/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx
@@ -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 = ({
selectedProvider,
setSelectedProvider,
providerModels,
- setProviderModelsFn,
getPlaceholder,
showAdvancedSettings,
setShowAdvancedSettings,
@@ -166,7 +164,6 @@ const AddModelForm: React.FC = ({
const applyProviderSelection = (provider: string | null) => {
setSelectedProvider(provider);
- setProviderModelsFn(provider);
form.setValue("model", []);
form.setValue("model_name", undefined);
};
diff --git a/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx b/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx
index 8cf4b077f39..529c0e2a63e 100644
--- a/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/litellm_model_name.tsx
@@ -131,12 +131,12 @@ const LiteLLMModelNameField: React.FC = ({
}
}}
/>
- ) : providerModels.length > 0 ? (
+ ) : providerModels.length > 0 && !(typeof control.value === "string" && control.value !== "") ? (
{
control.onChange(value);
handleModelChange(value);