From ed51aef35bd0d35e7a31e2e41c1a4e1643a30bbe Mon Sep 17 00:00:00 2001 From: Chung Nguyen <15166543+ChuKhaLi@users.noreply.github.com> Date: Mon, 14 Jul 2025 23:28:19 +0700 Subject: [PATCH 1/2] fix(litellm): handle baseurl with paths correctly --- .../fetchers/__tests__/litellm.spec.ts | 42 +++++++++++++++++++ src/api/providers/fetchers/litellm.ts | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index 07bbe9871ae..42e3571b775 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -39,6 +39,48 @@ describe("getLiteLLMModels", () => { }) }) + it("handles base URLs with a path correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + + it("handles base URLs with a path and trailing slash correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm/") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + it("successfully fetches and formats LiteLLM models", async () => { const mockResponse = { data: { diff --git a/src/api/providers/fetchers/litellm.ts b/src/api/providers/fetchers/litellm.ts index 0891527406d..add9d1bd6af 100644 --- a/src/api/providers/fetchers/litellm.ts +++ b/src/api/providers/fetchers/litellm.ts @@ -24,7 +24,7 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise headers["Authorization"] = `Bearer ${apiKey}` } // Use URL constructor to properly join base URL and path - const url = new URL("/v1/model/info", baseUrl).href + const url = new URL("v1/model/info", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href // Added timeout to prevent indefinite hanging const response = await axios.get(url, { headers, timeout: 5000 }) const models: ModelRecord = {} From c2a0446ce4713f3d7a1ad9da823b968f5dfabd4e Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Tue, 15 Jul 2025 10:55:49 -0500 Subject: [PATCH 2/2] fix: improve URL construction robustness and add edge case tests - Use URL object manipulation for more robust path joining - Handle edge cases: double slashes, query params, fragments - Normalize paths to prevent duplicate slashes - Add comprehensive test coverage for all URL scenarios --- .../fetchers/__tests__/litellm.spec.ts | 84 +++++++++++++++++++ src/api/providers/fetchers/litellm.ts | 6 +- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index 42e3571b775..f3a9d9971ec 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -81,6 +81,90 @@ describe("getLiteLLMModels", () => { }) }) + it("handles base URLs with double slashes correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm//") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + + it("handles base URLs with query parameters correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm?key=value") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info?key=value", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + + it("handles base URLs with fragments correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm#section") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info#section", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + + it("handles base URLs with port and no path correctly", async () => { + const mockResponse = { + data: { + data: [], + }, + } + + mockedAxios.get.mockResolvedValue(mockResponse) + + await getLiteLLMModels("test-api-key", "http://localhost:4000") + + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + it("successfully fetches and formats LiteLLM models", async () => { const mockResponse = { data: { diff --git a/src/api/providers/fetchers/litellm.ts b/src/api/providers/fetchers/litellm.ts index add9d1bd6af..e4e16c30e50 100644 --- a/src/api/providers/fetchers/litellm.ts +++ b/src/api/providers/fetchers/litellm.ts @@ -24,7 +24,11 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise headers["Authorization"] = `Bearer ${apiKey}` } // Use URL constructor to properly join base URL and path - const url = new URL("v1/model/info", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href + // This approach handles all edge cases including paths, query params, and fragments + const urlObj = new URL(baseUrl) + // Normalize the pathname by removing trailing slashes and multiple slashes + urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/model/info" + const url = urlObj.href // Added timeout to prevent indefinite hanging const response = await axios.get(url, { headers, timeout: 5000 }) const models: ModelRecord = {}