diff --git a/ui/litellm-dashboard/src/components/networking.test.ts b/ui/litellm-dashboard/src/components/networking.test.ts index d55f79bf0a2..05fc77d40fc 100644 --- a/ui/litellm-dashboard/src/components/networking.test.ts +++ b/ui/litellm-dashboard/src/components/networking.test.ts @@ -1038,33 +1038,34 @@ describe("getUiConfig runtime backend discovery", () => { return { networking, mockFetch }; }; - 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"); + it.each([ + ["https://backend.example.com", "/services/llm"], + ["https://backend.example.com/api-prefix", "/services/llm"], + ["https://backend.example.com", "/services/llm/"], + ])("loads static configuration from backend %s under normalized UI prefix %s", async (backendBase, uiPrefix) => { + 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(uiPrefix); + networking.switchToWorkerUrl("https://worker.example.com/worker-mount"); - await networking.getUiConfig(); + 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`); - }, - ); + 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 diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 2c247206b4b..7eb60c464b3 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -156,6 +156,7 @@ import { registerBaseUrlGetter, registerErrorHandler, } from "@/lib/http/runtime"; +import { withServerRoot } from "@/lib/assetPaths"; import { serverRootPath, setServerRootPath } from "@/lib/serverRootPath"; export { serverRootPath }; @@ -566,10 +567,9 @@ export const getUiConfig = async () => { : await (async (): Promise => { try { // eslint-disable-next-line no-restricted-syntax -- read the UI static config before the API origin is known - const response = await fetch( - `${serverRootPath === "/" ? "" : serverRootPath}/ui/litellm-runtime-config.json`, - { cache: "no-store" }, - ); + const response = await fetch(withServerRoot("/ui/litellm-runtime-config.json", serverRootPath), { + cache: "no-store", + }); if (!response.ok) return null; const runtimeConfig: unknown = await response.json(); if (