Skip to content

Commit 1237eb8

Browse files
fix: trim whitespace from OpenAI base URL to fix model detection (#6560)
Co-authored-by: Roo Code <[email protected]>
1 parent 7b6c6a8 commit 1237eb8

File tree

2 files changed

+154
-3
lines changed

2 files changed

+154
-3
lines changed

src/api/providers/__tests__/openai.spec.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// npx vitest run api/providers/__tests__/openai.spec.ts
22

3-
import { OpenAiHandler } from "../openai"
3+
import { OpenAiHandler, getOpenAiModels } from "../openai"
44
import { ApiHandlerOptions } from "../../../shared/api"
55
import { Anthropic } from "@anthropic-ai/sdk"
66
import OpenAI from "openai"
77
import { openAiModelInfoSaneDefaults } from "@roo-code/types"
88
import { Package } from "../../../shared/package"
9+
import axios from "axios"
910

1011
const mockCreate = vitest.fn()
1112

@@ -68,6 +69,13 @@ vitest.mock("openai", () => {
6869
}
6970
})
7071

72+
// Mock axios for getOpenAiModels tests
73+
vitest.mock("axios", () => ({
74+
default: {
75+
get: vitest.fn(),
76+
},
77+
}))
78+
7179
describe("OpenAiHandler", () => {
7280
let handler: OpenAiHandler
7381
let mockOptions: ApiHandlerOptions
@@ -776,3 +784,143 @@ describe("OpenAiHandler", () => {
776784
})
777785
})
778786
})
787+
788+
describe("getOpenAiModels", () => {
789+
beforeEach(() => {
790+
vi.mocked(axios.get).mockClear()
791+
})
792+
793+
it("should return empty array when baseUrl is not provided", async () => {
794+
const result = await getOpenAiModels(undefined, "test-key")
795+
expect(result).toEqual([])
796+
expect(axios.get).not.toHaveBeenCalled()
797+
})
798+
799+
it("should return empty array when baseUrl is empty string", async () => {
800+
const result = await getOpenAiModels("", "test-key")
801+
expect(result).toEqual([])
802+
expect(axios.get).not.toHaveBeenCalled()
803+
})
804+
805+
it("should trim whitespace from baseUrl", async () => {
806+
const mockResponse = {
807+
data: {
808+
data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }],
809+
},
810+
}
811+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
812+
813+
const result = await getOpenAiModels(" https://api.openai.com/v1 ", "test-key")
814+
815+
expect(axios.get).toHaveBeenCalledWith("https://api.openai.com/v1/models", expect.any(Object))
816+
expect(result).toEqual(["gpt-4", "gpt-3.5-turbo"])
817+
})
818+
819+
it("should handle baseUrl with trailing spaces", async () => {
820+
const mockResponse = {
821+
data: {
822+
data: [{ id: "model-1" }, { id: "model-2" }],
823+
},
824+
}
825+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
826+
827+
const result = await getOpenAiModels("https://api.example.com/v1 ", "test-key")
828+
829+
expect(axios.get).toHaveBeenCalledWith("https://api.example.com/v1/models", expect.any(Object))
830+
expect(result).toEqual(["model-1", "model-2"])
831+
})
832+
833+
it("should handle baseUrl with leading spaces", async () => {
834+
const mockResponse = {
835+
data: {
836+
data: [{ id: "model-1" }],
837+
},
838+
}
839+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
840+
841+
const result = await getOpenAiModels(" https://api.example.com/v1", "test-key")
842+
843+
expect(axios.get).toHaveBeenCalledWith("https://api.example.com/v1/models", expect.any(Object))
844+
expect(result).toEqual(["model-1"])
845+
})
846+
847+
it("should return empty array for invalid URL after trimming", async () => {
848+
const result = await getOpenAiModels(" not-a-valid-url ", "test-key")
849+
expect(result).toEqual([])
850+
expect(axios.get).not.toHaveBeenCalled()
851+
})
852+
853+
it("should include authorization header when apiKey is provided", async () => {
854+
const mockResponse = {
855+
data: {
856+
data: [{ id: "model-1" }],
857+
},
858+
}
859+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
860+
861+
await getOpenAiModels("https://api.example.com/v1", "test-api-key")
862+
863+
expect(axios.get).toHaveBeenCalledWith(
864+
"https://api.example.com/v1/models",
865+
expect.objectContaining({
866+
headers: expect.objectContaining({
867+
Authorization: "Bearer test-api-key",
868+
}),
869+
}),
870+
)
871+
})
872+
873+
it("should include custom headers when provided", async () => {
874+
const mockResponse = {
875+
data: {
876+
data: [{ id: "model-1" }],
877+
},
878+
}
879+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
880+
881+
const customHeaders = {
882+
"X-Custom-Header": "custom-value",
883+
}
884+
885+
await getOpenAiModels("https://api.example.com/v1", "test-key", customHeaders)
886+
887+
expect(axios.get).toHaveBeenCalledWith(
888+
"https://api.example.com/v1/models",
889+
expect.objectContaining({
890+
headers: expect.objectContaining({
891+
"X-Custom-Header": "custom-value",
892+
Authorization: "Bearer test-key",
893+
}),
894+
}),
895+
)
896+
})
897+
898+
it("should handle API errors gracefully", async () => {
899+
vi.mocked(axios.get).mockRejectedValueOnce(new Error("Network error"))
900+
901+
const result = await getOpenAiModels("https://api.example.com/v1", "test-key")
902+
903+
expect(result).toEqual([])
904+
})
905+
906+
it("should handle malformed response data", async () => {
907+
vi.mocked(axios.get).mockResolvedValueOnce({ data: null })
908+
909+
const result = await getOpenAiModels("https://api.example.com/v1", "test-key")
910+
911+
expect(result).toEqual([])
912+
})
913+
914+
it("should deduplicate model IDs", async () => {
915+
const mockResponse = {
916+
data: {
917+
data: [{ id: "gpt-4" }, { id: "gpt-4" }, { id: "gpt-3.5-turbo" }, { id: "gpt-4" }],
918+
},
919+
}
920+
vi.mocked(axios.get).mockResolvedValueOnce(mockResponse)
921+
922+
const result = await getOpenAiModels("https://api.example.com/v1", "test-key")
923+
924+
expect(result).toEqual(["gpt-4", "gpt-3.5-turbo"])
925+
})
926+
})

src/api/providers/openai.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,10 @@ export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiH
416416
return []
417417
}
418418

419-
if (!URL.canParse(baseUrl)) {
419+
// Trim whitespace from baseUrl to handle cases where users accidentally include spaces
420+
const trimmedBaseUrl = baseUrl.trim()
421+
422+
if (!URL.canParse(trimmedBaseUrl)) {
420423
return []
421424
}
422425

@@ -434,7 +437,7 @@ export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiH
434437
config["headers"] = headers
435438
}
436439

437-
const response = await axios.get(`${baseUrl}/models`, config)
440+
const response = await axios.get(`${trimmedBaseUrl}/models`, config)
438441
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
439442
return [...new Set<string>(modelsArray)]
440443
} catch (error) {

0 commit comments

Comments
 (0)