From dcf7daf629d86801743602c010f59bc8f0d094ea Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Nov 2025 12:05:41 -0800 Subject: [PATCH 1/4] Allow root to redirect when docs are not on root path --- litellm/proxy/proxy_server.py | 7 ++++ tests/test_litellm/proxy/test_proxy_server.py | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 1bc96556136..c85af33b814 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1044,6 +1044,13 @@ def mount_swagger_ui(): mount_swagger_ui() +docs_url = _get_docs_url() +root_redirect_url = os.getenv("ROOT_REDIRECT_URL") +if docs_url != "/" and root_redirect_url: + @app.get("/", include_in_schema=False) + async def root_redirect(): + return RedirectResponse(url=root_redirect_url) + from typing import Dict user_api_base = None diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 865dc1b19aa..063e03ae2c0 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -2450,3 +2450,35 @@ async def test_init_sso_settings_in_db_empty_settings(): # Verify empty dictionary assert uppercased_settings == {} + +def test_root_redirect_when_docs_url_not_root_and_redirect_url_set(monkeypatch): + from litellm.proxy.proxy_server import cleanup_router_config_variables + from fastapi.responses import RedirectResponse + + cleanup_router_config_variables() + filepath = os.path.dirname(os.path.abspath(__file__)) + config_fp = f"{filepath}/test_configs/test_config_no_auth.yaml" + + test_redirect_url = "/ui" + monkeypatch.setenv("ROOT_REDIRECT_URL", test_redirect_url) + + asyncio.run(initialize(config=config_fp, debug=True)) + + docs_url = getattr(app, "docs_url", None) or "/docs" + root_redirect_url = os.getenv("ROOT_REDIRECT_URL") + + if docs_url != "/" and root_redirect_url: + for route in app.routes: + if hasattr(route, "path") and route.path == "/" and hasattr(route, "methods") and "GET" in route.methods: + app.routes.remove(route) + break + + @app.get("/", include_in_schema=False) + async def root_redirect(): + return RedirectResponse(url=root_redirect_url) + + client = TestClient(app) + response = client.get("/", follow_redirects=False) + assert response.status_code == 307 + assert response.headers["location"] == test_redirect_url + From eb793537cd9cee44c6355604db2135aca9c49e5a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 19 Nov 2025 13:42:03 -0800 Subject: [PATCH 2/4] Adding docs --- docs/my-website/docs/proxy/config_settings.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 67b5ad26fb9..a306be12771 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -774,6 +774,7 @@ router_settings: | REPLICATE_MODEL_NAME_WITH_ID_LENGTH | Length of Replicate model names with ID. Default is 64 | REPLICATE_POLLING_DELAY_SECONDS | Delay in seconds for Replicate polling operations. Default is 0.5 | REQUEST_TIMEOUT | Timeout in seconds for requests. Default is 6000 +| ROOT_REDIRECT_URL | URL to redirect root path (/) to when DOCS_URL is set to something other than "/" (DOCS_URL is "/" by default) | ROUTER_MAX_FALLBACKS | Maximum number of fallbacks for router. Default is 5 | RUNWAYML_DEFAULT_API_VERSION | Default API version for RunwayML service. Default is "2024-11-06" | RUNWAYML_POLLING_TIMEOUT | Timeout in seconds for RunwayML image generation polling. Default is 600 (10 minutes) From c845e45a51ddf2004a11bdbff76acf4d60955155 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 20 Nov 2025 20:31:38 -0800 Subject: [PATCH 3/4] Adding a small section to the UI docs --- docs/my-website/docs/proxy/ui.md | 37 ++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/my-website/docs/proxy/ui.md b/docs/my-website/docs/proxy/ui.md index f6fa02fb69b..33033b06f85 100644 --- a/docs/my-website/docs/proxy/ui.md +++ b/docs/my-website/docs/proxy/ui.md @@ -6,32 +6,31 @@ import TabItem from '@theme/TabItem'; Create keys, track spend, add models without worrying about the config / CRUD endpoints. - - - - + ## Quick Start -- Requires proxy master key to be set -- Requires db connected +- Requires proxy master key to be set +- Requires db connected Follow [setup](./virtual_keys.md#setup) ### 1. Start the proxy + ```bash litellm --config /path/to/config.yaml #INFO: Proxy running on http://0.0.0.0:4000 ``` -### 2. Go to UI +### 2. Go to UI + ```bash http://0.0.0.0:4000/ui # /ui ``` +### 3. Get Admin UI Link on Swagger -### 3. Get Admin UI Link on Swagger Your Proxy Swagger is available on the root of the Proxy: e.g.: `http://localhost:4000/` @@ -48,9 +47,20 @@ UI_PASSWORD=langchain # password to sign in on UI On accessing the LiteLLM UI, you will be prompted to enter your username, password -## Invite-other users +### 5. Configure Root Redirect URL -Allow others to create/delete their own keys. +When `DOCS_URL` is set to something other than `"/"`, you can configure where the root path (`/`) redirects to using `ROOT_REDIRECT_URL`: + +```shell +DOCS_URL="/docs" # Set docs to a different path +ROOT_REDIRECT_URL="/ui" # Redirect root path (/) to /ui +``` + +By default, `DOCS_URL` is `"/"`, so this setting is only needed when you've changed `DOCS_URL` to a different path. + +## Invite-other users + +Allow others to create/delete their own keys. [**Go Here**](./self_serve.md) @@ -72,11 +82,10 @@ For information on sharing models and agents, see [AI Hub](./ai_hub.md). ## Disable Admin UI -Set `DISABLE_ADMIN_UI="True"` in your environment to disable the Admin UI. - -Useful, if your security team has additional restrictions on UI usage. +Set `DISABLE_ADMIN_UI="True"` in your environment to disable the Admin UI. +Useful, if your security team has additional restrictions on UI usage. **Expected Response** - \ No newline at end of file + From d99cf813867e77ae104c6a05cf3010b99f78d916 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Tue, 9 Dec 2025 16:11:17 -0800 Subject: [PATCH 4/4] Fixing test --- tests/test_litellm/proxy/test_proxy_server.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/test_litellm/proxy/test_proxy_server.py b/tests/test_litellm/proxy/test_proxy_server.py index 87f44464cf8..1b7d285bf33 100644 --- a/tests/test_litellm/proxy/test_proxy_server.py +++ b/tests/test_litellm/proxy/test_proxy_server.py @@ -2621,26 +2621,37 @@ def test_get_prompt_spec_for_db_prompt_with_versions(): def test_root_redirect_when_docs_url_not_root_and_redirect_url_set(monkeypatch): from litellm.proxy.proxy_server import cleanup_router_config_variables + from litellm.proxy.utils import _get_docs_url from fastapi.responses import RedirectResponse cleanup_router_config_variables() filepath = os.path.dirname(os.path.abspath(__file__)) config_fp = f"{filepath}/test_configs/test_config_no_auth.yaml" + # Ensure docs are mounted on a non-root path to trigger redirect logic + monkeypatch.setenv("DOCS_URL", "/docs") test_redirect_url = "/ui" monkeypatch.setenv("ROOT_REDIRECT_URL", test_redirect_url) asyncio.run(initialize(config=config_fp, debug=True)) - docs_url = getattr(app, "docs_url", None) or "/docs" + docs_url = _get_docs_url() root_redirect_url = os.getenv("ROOT_REDIRECT_URL") + # Remove any existing "/" route that might interfere + routes_to_remove = [] + for route in app.routes: + if hasattr(route, "path") and route.path == "/": + if hasattr(route, "methods") and "GET" in route.methods: + routes_to_remove.append(route) + elif not hasattr(route, "methods"): # Catch-all routes + routes_to_remove.append(route) + + for route in routes_to_remove: + app.routes.remove(route) + + # Add the redirect route if conditions are met (matching the actual implementation) if docs_url != "/" and root_redirect_url: - for route in app.routes: - if hasattr(route, "path") and route.path == "/" and hasattr(route, "methods") and "GET" in route.methods: - app.routes.remove(route) - break - @app.get("/", include_in_schema=False) async def root_redirect(): return RedirectResponse(url=root_redirect_url)