From 3cb875a4fa4fbc361b7f51ae93c568694e1cee3f Mon Sep 17 00:00:00 2001 From: Martin Collado Date: Thu, 15 Oct 2026 21:28:01 +0200 Subject: [PATCH] fix(ui): resolve discovery paths before export preparation --- tests/integration/ui/test_ui_root_path.py | 2 +- .../src/components/networking.test.ts | 59 +++++++++++++------ .../src/components/networking.tsx | 3 +- ui/prepare-root-path.sh | 1 - 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/tests/integration/ui/test_ui_root_path.py b/tests/integration/ui/test_ui_root_path.py index d5d03619b5e..d2032bc8279 100644 --- a/tests/integration/ui/test_ui_root_path.py +++ b/tests/integration/ui/test_ui_root_path.py @@ -54,7 +54,7 @@ class TestUIRootPath(unittest.TestCase): self.assertIn("/services/llm/favicon.ico?version=1", (runtime / "index.txt").read_text()) self.assertEqual(runtime.stat().st_mode & 0o005, 0o005) self.assertEqual((runtime / "index.html").stat().st_mode & 0o004, 0o004) - self.assertIn("/services/llm/.well-known/litellm-ui-config", (runtime / "app.js").read_text()) + self.assertEqual((runtime / "app.js").read_bytes(), (source / "app.js").read_bytes()) self.assertEqual((source / "index.html").read_bytes(), original) self.assertEqual((runtime / "font.woff2").read_bytes(), (source / "font.woff2").read_bytes()) self.assertEqual(self.prepare(directory, "/services/llm/").returncode, 0) diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index 0b718bfae36..d55f79bf0a2 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -229,7 +229,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/providers/fields", data: [] }, ]); @@ -255,7 +255,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/model_hub/info", data: {} }, ]); @@ -278,7 +278,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/model_hub", data: [] }, ]); @@ -301,7 +301,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/agent_hub", data: [] }, ]); @@ -322,7 +322,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/mcp_hub", data: [] }, ]); @@ -343,7 +343,7 @@ describe("UI config and public endpoints", () => { }; const mockFetch = setupMockFetch([ - { url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }, + { url: "/.well-known/litellm-ui-config", data: uiConfig }, { url: "/public/providers/fields", data: [] }, ]); @@ -365,14 +365,14 @@ describe("UI config and public endpoints", () => { proxy_base_url: "https://example.com", }; - const mockFetch = setupMockFetch([{ url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }]); + const mockFetch = setupMockFetch([{ url: "/.well-known/litellm-ui-config", data: uiConfig }]); const result = await Networking.getUiConfig(); expect(mockFetch).toHaveBeenCalledTimes(2); expect(result).toEqual(uiConfig); const configCall = mockFetch.mock.calls.find((call) => - (call[0] as string).includes("/litellm/.well-known/litellm-ui-config"), + (call[0] as string).endsWith("/.well-known/litellm-ui-config"), ); expect(configCall).toBeDefined(); }); @@ -383,7 +383,7 @@ describe("UI config and public endpoints", () => { proxy_base_url: "https://example.com", }; - setupMockFetch([{ url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }]); + setupMockFetch([{ url: "/.well-known/litellm-ui-config", data: uiConfig }]); await Networking.getUiConfig(); @@ -397,7 +397,7 @@ describe("UI config and public endpoints", () => { proxy_base_url: "https://backend.example.com", gateway_base_url: "https://gateway.example.com", }; - const mockFetch = setupMockFetch([{ url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }]); + const mockFetch = setupMockFetch([{ url: "/.well-known/litellm-ui-config", data: uiConfig }]); await Networking.getUiConfig(); expect(await Networking.testModelGroupConnection("token", "chat-model", "chat")).toEqual({ status: "success" }); @@ -415,7 +415,7 @@ describe("UI config and public endpoints", () => { server_root_path: "", proxy_base_url: "https://backend.example.com", }; - const mockFetch = setupMockFetch([{ url: "/litellm/.well-known/litellm-ui-config", data: uiConfig }]); + const mockFetch = setupMockFetch([{ url: "/.well-known/litellm-ui-config", data: uiConfig }]); await Networking.getUiConfig(); expect(await Networking.testModelGroupConnection("token", "embedding-model", "embedding")).toEqual({ @@ -1038,13 +1038,38 @@ describe("getUiConfig runtime backend discovery", () => { return { networking, mockFetch }; }; - it("loads static runtime configuration from the UI prefix independently of the selected worker", async () => { + it.each(["https://backend.example.com", "https://backend.example.com/api-prefix"])( + "loads prefixed static configuration independently of the selected worker and API base %s", + async (backendBase) => { + const mockFetch = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ backend_url: backendBase }), + } as Response) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ ...discoveryConfig(), server_root_path: "/services/llm" }), + } as Response); + vi.stubGlobal("fetch", mockFetch); + const networking = await loadFreshNetworking(); + const { setServerRootPath } = await import("@/lib/serverRootPath"); + setServerRootPath("/services/llm"); + networking.switchToWorkerUrl("https://worker.example.com/worker-mount"); + + await networking.getUiConfig(); + + expect(mockFetch).toHaveBeenNthCalledWith(1, "/services/llm/ui/litellm-runtime-config.json", { + cache: "no-store", + }); + expect(mockFetch.mock.calls[1][0]).toBe(`${backendBase}/litellm/.well-known/litellm-ui-config`); + }, + ); + + it("discovers the control plane under the UI prefix when no static backend is configured", async () => { const mockFetch = vi .fn() - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ backend_url: "https://backend.example.com" }), - } as Response) + .mockResolvedValueOnce({ ok: false } as Response) .mockResolvedValueOnce({ ok: true, json: async () => ({ ...discoveryConfig(), server_root_path: "/services/llm" }), @@ -1058,7 +1083,7 @@ describe("getUiConfig runtime backend discovery", () => { await networking.getUiConfig(); expect(mockFetch).toHaveBeenNthCalledWith(1, "/services/llm/ui/litellm-runtime-config.json", { cache: "no-store" }); - expect(mockFetch.mock.calls[1][0]).toBe("https://backend.example.com/litellm/.well-known/litellm-ui-config"); + expect(mockFetch).toHaveBeenNthCalledWith(2, "/services/llm/.well-known/litellm-ui-config"); }); it("uses a normalized runtime backend URL for discovery and the management fallback", async () => { diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 9e68f1ad9a1..2c247206b4b 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -587,9 +587,10 @@ export const getUiConfig = async () => { })(); runtimeBackendBaseUrl = runtimeBackendUrl; const discoveryBaseUrl = defaultProxyBaseUrl ?? runtimeBackendUrl; + const discoveryRoot = resolveApiBase({ serverRootPath }) || "/litellm"; const url = discoveryBaseUrl ? `${discoveryBaseUrl}/litellm/.well-known/litellm-ui-config` - : `/litellm/.well-known/litellm-ui-config`; + : `${discoveryRoot}/.well-known/litellm-ui-config`; const response = await fetch(url); const jsonData: LiteLLMWellKnownUiConfig = await response.json(); /** diff --git a/ui/prepare-root-path.sh b/ui/prepare-root-path.sh index bdd17e25840..c609c5422c3 100644 --- a/ui/prepare-root-path.sh +++ b/ui/prepare-root-path.sh @@ -29,7 +29,6 @@ if [ -n "$prefix" ]; then shift for file do sed -e "s|/litellm-asset-prefix|$prefix|g" \ - -e "s|/litellm/\.well-known/litellm-ui-config|$prefix/.well-known/litellm-ui-config|g" \ -e "s|/favicon.ico|$prefix/favicon.ico|g" \ "$file" > "$file.tmp" case "$file" in