test(e2e): add edit team model TPM/RPM limits test

Covers the full write-path flow for team-scoped models on the Models +
Endpoints page: create via /model/new, click the row to open the detail
view, click Edit Settings, change TPM/RPM, click Save Changes, assert
the new values render back. Cleans up via /model/delete in finally so
reruns stay deterministic.

Requires store_model_in_db: true in the fixture general_settings so the
proxy accepts /model/new and /model/delete. No existing test in the
dashboard e2e suite reads the all-models table or hits the model CRUD
endpoints, so enabling the flag has no cross-test impact.
This commit is contained in:
Ryan Crabbe 2026-04-13 16:27:39 -07:00
parent 548225ef31
commit 44614c43c6
No known key found for this signature in database
2 changed files with 66 additions and 1 deletions

View file

@ -14,3 +14,4 @@ general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
store_prompts_in_spend_logs: true
store_model_in_db: true

View file

@ -1,5 +1,6 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ID } from "../../constants";
import { Role, users } from "../../fixtures/users";
test.describe("Add Model", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
@ -20,4 +21,67 @@ test.describe("Add Model", () => {
await providerModelsDropdown.click();
await expect(page.getByTitle("claude-haiku-4-5", { exact: true })).toBeVisible();
});
test("Edit team model TPM and RPM limits", async ({ page }) => {
const masterKey = users[Role.ProxyAdmin].password;
const modelName = `e2e-team-model-${Date.now()}`;
let createdModelId: string | null = null;
// Create a team-scoped model via API so the test has something to edit
const createResponse = await page.request.post("/model/new", {
headers: { Authorization: `Bearer ${masterKey}` },
data: {
model_name: modelName,
litellm_params: {
model: "openai/fake-gpt-4",
api_base: "http://127.0.0.1:8090/v1",
api_key: "fake-key",
tpm: 100,
rpm: 200,
},
model_info: {
team_id: E2E_TEAM_CRUD_ID,
},
},
});
expect(createResponse.ok()).toBe(true);
const created = await createResponse.json();
createdModelId = created?.model_info?.id ?? created?.model_id ?? null;
expect(createdModelId).toBeTruthy();
try {
// Navigate to Models + Endpoints
await page.goto("/ui");
await page.getByText("Models + Endpoints").click();
// Click the new model row to open its detail view. The table renders
// a clickable outer row plus a nested detail row for the same model,
// so we target the first match (outer row) explicitly.
const modelRow = page.locator("tr", { hasText: modelName }).first();
await expect(modelRow).toBeVisible({ timeout: 10_000 });
await modelRow.click();
await expect(page.getByText("Back to Models").first()).toBeVisible({ timeout: 10_000 });
// Edit Settings → change TPM/RPM → Save
await page.getByRole("button", { name: "Edit Settings" }).click();
await page.getByPlaceholder("Enter TPM").fill("999");
await page.getByPlaceholder("Enter RPM").fill("888");
await page.getByRole("button", { name: "Save Changes" }).click();
// Verify the new values render back in view mode
await expect(page.getByText("999", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("888", { exact: true })).toBeVisible({ timeout: 10_000 });
} finally {
if (createdModelId) {
await page.request.post("/model/delete", {
headers: { Authorization: `Bearer ${masterKey}` },
data: { id: createdModelId },
});
}
}
});
});