diff --git a/.circleci/config.yml b/.circleci/config.yml index a518628afb..0adfd5be52 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3496,8 +3496,13 @@ jobs: command: | npx playwright test e2e_ui_tests/ --reporter=html --output=test-results no_output_timeout: 120m - - store_test_results: + - store_artifacts: path: test-results + destination: playwright-results + + - store_artifacts: + path: playwright-report + destination: playwright-report test_nonroot_image: machine: diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 8e2c229c91..e1d5a90dc7 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -556,10 +556,11 @@ else: global_max_parallel_request_retry_timeout_env ) -ui_link = f"{server_root_path}/ui/" +ui_link = f"{server_root_path}/ui" +fallback_login_link = f"{server_root_path}/fallback/login" model_hub_link = f"{server_root_path}/ui/model_hub_table" ui_message = ( - f"šŸ‘‰ [```LiteLLM Admin Panel on /ui```]({ui_link}). Create, Edit Keys with SSO" + f"šŸ‘‰ [```LiteLLM Admin Panel on /ui```]({ui_link}). Create, Edit Keys with SSO. Having issues? Try [```Fallback Login```]({fallback_login_link})" ) ui_message += "\n\nšŸ’ø [```LiteLLM Model Cost Map```](https://models.litellm.ai/)." diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts index f691389de5..e5a397a6a6 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/login_to_ui.spec.ts @@ -11,15 +11,20 @@ import { test, expect } from "@playwright/test"; test("admin login test", async ({ page }) => { // Go to the specified URL await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); + + await page.screenshot({ path: "test-results/login_before.png" }); // Enter "admin" in the username input field - await page.fill('input[name="username"]', "admin"); + await page.fill('input[placeholder="Enter your username"]', "admin"); // Enter "gm" in the password input field - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your password"]', "gm"); + + page.screenshot({ path: "test-results/login_after_inputs.png" }); // Optionally, you can add an assertion to verify the login button is enabled - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); // Optionally, you can click the login button to submit the form diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts index 18bc91c097..4e4bd2fcd9 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/require_auth_for_dashboard.spec.ts @@ -2,15 +2,19 @@ import { test, expect } from "@playwright/test"; test.describe("Authentication Checks", () => { - test("should redirect unauthenticated user from a protected page", async ({ page }) => { + test("should redirect unauthenticated user from a protected page", async ({ + page, + }) => { test.setTimeout(30000); page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); const protectedPageUrl = "http://localhost:4000/ui?page=llm-playground"; - const expectedRedirectUrl = "http://localhost:4000/sso/key/generate"; + const expectedRedirectUrl = "http://localhost:4000/ui/login/"; - console.log(`Attempting to navigate to protected page: ${protectedPageUrl}`); + console.log( + `Attempting to navigate to protected page: ${protectedPageUrl}` + ); await page.goto(protectedPageUrl); @@ -20,7 +24,9 @@ test.describe("Authentication Checks", () => { await page.waitForURL(expectedRedirectUrl, { timeout: 10000 }); console.log(`Waited for URL. Current URL is now: ${page.url()}`); } catch (error) { - console.error(`Timeout waiting for URL: ${expectedRedirectUrl}. Current URL: ${page.url()}`); + console.error( + `Timeout waiting for URL: ${expectedRedirectUrl}. Current URL: ${page.url()}` + ); await page.screenshot({ path: "redirect-fail-screenshot.png" }); throw error; } diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts index 7b9da6a27d..d72c44ab8c 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/search_users.spec.ts @@ -21,17 +21,22 @@ test("user search test", async ({ page }) => { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); + page.screenshot({ path: "test-results/search_users_before_login.png" }); + // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); @@ -128,17 +133,20 @@ test("user filter test", async ({ page }) => { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts index e578ab57e3..2aae9e2bb5 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_internal_user.spec.ts @@ -7,15 +7,18 @@ import { test, expect } from "@playwright/test"; test("view internal user page", async ({ page }) => { // Go to the specified URL await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); + + page.screenshot({ path: "test-results/view_internal_user_before_login.png" }); // Enter "admin" in the username input field - await page.fill('input[name="username"]', "admin"); + await page.fill('input[placeholder="Enter your username"]', "admin"); // Enter "gm" in the password input field - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your password"]', "gm"); // Click the login button - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); @@ -35,9 +38,9 @@ test("view internal user page", async ({ page }) => { const rowCount = await page.locator("tbody tr").count(); expect(rowCount).toBeGreaterThan(0); - // Verify table headers are present (including Virtual Keys column) - const virtualKeysHeader = page.locator("th", { hasText: "Virtual Keys" }); - await expect(virtualKeysHeader).toBeVisible(); + const userIdHeader = page.locator("th", { hasText: "User ID" }); + page.screenshot({ path: "user_id_header.png" }); + await expect(userIdHeader).toBeVisible(); // test pagination // Wait for pagination controls to be visible diff --git a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts index 01eadc9ad1..adda3088f1 100644 --- a/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts +++ b/tests/proxy_admin_ui_tests/e2e_ui_tests/view_user_info.spec.ts @@ -2,22 +2,56 @@ import { test, expect } from "@playwright/test"; import { loginToUI } from "../utils/login"; test.describe("User Info View", () => { - test.beforeEach(async ({ page }) => { - await loginToUI(page); - // Navigate to users page - await page.goto("http://localhost:4000/ui?page=users"); - }); - test("should display user info when clicking on user ID", async ({ page, }) => { + await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); + + page.screenshot({ + path: "test-results/view_user_info_before_login.png", + }); + + // Enter "admin" in the username input field + await page.fill('input[placeholder="Enter your username"]', "admin"); + page.screenshot({ + path: "test-results/view_user_info_after_username_input.png", + }); + + // Enter "gm" in the password input field + await page.fill('input[placeholder="Enter your password"]', "gm"); + page.screenshot({ + path: "test-results/view_user_info_after_password_input.png", + }); + + // Click the login button + const loginButton = page.getByRole("button", { name: "Login" }); + await expect(loginButton).toBeEnabled(); + await loginButton.click(); + page.screenshot({ + path: "test-results/view_user_info_after_login_button_click.png", + }); + + // Wait for navigation to complete and dashboard to load + await page.waitForLoadState("networkidle"); + const tabElement = page.locator("span.ant-menu-title-content", { + hasText: "Internal User", + }); + await tabElement.click(); + page.screenshot({ + path: "test-results/view_user_info_after_internal_user_tab_click.png", + }); // Wait for loading state to disappear await page.waitForSelector('text="šŸš… Loading users..."', { state: "hidden", + timeout: 10000, }); + page.screenshot({ path: "test-results/view_user_info_after_loading.png" }); // Wait for users table to load await page.waitForSelector("table"); - + page.screenshot({ + path: "test-results/view_user_info_after_table_load.png", + }); // Get the first user ID cell const firstUserIdCell = page.locator( "table tbody tr:first-child td:first-child" @@ -27,6 +61,7 @@ test.describe("User Info View", () => { // Click on the user ID await firstUserIdCell.click(); + await page.waitForLoadState("networkidle"); // Check for tabs await expect(page.locator('button:has-text("Overview")')).toBeVisible({ diff --git a/tests/proxy_admin_ui_tests/utils/login.ts b/tests/proxy_admin_ui_tests/utils/login.ts index e875508997..25858d9f57 100644 --- a/tests/proxy_admin_ui_tests/utils/login.ts +++ b/tests/proxy_admin_ui_tests/utils/login.ts @@ -3,17 +3,21 @@ import { Page, expect } from "@playwright/test"; export async function loginToUI(page: Page) { // Login first await page.goto("http://localhost:4000/ui"); + await page.waitForLoadState("networkidle"); console.log("Navigated to login page"); + page.screenshot({ path: "test-results/login_utils_before.png" }); // Wait for login form to be visible - await page.waitForSelector('input[name="username"]', { timeout: 10000 }); + await page.waitForSelector('input[placeholder="Enter your username"]', { + timeout: 10000, + }); console.log("Login form is visible"); - await page.fill('input[name="username"]', "admin"); - await page.fill('input[name="password"]', "gm"); + await page.fill('input[placeholder="Enter your username"]', "admin"); + await page.fill('input[placeholder="Enter your password"]', "gm"); console.log("Filled login credentials"); - const loginButton = page.locator('input[type="submit"]'); + const loginButton = page.getByRole("button", { name: "Login" }); await expect(loginButton).toBeEnabled(); await loginButton.click(); console.log("Clicked login button"); diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx new file mode 100644 index 0000000000..cce063eceb --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/LoginPage.test.tsx @@ -0,0 +1,172 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import LoginPage from "./LoginPage"; + +const mockPush = vi.fn(); +const mockReplace = vi.fn(); + +vi.mock("next/navigation", () => ({ + useRouter: vi.fn(() => ({ + push: mockPush, + replace: mockReplace, + })), +})); + +vi.mock("@/app/(dashboard)/hooks/uiConfig/useUIConfig", () => ({ + useUIConfig: vi.fn(), +})); + +vi.mock("@/utils/cookieUtils", () => ({ + getCookie: vi.fn(), +})); + +vi.mock("@/utils/jwtUtils", () => ({ + isJwtExpired: vi.fn(), +})); + +vi.mock("@/components/networking", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getProxyBaseUrl: vi.fn().mockReturnValue("http://localhost:4000"), + }; +}); + +vi.mock("@/app/(dashboard)/hooks/login/useLogin", () => ({ + useLogin: vi.fn(() => ({ + mutate: vi.fn(), + isPending: false, + error: null, + })), +})); + +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import { getCookie } from "@/utils/cookieUtils"; +import { isJwtExpired } from "@/utils/jwtUtils"; + +const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }); + +describe("LoginPage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockPush.mockClear(); + mockReplace.mockClear(); + }); + + it("should render", async () => { + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(null); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument(); + }); + }); + + it("should call router.replace to dashboard when jwt is valid", async () => { + const validToken = "valid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(validToken); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith("http://localhost:4000/ui"); + }); + }); + + it("should call router.push to SSO when jwt is invalid and auto_redirect_to_sso is true", async () => { + const invalidToken = "invalid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: true, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(invalidToken); + (isJwtExpired as ReturnType).mockReturnValue(true); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockPush).toHaveBeenCalledWith("http://localhost:4000/sso/key/generate"); + }); + }); + + it("should not call router when jwt is invalid and auto_redirect_to_sso is false", async () => { + const invalidToken = "invalid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: false, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(invalidToken); + (isJwtExpired as ReturnType).mockReturnValue(true); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Login" })).toBeInTheDocument(); + }); + + expect(mockPush).not.toHaveBeenCalled(); + expect(mockReplace).not.toHaveBeenCalled(); + }); + + it("should send user to dashboard when jwt is valid even if auto_redirect_to_sso is true", async () => { + const validToken = "valid-token"; + (useUIConfig as ReturnType).mockReturnValue({ + data: { auto_redirect_to_sso: true, server_root_path: "/", proxy_base_url: null }, + isLoading: false, + }); + (getCookie as ReturnType).mockReturnValue(validToken); + (isJwtExpired as ReturnType).mockReturnValue(false); + + const queryClient = createQueryClient(); + render( + + + , + ); + + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith("http://localhost:4000/ui"); + }); + + expect(mockPush).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx new file mode 100644 index 0000000000..85f2c6dd87 --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx @@ -0,0 +1,159 @@ +"use client"; + +import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { getProxyBaseUrl } from "@/components/networking"; +import { getCookie } from "@/utils/cookieUtils"; +import { isJwtExpired } from "@/utils/jwtUtils"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Alert, Button, Card, Form, Input, Space, Typography } from "antd"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; + +function LoginPageContent() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const { data: uiConfig, isLoading: isConfigLoading } = useUIConfig(); + const loginMutation = useLogin(); + const router = useRouter(); + + useEffect(() => { + if (isConfigLoading) { + return; + } + + const rawToken = getCookie("token"); + if (rawToken && !isJwtExpired(rawToken)) { + router.replace(`${getProxyBaseUrl()}/ui`); + return; + } + + if (uiConfig && uiConfig.auto_redirect_to_sso) { + router.push(`${getProxyBaseUrl()}/sso/key/generate`); + return; + } + + setIsLoading(false); + }, [isConfigLoading, router, uiConfig]); + + const handleSubmit = () => { + loginMutation.mutate( + { username, password }, + { + onSuccess: (data) => { + router.push(data.redirect_url); + }, + }, + ); + }; + + const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; + const isLoginLoading = loginMutation.isPending; + + const { Title, Text, Paragraph } = Typography; + + if (isConfigLoading || isLoading) { + return ; + } + + return ( +
+ + +
+ šŸš… LiteLLM +
+ +
+ Login + Access your LiteLLM Admin UI. +
+ + + + By default, Username is admin and + Password is your set LiteLLM Proxy + MASTER_KEY. + + + Need to set UI credentials or SSO?{" "} + + Check the documentation + + . + + + } + type="info" + icon={} + showIcon + /> + + {error && } + +
+ + setUsername(e.target.value)} + disabled={isLoginLoading} + size="large" + className="rounded-md border-gray-300" + /> + + + + setPassword(e.target.value)} + disabled={isLoginLoading} + size="large" + /> + + + + + +
+
+
+
+ ); +} + +export default function LoginPage() { + const queryClient = new QueryClient(); + + return ( + + + + ); +} diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx index bd24240a6c..a539f87ee5 100644 --- a/ui/litellm-dashboard/src/app/login/page.tsx +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -1,154 +1,5 @@ "use client"; -import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; -import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; -import LoadingScreen from "@/components/common_components/LoadingScreen"; -import { getProxyBaseUrl } from "@/components/networking"; -import { getCookie } from "@/utils/cookieUtils"; -import { isJwtExpired } from "@/utils/jwtUtils"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Alert, Button, Card, Form, Input, Space, Typography } from "antd"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import LoginPage from "./LoginPage"; -function LoginPageContent() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(true); - const { isLoading: isConfigLoading } = useUIConfig(); - const loginMutation = useLogin(); - const router = useRouter(); - - useEffect(() => { - if (isConfigLoading) { - return; - } - - const rawToken = getCookie("token"); - if (rawToken && !isJwtExpired(rawToken)) { - router.replace(`${getProxyBaseUrl()}/ui`); - return; - } - - setIsLoading(false); - }, [isConfigLoading, router]); - - const handleSubmit = () => { - loginMutation.mutate( - { username, password }, - { - onSuccess: (data) => { - router.push(data.redirect_url); - }, - }, - ); - }; - - const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; - const isLoginLoading = loginMutation.isPending; - - const { Title, Text, Paragraph } = Typography; - - if (isConfigLoading || isLoading) { - return ; - } - - return ( -
- - -
- šŸš… LiteLLM -
- -
- Login - Access your LiteLLM Admin UI. -
- - - - By default, Username is admin and - Password is your set LiteLLM Proxy - MASTER_KEY. - - - Need to set UI credentials or SSO?{" "} - - Check the documentation - - . - - - } - type="info" - icon={} - showIcon - /> - - {error && } - -
- - setUsername(e.target.value)} - disabled={isLoginLoading} - size="large" - className="rounded-md border-gray-300" - /> - - - - setPassword(e.target.value)} - disabled={isLoginLoading} - size="large" - /> - - - - - -
-
-
-
- ); -} - -export default function LoginPage() { - const queryClient = new QueryClient(); - - return ( - - - - ); -} +export default LoginPage; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 68060ae696..20f5480c97 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -186,7 +186,7 @@ export default function CreateKeyPage() { useEffect(() => { if (redirectToLogin) { // Replace instead of assigning to avoid back-button loops - const dest = (proxyBaseUrl || "") + "/sso/key/generate"; + const dest = (proxyBaseUrl || "") + "/ui/login"; window.location.replace(dest); } }, [redirectToLogin]); diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 061d62d670..0e45c0f3a9 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -205,6 +205,7 @@ export interface PublicModelHubInfo { export interface LiteLLMWellKnownUiConfig { server_root_path: string; proxy_base_url: string | null; + auto_redirect_to_sso: boolean; } export interface CredentialsResponse { diff --git a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx index 9287e20318..c3c5ae5923 100644 --- a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx +++ b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx @@ -193,7 +193,7 @@ describe("CreateKeyPage auth behavior", () => { // Assert: we eventually redirect to SSO login (single replace, not assign/href) await waitFor(() => { - expect(window.location.replace).toHaveBeenCalledWith("https://example.com/sso/key/generate"); + expect(window.location.replace).toHaveBeenCalledWith("https://example.com/ui/login"); }); // And we attempted to clear the cookie (defensive deletion)