(baseUrl = "http://localhost:1234")
| 50 | } |
| 51 | |
| 52 | export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Promise<Record<string, ModelInfo>> { |
| 53 | // clear the set of models that have full details loaded |
| 54 | modelsWithLoadedDetails.clear() |
| 55 | // clearing the input can leave an empty string; use the default in that case |
| 56 | baseUrl = baseUrl === "" ? "http://localhost:1234" : baseUrl |
| 57 | |
| 58 | const models: Record<string, ModelInfo> = {} |
| 59 | // ws is required to connect using the LMStudio library |
| 60 | const lmsUrl = baseUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://") |
| 61 | |
| 62 | try { |
| 63 | if (!URL.canParse(lmsUrl)) { |
| 64 | return models |
| 65 | } |
| 66 | |
| 67 | // test the connection to LM Studio first |
| 68 | // errors will be caught further down |
| 69 | await axios.get(`${baseUrl}/v1/models`) |
| 70 | |
| 71 | const client = new LMStudioClient({ baseUrl: lmsUrl }) |
| 72 | |
| 73 | // First, try to get all downloaded models |
| 74 | try { |
| 75 | const downloadedModels = await client.system.listDownloadedModels("llm") |
| 76 | for (const model of downloadedModels) { |
| 77 | // Use the model path as the key since that's what users select |
| 78 | models[model.path] = parseLMStudioModel(model) |
| 79 | } |
| 80 | } catch (error) { |
| 81 | console.warn("Failed to list downloaded models, falling back to loaded models only") |
| 82 | } |
| 83 | |
| 84 | // Get loaded models for their runtime info (context size) |
| 85 | const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => { |
| 86 | return Promise.all(models.map((m) => m.getModelInfo())) |
| 87 | })) as Array<LLMInstanceInfo> |
| 88 | |
| 89 | // Deduplicate: For each loaded model, check if any downloaded model path contains the loaded model's key |
| 90 | // This handles cases like loaded "llama-3.1" matching downloaded "Meta/Llama-3.1/Something" |
| 91 | // If found, remove the downloaded version and add the loaded model (prefer loaded over downloaded for accurate runtime info) |
| 92 | for (const lmstudioModel of loadedModels) { |
| 93 | const loadedModelId = lmstudioModel.modelKey.toLowerCase() |
| 94 | |
| 95 | // Find if any downloaded model path contains the loaded model's key as a path segment |
| 96 | // Use word boundaries or path separators to avoid false matches like "llama" matching "codellama" |
| 97 | const existingKey = Object.keys(models).find((key) => { |
| 98 | const keyLower = key.toLowerCase() |
| 99 | // Check if the loaded model ID appears as a distinct segment in the path |
| 100 | // This matches "llama-3.1" in "Meta/Llama-3.1/Something" but not "llama" in "codellama" |
| 101 | return ( |
| 102 | keyLower.includes(`/${loadedModelId}/`) || |
| 103 | keyLower.includes(`/${loadedModelId}`) || |
| 104 | keyLower.startsWith(`${loadedModelId}/`) || |
| 105 | keyLower === loadedModelId |
| 106 | ) |
| 107 | }) |
| 108 | |
| 109 | if (existingKey) { |
no test coverage detected