Skip to content

Commit c2a0446

Browse files
committed
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
1 parent ed51aef commit c2a0446

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

src/api/providers/fetchers/__tests__/litellm.spec.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,90 @@ describe("getLiteLLMModels", () => {
8181
})
8282
})
8383

84+
it("handles base URLs with double slashes correctly", async () => {
85+
const mockResponse = {
86+
data: {
87+
data: [],
88+
},
89+
}
90+
91+
mockedAxios.get.mockResolvedValue(mockResponse)
92+
93+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm//")
94+
95+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info", {
96+
headers: {
97+
Authorization: "Bearer test-api-key",
98+
"Content-Type": "application/json",
99+
...DEFAULT_HEADERS,
100+
},
101+
timeout: 5000,
102+
})
103+
})
104+
105+
it("handles base URLs with query parameters correctly", async () => {
106+
const mockResponse = {
107+
data: {
108+
data: [],
109+
},
110+
}
111+
112+
mockedAxios.get.mockResolvedValue(mockResponse)
113+
114+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm?key=value")
115+
116+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info?key=value", {
117+
headers: {
118+
Authorization: "Bearer test-api-key",
119+
"Content-Type": "application/json",
120+
...DEFAULT_HEADERS,
121+
},
122+
timeout: 5000,
123+
})
124+
})
125+
126+
it("handles base URLs with fragments correctly", async () => {
127+
const mockResponse = {
128+
data: {
129+
data: [],
130+
},
131+
}
132+
133+
mockedAxios.get.mockResolvedValue(mockResponse)
134+
135+
await getLiteLLMModels("test-api-key", "http://localhost:4000/litellm#section")
136+
137+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/litellm/v1/model/info#section", {
138+
headers: {
139+
Authorization: "Bearer test-api-key",
140+
"Content-Type": "application/json",
141+
...DEFAULT_HEADERS,
142+
},
143+
timeout: 5000,
144+
})
145+
})
146+
147+
it("handles base URLs with port and no path correctly", async () => {
148+
const mockResponse = {
149+
data: {
150+
data: [],
151+
},
152+
}
153+
154+
mockedAxios.get.mockResolvedValue(mockResponse)
155+
156+
await getLiteLLMModels("test-api-key", "http://localhost:4000")
157+
158+
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", {
159+
headers: {
160+
Authorization: "Bearer test-api-key",
161+
"Content-Type": "application/json",
162+
...DEFAULT_HEADERS,
163+
},
164+
timeout: 5000,
165+
})
166+
})
167+
84168
it("successfully fetches and formats LiteLLM models", async () => {
85169
const mockResponse = {
86170
data: {

src/api/providers/fetchers/litellm.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
2424
headers["Authorization"] = `Bearer ${apiKey}`
2525
}
2626
// Use URL constructor to properly join base URL and path
27-
const url = new URL("v1/model/info", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).href
27+
// This approach handles all edge cases including paths, query params, and fragments
28+
const urlObj = new URL(baseUrl)
29+
// Normalize the pathname by removing trailing slashes and multiple slashes
30+
urlObj.pathname = urlObj.pathname.replace(/\/+$/, "").replace(/\/+/g, "/") + "/v1/model/info"
31+
const url = urlObj.href
2832
// Added timeout to prevent indefinite hanging
2933
const response = await axios.get(url, { headers, timeout: 5000 })
3034
const models: ModelRecord = {}

0 commit comments

Comments
 (0)