From 32a41f4f6b04190a7db8dd9b11b99b9fe1b27077 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 14 Jul 2025 16:10:30 +0000 Subject: [PATCH] fix: correct LiteLLM baseURL handling when path is included - Fix LiteLLMHandler to append /v1 to baseURL like other providers - Fix getLiteLLMModels to properly join URLs preserving existing paths - Add joinUrl helper function to avoid URL constructor path replacement - Add test case for baseURL with paths - Update existing tests to match new URL construction Fixes #5696 --- .../fetchers/__tests__/litellm.spec.ts | 29 ++++++++++++++++--- src/api/providers/fetchers/litellm.ts | 18 ++++++++++-- src/api/providers/lite-llm.ts | 2 +- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/litellm.spec.ts b/src/api/providers/fetchers/__tests__/litellm.spec.ts index 07bbe9871a..895393af1f 100644 --- a/src/api/providers/fetchers/__tests__/litellm.spec.ts +++ b/src/api/providers/fetchers/__tests__/litellm.spec.ts @@ -29,7 +29,28 @@ describe("getLiteLLMModels", () => { await getLiteLLMModels("test-api-key", "http://localhost:4000/") - expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", { + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/model/info", { + headers: { + Authorization: "Bearer test-api-key", + "Content-Type": "application/json", + ...DEFAULT_HEADERS, + }, + timeout: 5000, + }) + }) + + it("handles base URLs with paths 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/model/info", { headers: { Authorization: "Bearer test-api-key", "Content-Type": "application/json", @@ -81,7 +102,7 @@ describe("getLiteLLMModels", () => { const result = await getLiteLLMModels("test-api-key", "http://localhost:4000") - expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", { + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/model/info", { headers: { Authorization: "Bearer test-api-key", "Content-Type": "application/json", @@ -125,7 +146,7 @@ describe("getLiteLLMModels", () => { await getLiteLLMModels("", "http://localhost:4000") - expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", { + expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/model/info", { headers: { "Content-Type": "application/json", ...DEFAULT_HEADERS, @@ -257,7 +278,7 @@ describe("getLiteLLMModels", () => { await getLiteLLMModels("test-api-key", "http://localhost:4000") expect(mockedAxios.get).toHaveBeenCalledWith( - "http://localhost:4000/v1/model/info", + "http://localhost:4000/model/info", expect.objectContaining({ timeout: 5000, }), diff --git a/src/api/providers/fetchers/litellm.ts b/src/api/providers/fetchers/litellm.ts index 0891527406..3a84384657 100644 --- a/src/api/providers/fetchers/litellm.ts +++ b/src/api/providers/fetchers/litellm.ts @@ -5,6 +5,20 @@ import { LITELLM_COMPUTER_USE_MODELS } from "@roo-code/types" import type { ModelRecord } from "../../../shared/api" import { DEFAULT_HEADERS } from "../constants" + +/** + * Properly joins a base URL with a path, preserving any existing path in the base URL + * @param baseUrl The base URL (may include a path) + * @param path The path to append + * @returns The properly joined URL + */ +function joinUrl(baseUrl: string, path: string): string { + // Remove trailing slash from baseUrl and leading slash from path + const cleanBaseUrl = baseUrl.replace(/\/$/, "") + const cleanPath = path.replace(/^\//, "") + return `${cleanBaseUrl}/${cleanPath}` +} + /** * Fetches available models from a LiteLLM server * @@ -23,8 +37,8 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise if (apiKey) { headers["Authorization"] = `Bearer ${apiKey}` } - // Use URL constructor to properly join base URL and path - const url = new URL("/v1/model/info", baseUrl).href + // Use helper function to properly join base URL and path, preserving any existing path + const url = joinUrl(baseUrl, "model/info") // Added timeout to prevent indefinite hanging const response = await axios.get(url, { headers, timeout: 5000 }) const models: ModelRecord = {} diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index e8cd58b12c..7b454f2cd8 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -24,7 +24,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa super({ options, name: "litellm", - baseURL: `${options.litellmBaseUrl || "http://localhost:4000"}`, + baseURL: `${options.litellmBaseUrl || "http://localhost:4000"}/v1`, apiKey: options.litellmApiKey || "dummy-key", modelId: options.litellmModelId, defaultModelId: litellmDefaultModelId,