Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 125 additions & 10 deletions ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ID } from "../../constants";
import { Role, users } from "../../fixtures/users";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";

/**
* Helper to select a provider from the Add Model form dropdown.
*/
async function selectProvider(page: any, providerName: string) {
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill(providerName);
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
}

test.describe("Add Model", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });

test("Able to see all models for a specific provider in the model dropdown", async ({ page }) => {
await page.goto("/ui");

await page.getByText("Models + Endpoints").click();
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();

const providerInputDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerInputDropdown.fill("Anthropic");
await page.waitForTimeout(1000);
await providerInputDropdown.press("Enter");
await page.waitForTimeout(2000);
await selectProvider(page, "Anthropic");

const providerModelsDropdown = page.locator(".ant-select-selection-overflow").first();
await providerModelsDropdown.click();
// The model field should be a multi-select dropdown; click to open it
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();

// Verify provider-specific models are listed
await expect(page.getByTitle("claude-haiku-4-5", { exact: true })).toBeVisible();
});

Expand Down Expand Up @@ -72,4 +82,109 @@ test.describe("Add Model", () => {
await expect(page.getByText("999", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("888", { exact: true })).toBeVisible({ timeout: 10_000 });
});

test("Test connection with bad credentials shows failure", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();

await selectProvider(page, "Anthropic");

// Select model: claude-haiku-4-5
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
await page.getByTitle("claude-haiku-4-5", { exact: true }).click();
await page.keyboard.press("Escape");

// Enter bad API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-bad-key-12345");

// Click Test Connect button by its text
await page.getByRole("button", { name: "Test Connect" }).click();

// Wait for modal to appear and connection test to complete
await expect(page.getByText("Connection Test Results")).toBeVisible({ timeout: 10_000 });

// Verify failure message appears (the test makes a real API call, so it will fail with bad creds)
await expect(page.getByText(/Connection to .* failed/)).toBeVisible({ timeout: 30_000 });
});

test("Add specific model and verify it appears in All Models", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();

await selectProvider(page, "Anthropic");

// Select model: claude-haiku-4-5
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
await page.getByTitle("claude-haiku-4-5", { exact: true }).click();
await page.keyboard.press("Escape");

// Enter any API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-add-test");

// Click Add Model button by its text
await page.getByRole("button", { name: "Add Model" }).last().click();

// Wait for success notification
await expect(page.getByText("created successfully")).toBeVisible({ timeout: 15_000 });

// Navigate to All Models tab
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
await page.waitForTimeout(2000);

// Search for the model we just added
await page.locator('input[placeholder="Search model names..."]').fill("claude-haiku-4-5");
await page.waitForTimeout(1000);

// Verify the model appears in the results count (not "Showing 0 results")
await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 });

// Verify the model name appears in the table body
const tableBody = page.locator("table tbody");
await expect(tableBody.getByText("claude-haiku-4-5").first()).toBeVisible({ timeout: 15_000 });
});

test("Add wildcard route and verify it appears in All Models", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();

await selectProvider(page, "Cohere");

// Select All Cohere Models (Wildcard)
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
const wildcardOption = page.getByTitle(/All .* Models \(Wildcard\)/);
await wildcardOption.click();
await page.keyboard.press("Escape");

// Enter any API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-wildcard-test");

// Click Add Model button by its text
await page.getByRole("button", { name: "Add Model" }).last().click();

// Wait for success notification
await expect(page.getByText("created successfully")).toBeVisible({ timeout: 15_000 });

// Navigate to All Models tab
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
await page.waitForTimeout(2000);

// Search for the wildcard model
await page.locator('input[placeholder="Search model names..."]').fill("cohere");
await page.waitForTimeout(1000);

// Verify the model appears in the results count (not "Showing 0 results")
await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 });

// Verify the wildcard model appears in the table body (wildcard models show as "cohere/*")
const tableBody = page.locator("table tbody");
await expect(tableBody.getByText("cohere/").first()).toBeVisible({ timeout: 15_000 });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ const AllModelsTab = ({
<input
type="text"
placeholder="Search model names..."
data-testid="model-search-input"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
Expand Down Expand Up @@ -472,7 +473,7 @@ const AllModelsTab = ({
{isLoading ? (
<Skeleton.Input active style={{ width: 184, height: 20 }} />
) : (
<span className="text-sm text-gray-700">
<span data-testid="models-results-count" className="text-sm text-gray-700">
{paginationMeta.total_count > 0
? `Showing ${((currentPage - 1) * pageSize) + 1} - ${Math.min(currentPage * pageSize, paginationMeta.total_count)} of ${paginationMeta.total_count} results`
: "Showing 0 results"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,10 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
</Tooltip>
<div className="space-x-2">
<Button onClick={handleTestConnection} loading={isTestingConnection}>
<Button data-testid="test-connect-btn" onClick={handleTestConnection} loading={isTestingConnection}>
Test Connect
</Button>
<Button htmlType="submit">Add Model</Button>
<Button data-testid="add-model-btn" htmlType="submit">Add Model</Button>
</div>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
</>
) : providerModels.length > 0 ? (
<AntSelect
data-testid="model-name-select"
mode="multiple"
allowClear
showSearch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ ${formattedBody}
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
</svg>
</div>
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
<Text data-testid="connection-success-msg" type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
Connection to {modelName} successful!
</Text>
</div>
Expand All @@ -190,7 +190,7 @@ ${formattedBody}
<div>
<div style={{ display: "flex", alignItems: "center", marginBottom: "20px" }}>
<WarningOutlined style={{ color: "#ff4d4f", fontSize: "24px", marginRight: "12px" }} />
<Text type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
<Text data-testid="connection-failure-msg" type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
Connection to {modelName} failed
</Text>
</div>
Expand Down
Loading