mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: add fallback to OpenAI-compatible API for LM Studio model detection
- Modified getLMStudioModels to fall back to /v1/models endpoint when SDK methods fail - This fixes the issue where models like openai/gpt-oss-20b were not being detected - Added comprehensive test coverage for the fallback behavior - Fixes #6766
This commit is contained in:
parent
2b647ed9a1
commit
44ca7d4e57
2 changed files with 113 additions and 13 deletions
|
|
@ -212,11 +212,12 @@ describe("LMStudio Fetcher", () => {
|
|||
consoleInfoSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should return an empty object and log error if listDownloadedModels fails", async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
it("should return an empty object and log warning if listLoaded fails", async () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
const listError = new Error("LMStudio SDK internal error")
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: {} })
|
||||
mockListDownloadedModels.mockRejectedValueOnce(new Error("Failed to list downloaded"))
|
||||
mockListLoaded.mockRejectedValueOnce(listError)
|
||||
|
||||
const result = await getLMStudioModels(baseUrl)
|
||||
|
|
@ -225,11 +226,86 @@ describe("LMStudio Fetcher", () => {
|
|||
expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
|
||||
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
|
||||
expect(mockListLoaded).toHaveBeenCalledTimes(1)
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
`Error fetching LMStudio models: ${JSON.stringify(listError, Object.getOwnPropertyNames(listError), 2)}`,
|
||||
// Now it should log a warning for failed SDK methods, not an error
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
"Failed to list downloaded models, falling back to loaded models only",
|
||||
)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith("Failed to list loaded models via SDK")
|
||||
expect(result).toEqual({})
|
||||
consoleErrorSpy.mockRestore()
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should fall back to OpenAI API models when SDK methods fail", async () => {
|
||||
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
|
||||
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
|
||||
// Mock OpenAI API response with models
|
||||
const openAiModels = {
|
||||
data: [
|
||||
{ id: "openai/gpt-oss-20b", object: "model", owned_by: "organization_owner" },
|
||||
{ id: "unsloth/gpt-oss-20b", object: "model", owned_by: "organization_owner" },
|
||||
{ id: "qwen/qwen3-coder-30b", object: "model", owned_by: "organization_owner" },
|
||||
],
|
||||
object: "list",
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: openAiModels })
|
||||
|
||||
// Make SDK methods fail
|
||||
mockListDownloadedModels.mockRejectedValueOnce(new Error("SDK not available"))
|
||||
mockListLoaded.mockRejectedValueOnce(new Error("SDK not available"))
|
||||
|
||||
const result = await getLMStudioModels(baseUrl)
|
||||
|
||||
// Should have called the OpenAI endpoint
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
|
||||
|
||||
// Should have tried SDK methods
|
||||
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
|
||||
expect(mockListDownloadedModels).toHaveBeenCalled()
|
||||
expect(mockListLoaded).toHaveBeenCalled()
|
||||
|
||||
// Should have logged the fallback
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Falling back to OpenAI-compatible API models")
|
||||
|
||||
// Should return models from OpenAI API
|
||||
expect(Object.keys(result)).toHaveLength(3)
|
||||
expect(result["openai/gpt-oss-20b"]).toBeDefined()
|
||||
expect(result["openai/gpt-oss-20b"].description).toBe("openai/gpt-oss-20b")
|
||||
expect(result["unsloth/gpt-oss-20b"]).toBeDefined()
|
||||
expect(result["qwen/qwen3-coder-30b"]).toBeDefined()
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should not use OpenAI API fallback if SDK returns models", async () => {
|
||||
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {})
|
||||
|
||||
// Mock OpenAI API response with models
|
||||
const openAiModels = {
|
||||
data: [{ id: "openai/gpt-oss-20b", object: "model", owned_by: "organization_owner" }],
|
||||
object: "list",
|
||||
}
|
||||
|
||||
mockedAxios.get.mockResolvedValueOnce({ data: openAiModels })
|
||||
|
||||
// SDK returns models successfully
|
||||
mockListDownloadedModels.mockResolvedValueOnce([])
|
||||
mockListLoaded.mockResolvedValueOnce([{ getModelInfo: mockGetModelInfo }])
|
||||
mockGetModelInfo.mockResolvedValueOnce(mockRawModel)
|
||||
|
||||
const result = await getLMStudioModels(baseUrl)
|
||||
|
||||
// Should NOT log the fallback message
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith("Falling back to OpenAI-compatible API models")
|
||||
|
||||
// Should return SDK models, not OpenAI API models
|
||||
expect(Object.keys(result)).toHaveLength(1)
|
||||
expect(result[mockRawModel.modelKey]).toBeDefined()
|
||||
expect(result["openai/gpt-oss-20b"]).toBeUndefined()
|
||||
|
||||
consoleLogSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Prom
|
|||
return models
|
||||
}
|
||||
|
||||
// test the connection to LM Studio first
|
||||
// test the connection to LM Studio first and get models from OpenAI-compatible endpoint
|
||||
// errors will be caught further down
|
||||
await axios.get(`${baseUrl}/v1/models`)
|
||||
const openAiModelsResponse = await axios.get(`${baseUrl}/v1/models`)
|
||||
|
||||
const client = new LMStudioClient({ baseUrl: lmsUrl })
|
||||
|
||||
|
|
@ -82,13 +82,37 @@ export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Prom
|
|||
console.warn("Failed to list downloaded models, falling back to loaded models only")
|
||||
}
|
||||
// We want to list loaded models *anyway* since they provide valuable extra info (context size)
|
||||
const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => {
|
||||
return Promise.all(models.map((m) => m.getModelInfo()))
|
||||
})) as Array<LLMInstanceInfo>
|
||||
try {
|
||||
const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => {
|
||||
return Promise.all(models.map((m) => m.getModelInfo()))
|
||||
})) as Array<LLMInstanceInfo>
|
||||
|
||||
for (const lmstudioModel of loadedModels) {
|
||||
models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel)
|
||||
modelsWithLoadedDetails.add(lmstudioModel.modelKey)
|
||||
for (const lmstudioModel of loadedModels) {
|
||||
models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel)
|
||||
modelsWithLoadedDetails.add(lmstudioModel.modelKey)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to list loaded models via SDK")
|
||||
}
|
||||
|
||||
// If we didn't get any models from the SDK, fall back to OpenAI-compatible API
|
||||
if (Object.keys(models).length === 0 && openAiModelsResponse.data?.data) {
|
||||
console.log("Falling back to OpenAI-compatible API models")
|
||||
const openAiModels = openAiModelsResponse.data.data
|
||||
|
||||
for (const model of openAiModels) {
|
||||
// Use the model ID as the key
|
||||
models[model.id] = {
|
||||
...lMStudioDefaultModelInfo,
|
||||
description: model.id,
|
||||
// We don't have detailed info from the OpenAI API, so use defaults
|
||||
contextWindow: lMStudioDefaultModelInfo.contextWindow,
|
||||
maxTokens: lMStudioDefaultModelInfo.maxTokens,
|
||||
supportsPromptCache: true,
|
||||
supportsImages: false, // Conservative default
|
||||
supportsComputerUse: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ECONNREFUSED") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue