Skip to content
Merged
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
107 changes: 106 additions & 1 deletion ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ID } from "../../constants";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ALIAS, E2E_TEAM_CRUD_ID } from "../../constants";
import { Role, users } from "../../fixtures/users";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";
Expand Down Expand Up @@ -150,6 +150,111 @@ test.describe("Add Model", () => {
await expect(tableBody.getByText("claude-haiku-4-5").first()).toBeVisible({ timeout: 15_000 });
});

test("Add team-only model via Team-BYOK toggle and verify it appears with the team", async ({ page, request }) => {
// The Team-BYOK switch is gated on `premiumUser` — without a license set
// for the proxy under test, the toggle is disabled and this manual-QA
// step cannot be exercised.
test.skip(
!process.env.LITELLM_LICENSE,
"LITELLM_LICENSE not set in test env — Team-BYOK switch is disabled",
);

// Make the test idempotent across retries and local reruns: delete any
// Cohere model already scoped to the e2e team before we start, and again
// after we finish. The sibling "Add wildcard route" test creates a
// team-less Cohere wildcard, so we only target rows that have BOTH the
// cohere/* model_name AND team_id == e2e-team-crud.
const masterKey = users[Role.ProxyAdmin].password;
const auth = { Authorization: `Bearer ${masterKey}` };
const deleteTeamScopedCohereModels = async () => {
const res = await request.get("/v2/model/info", { headers: auth });
if (!res.ok()) return;
const body = await res.json();
const matches: Array<{ id: string }> = (body?.data ?? []).filter((m: any) =>
typeof m?.model_name === "string" &&
m.model_name.startsWith("cohere") &&
m?.model_info?.team_id === E2E_TEAM_CRUD_ID,
);
for (const m of matches) {
await request.post("/model/delete", { headers: auth, data: { id: m.id } });
}
};
await deleteTeamScopedCohereModels();

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

await selectProvider(page, "Cohere");

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");

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

// Flip the Team-BYOK switch on (Form.Item label "Team-BYOK Model")
const teamByokRow = page.locator(".ant-form-item", { hasText: "Team-BYOK Model" });
await teamByokRow.getByRole("switch").click();

// The Team dropdown appears underneath once the switch is on. TeamDropdown
// renders its Select.Option children with custom <span>/<Text> markup, so
// the popup items don't carry role="option" — match by text content,
// scoped to the visible dropdown so a stale tag elsewhere in the form
// can't satisfy it.
const teamDropdown = page.getByTestId("team-dropdown");
await expect(teamDropdown).toBeVisible({ timeout: 5_000 });
await teamDropdown.click();
const teamOption = page.locator(".ant-select-dropdown:visible")
.getByText(E2E_TEAM_CRUD_ID)
.first();
await expect(teamOption).toBeVisible({ timeout: 5_000 });
await teamOption.click();

await page.getByRole("button", { name: "Add Model" }).last().click();

// Scope the success toast to antd's notification container so a stale
// success message from an earlier test in the same context can't satisfy
// the assertion.
await expect(page.locator(".ant-notification").getByText("created successfully").last())
.toBeVisible({ timeout: 15_000 });

// Verify the model is now in All Models with the team_id attached. The
// Models table renders team-scoped models with the team id in the row.
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
// Match the sibling tests in this file — networkidle fires before the
// table finishes re-rendering, so give it the same 2s settle before
// searching.
await page.waitForTimeout(2000);

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

// Confirm the search returned at least one result — gives a clear
// failure message when the table is empty instead of timing out on a
// row assertion.
await expect(page.getByTestId("models-results-count")).toHaveText(
/Showing \d+ - \d+ of \d+ results/,
{ timeout: 15_000 },
);

// Stronger than "alias appears somewhere in tbody" — pin the assertion
// to a single row that has BOTH the cohere model_name AND the seeded
// team alias, so a stale cohere row from "Add wildcard route" (no team)
// can't satisfy the check.
const teamCohereRow = page.locator("table tbody tr")
.filter({ hasText: "cohere/" })
.filter({ hasText: E2E_TEAM_CRUD_ALIAS });
await expect(teamCohereRow).toHaveCount(1, { timeout: 15_000 });
} finally {
await deleteTeamScopedCohereModels();
}
});

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();
Expand Down
Loading