fix(ui): resolve discovery paths before export preparation

This commit is contained in:
Martin Collado 2026-10-15 21:28:01 +02:00
parent aa7d2afa66
commit 3cb875a4fa
No known key found for this signature in database
GPG key ID: 566182E0C35B0EC8
4 changed files with 45 additions and 20 deletions

View file

@ -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)

View file

@ -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 () => {

View file

@ -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();
/**

View file

@ -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