diff --git a/src/api/providers/fetchers/lm-studio.ts b/src/api/providers/fetchers/lm-studio.ts deleted file mode 100644 index 4b5f43bf7c..0000000000 --- a/src/api/providers/fetchers/lm-studio.ts +++ /dev/null @@ -1,35 +0,0 @@ -import axios from "axios" -import { ModelRecord } from "../../../shared/api" -import { openAiModelInfoSaneDefaults } from "@roo-code/types" - -export async function getLmStudioModels(baseUrl = "http://localhost:1234"): Promise { - try { - if (!URL.canParse(baseUrl)) { - return {} - } - - const response = await axios.get(`${baseUrl}/api/v0/models`) - return response.data?.data?.reduce((acc: ModelRecord, model: any) => { - acc[model.id] = { - maxTokens: - model.loaded_context_length || - model.max_context_length || - openAiModelInfoSaneDefaults.contextWindow, - contextWindow: - model.loaded_context_length || - model.max_context_length || - openAiModelInfoSaneDefaults.contextWindow, - supportsImages: false, - supportsPromptCache: false, - supportsComputerUse: false, - inputPrice: 0, - outputPrice: 0, - cacheWritesPrice: 0, - cacheReadsPrice: 0, - } - return acc - }, {}) - } catch (error) { - return {} - } -} diff --git a/src/api/providers/fetchers/modelCache.ts b/src/api/providers/fetchers/modelCache.ts index df63c83a36..409e250c63 100644 --- a/src/api/providers/fetchers/modelCache.ts +++ b/src/api/providers/fetchers/modelCache.ts @@ -1,23 +1,22 @@ -import * as path from "path" import fs from "fs/promises" +import * as path from "path" import NodeCache from "node-cache" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ContextProxy } from "../../../core/config/ContextProxy" -import { getCacheDirectoryPath } from "../../../utils/storage" -import { RouterName, ModelRecord } from "../../../shared/api" +import { ModelRecord, RouterName } from "../../../shared/api" import { fileExistsAtPath } from "../../../utils/fs" +import { getCacheDirectoryPath } from "../../../utils/storage" +import { GetModelsOptions } from "../../../shared/api" +import { getGlamaModels } from "./glama" +import { getLiteLLMModels } from "./litellm" +import { getLMStudioModels } from "./lmstudio" +import { getOllamaModels } from "./ollama" import { getOpenRouterModels } from "./openrouter" import { getRequestyModels } from "./requesty" -import { getGlamaModels } from "./glama" import { getUnboundModels } from "./unbound" -import { getLiteLLMModels } from "./litellm" -import { getLmStudioModels } from "./lm-studio" -import { GetModelsOptions } from "../../../shared/api" -import { getOllamaModels } from "./ollama" -import { getLMStudioModels } from "./lmstudio" const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 }) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 19c9a7c9fc..3ab11ce427 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -16,6 +16,7 @@ import { Task, TaskOptions } from "../../task/Task" import { safeWriteJson } from "../../../utils/safeWriteJson" import { ClineProvider } from "../ClineProvider" +import { LmStudioHandler } from "../../../api/providers" // Mock setup must come before imports vi.mock("../../prompts/sections/custom-instructions") @@ -2409,6 +2410,7 @@ describe("ClineProvider - Router Models", () => { unboundApiKey: "unbound-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", + lmStudioBaseUrl: "http://localhost:1234", }, } as any) @@ -2442,6 +2444,10 @@ describe("ClineProvider - Router Models", () => { apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) + expect(getModels).toHaveBeenCalledWith({ + provider: "lmstudio", + baseUrl: "http://localhost:1234", + }) // Verify response was sent expect(mockPostMessage).toHaveBeenCalledWith({ @@ -2453,7 +2459,7 @@ describe("ClineProvider - Router Models", () => { unbound: mockModels, litellm: mockModels, ollama: {}, - lmstudio: {}, + lmstudio: mockModels, }, }) }) @@ -2470,6 +2476,7 @@ describe("ClineProvider - Router Models", () => { unboundApiKey: "unbound-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", + lmStudioBaseUrl: "http://localhost:1234", }, } as any) @@ -2485,6 +2492,7 @@ describe("ClineProvider - Router Models", () => { .mockResolvedValueOnce(mockModels) // glama success .mockRejectedValueOnce(new Error("Unbound API error")) // unbound fail .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm fail + .mockRejectedValueOnce(new Error("LMStudio API error")) // lmstudio fail await messageHandler({ type: "requestRouterModels" }) @@ -2530,6 +2538,13 @@ describe("ClineProvider - Router Models", () => { error: "LiteLLM connection failed", values: { provider: "litellm" }, }) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "LMStudio API error", + values: { provider: "lmstudio" }, + }) }) test("handles requestRouterModels with LiteLLM values from message", async () => { @@ -2608,7 +2623,7 @@ describe("ClineProvider - Router Models", () => { unbound: mockModels, litellm: {}, ollama: {}, - lmstudio: {}, + lmstudio: mockModels, }, }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 2f356aef55..597c553916 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -101,6 +101,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unboundApiKey: "unbound-key", litellmApiKey: "litellm-key", litellmBaseUrl: "http://localhost:4000", + lmStudioBaseUrl: "http://localhost:1234", }, }) }) @@ -137,6 +138,10 @@ describe("webviewMessageHandler - requestRouterModels", () => { apiKey: "litellm-key", baseUrl: "http://localhost:4000", }) + expect(mockGetModels).toHaveBeenCalledWith({ + provider: "lmstudio", + baseUrl: "http://localhost:1234", + }) // Verify response was sent expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ @@ -148,7 +153,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: mockModels, litellm: mockModels, ollama: {}, - lmstudio: {}, + lmstudio: mockModels, }, }) }) @@ -235,7 +240,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { unbound: mockModels, litellm: {}, ollama: {}, - lmstudio: {}, + lmstudio: mockModels, }, }) }) @@ -257,6 +262,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockResolvedValueOnce(mockModels) // glama .mockRejectedValueOnce(new Error("Unbound API error")) // unbound .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + .mockRejectedValueOnce(new Error("LMStudio API error")) // lmstudio")) await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", @@ -307,6 +313,7 @@ describe("webviewMessageHandler - requestRouterModels", () => { .mockRejectedValueOnce(new Error("Glama API error")) // glama .mockRejectedValueOnce(new Error("Unbound API error")) // unbound .mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm + .mockRejectedValueOnce(new Error("LMStudio API error")) // lmstudio await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels", @@ -347,6 +354,13 @@ describe("webviewMessageHandler - requestRouterModels", () => { error: "LiteLLM connection failed", values: { provider: "litellm" }, }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "singleRouterModelFetchResponse", + success: false, + error: "LMStudio API error", + values: { provider: "lmstudio" }, + }) }) it("prefers config values over message values for LiteLLM", async () => { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c623542ee1..478548a5ab 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -605,9 +605,8 @@ export const webviewMessageHandler = async ( const fetchedRouterModels: Partial> = { ...routerModels, - // Initialize ollama and lmstudio with empty objects since they use separate handlers + // Initialize ollama with empty objects since it uses separate handlers ollama: {}, - lmstudio: {}, } results.forEach((result, index) => { @@ -616,18 +615,14 @@ export const webviewMessageHandler = async ( if (result.status === "fulfilled") { fetchedRouterModels[routerName] = result.value.models - // Ollama and LM Studio settings pages still need these events + // Ollama settings pages still need these events if (routerName === "ollama" && Object.keys(result.value.models).length > 0) { provider.postMessageToWebview({ type: "ollamaModels", ollamaModels: Object.keys(result.value.models), }) - } else if (routerName === "lmstudio" && Object.keys(result.value.models).length > 0) { - provider.postMessageToWebview({ - type: "lmStudioModels", - lmStudioModels: Object.keys(result.value.models), - }) } + // LM Studio models have moved to main router models message } else { // Handle rejection: Post a specific error message for this provider const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)