diff --git a/ui/litellm-dashboard/src/lib/http/api.ts b/ui/litellm-dashboard/src/lib/http/api.ts index ef628169ea1..508a27db78d 100644 --- a/ui/litellm-dashboard/src/lib/http/api.ts +++ b/ui/litellm-dashboard/src/lib/http/api.ts @@ -3,11 +3,14 @@ import createQueryClient from "openapi-react-query"; import type { paths } from "./schema"; import { ApiError, deriveErrorMessage } from "./client"; import { getAuthHeaderName, getAuthToken, getRequestBaseUrl, reportError } from "./runtime"; - -const resolveRequestBase = (): string => (getRequestBaseUrl() || globalThis.location?.origin || "").replace(/\/+$/, ""); +import { resolveRequestUrl } from "./resolveApiBase"; const BaseAwareRequest = function (url: string, init?: RequestInit): Request { - return new globalThis.Request(`${resolveRequestBase()}${url}`, init); + const target = resolveRequestUrl(url, { + registeredBase: getRequestBaseUrl(), + pageOrigin: globalThis.location?.origin, + }); + return new globalThis.Request(target, init); } as unknown as typeof Request; const middleware: Middleware = { diff --git a/ui/litellm-dashboard/src/lib/http/resolveApiBase.test.ts b/ui/litellm-dashboard/src/lib/http/resolveApiBase.test.ts index 988b88cf07d..6b41ef320fc 100644 --- a/ui/litellm-dashboard/src/lib/http/resolveApiBase.test.ts +++ b/ui/litellm-dashboard/src/lib/http/resolveApiBase.test.ts @@ -1,5 +1,41 @@ import { describe, expect, it } from "vitest"; -import { resolveApiBase } from "./resolveApiBase"; +import { resolveApiBase, resolveRequestUrl } from "./resolveApiBase"; + +describe("resolveRequestUrl", () => { + it("targets the registered base when one is registered", () => { + expect( + resolveRequestUrl("/model_group/info", { + registeredBase: "https://proxy.example.com", + pageOrigin: "http://localhost:3000", + }), + ).toBe("https://proxy.example.com/model_group/info"); + }); + + it("falls back to the page origin when no base is registered", () => { + expect(resolveRequestUrl("/model_group/info", { registeredBase: "", pageOrigin: "http://localhost:3000" })).toBe( + "http://localhost:3000/model_group/info", + ); + }); + + it("trims a trailing slash so the path is not doubled up", () => { + expect(resolveRequestUrl("/model_group/info", { registeredBase: "https://proxy.example.com/" })).toBe( + "https://proxy.example.com/model_group/info", + ); + }); + + it("keeps the path relative when neither a base nor an origin is available", () => { + expect(resolveRequestUrl("/model_group/info", {})).toBe("/model_group/info"); + expect(resolveRequestUrl("/model_group/info", { registeredBase: null, pageOrigin: null })).toBe( + "/model_group/info", + ); + }); + + it("preserves an already-serialized query string", () => { + expect( + resolveRequestUrl("/model_group/info?model_group=gpt-4o", { registeredBase: "https://proxy.example.com" }), + ).toBe("https://proxy.example.com/model_group/info?model_group=gpt-4o"); + }); +}); describe("resolveApiBase", () => { describe("same-origin (no explicit base)", () => { diff --git a/ui/litellm-dashboard/src/lib/http/resolveApiBase.ts b/ui/litellm-dashboard/src/lib/http/resolveApiBase.ts index 1d40784af92..661f9bb9eca 100644 --- a/ui/litellm-dashboard/src/lib/http/resolveApiBase.ts +++ b/ui/litellm-dashboard/src/lib/http/resolveApiBase.ts @@ -33,3 +33,15 @@ export const resolveApiBase = ({ explicitBase, serverRootPath }: ApiBaseInputs): if (rootPath === "" || base.endsWith(rootPath)) return base; return `${base}${rootPath}`; }; + +export interface RequestUrlInputs { + /** Base registered at runtime (a split-origin proxy or worker URL); empty means none. */ + registeredBase?: string | null; + /** Origin of the page issuing the request; the same-origin fallback. */ + pageOrigin?: string | null; +} + +export const resolveRequestUrl = (path: string, { registeredBase, pageOrigin }: RequestUrlInputs): string => { + const base = (registeredBase || pageOrigin || "").replace(/\/+$/, ""); + return `${base}${path}`; +};