From 44614c43c6973c1a218920ee296c2f7b34dbad29 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Mon, 13 Apr 2026 16:27:39 -0700 Subject: [PATCH] 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. --- .../e2e_tests/fixtures/config.yml | 1 + .../tests/modelsPage/addModel.spec.ts | 66 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/config.yml b/ui/litellm-dashboard/e2e_tests/fixtures/config.yml index 438c236b03b..3d250984bca 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/config.yml +++ b/ui/litellm-dashboard/e2e_tests/fixtures/config.yml @@ -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 diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts index 2ab782d5678..3bf4cd925d7 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts @@ -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 }, + }); + } + } + }); });