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
This commit is contained in:
Roo Code 2025-07-14 16:10:30 +00:00
parent a163053430
commit 32a41f4f6b
3 changed files with 42 additions and 7 deletions

View file

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

View file

@ -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 = {}

View file

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