Skip to content

Commit 075e9af

Browse files
committed
fix: proper context window loading for LMStudio (fixes #5075)
1 parent dbde23c commit 075e9af

File tree

7 files changed

+103
-78
lines changed

7 files changed

+103
-78
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import axios from "axios"
2+
import { ModelRecord } from "../../../shared/api"
3+
import { openAiModelInfoSaneDefaults } from "@roo-code/types"
4+
5+
export async function getLmStudioModels(baseUrl = "http://localhost:1234"): Promise<ModelRecord> {
6+
try {
7+
if (!URL.canParse(baseUrl)) {
8+
return {}
9+
}
10+
11+
const response = await axios.get(`${baseUrl}/api/v0/models`)
12+
return response.data?.data?.reduce((acc: ModelRecord, model: any) => {
13+
acc[model.id] = {
14+
maxTokens:
15+
model.loaded_context_length ||
16+
model.max_context_length ||
17+
openAiModelInfoSaneDefaults.contextWindow,
18+
contextWindow:
19+
model.loaded_context_length ||
20+
model.max_context_length ||
21+
openAiModelInfoSaneDefaults.contextWindow,
22+
supportsImages: false,
23+
supportsPromptCache: false,
24+
supportsComputerUse: false,
25+
inputPrice: 0,
26+
outputPrice: 0,
27+
cacheWritesPrice: 0,
28+
cacheReadsPrice: 0,
29+
}
30+
return acc
31+
}, {})
32+
} catch (error) {
33+
return {}
34+
}
35+
}

src/api/providers/fetchers/modelCache.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { getRequestyModels } from "./requesty"
1414
import { getGlamaModels } from "./glama"
1515
import { getUnboundModels } from "./unbound"
1616
import { getLiteLLMModels } from "./litellm"
17+
import { getLmStudioModels } from "./lm-studio"
1718
import { GetModelsOptions } from "../../../shared/api"
1819
import { getOllamaModels } from "./ollama"
1920
import { getLMStudioModels } from "./lmstudio"

src/api/providers/lm-studio.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
3-
import axios from "axios"
43

5-
import { type ModelInfo, openAiModelInfoSaneDefaults, LMSTUDIO_DEFAULT_TEMPERATURE } from "@roo-code/types"
4+
import { LMSTUDIO_DEFAULT_TEMPERATURE, type ModelInfo, openAiModelInfoSaneDefaults } from "@roo-code/types"
65

76
import type { ApiHandlerOptions } from "../../shared/api"
87

@@ -11,22 +10,32 @@ import { XmlMatcher } from "../../utils/xml-matcher"
1110
import { convertToOpenAiMessages } from "../transform/openai-format"
1211
import { ApiStream } from "../transform/stream"
1312

13+
import type { ApiHandlerCreateMessageMetadata, SingleCompletionHandler } from "../index"
1414
import { BaseProvider } from "./base-provider"
15-
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
15+
import { flushModels, getModels } from "./fetchers/modelCache"
1616

1717
export class LmStudioHandler extends BaseProvider implements SingleCompletionHandler {
1818
protected options: ApiHandlerOptions
1919
private client: OpenAI
20+
private cachedModelInfo: ModelInfo = openAiModelInfoSaneDefaults
2021

2122
constructor(options: ApiHandlerOptions) {
2223
super()
2324
this.options = options
2425
this.client = new OpenAI({
25-
baseURL: (this.options.lmStudioBaseUrl || "http://localhost:1234") + "/v1",
26+
baseURL: this.getBaseUrl() + "/v1",
2627
apiKey: "noop",
2728
})
2829
}
2930

31+
private getBaseUrl(): string {
32+
if (this.options.lmStudioBaseUrl && this.options.lmStudioBaseUrl.trim() !== "") {
33+
return this.options.lmStudioBaseUrl.trim()
34+
} else {
35+
return "http://localhost:1234"
36+
}
37+
}
38+
3039
override async *createMessage(
3140
systemPrompt: string,
3241
messages: Anthropic.Messages.MessageParam[],
@@ -118,6 +127,23 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
118127
outputTokens = 0
119128
}
120129

130+
if (this.cachedModelInfo === openAiModelInfoSaneDefaults) {
131+
// We need to fetch the model info every time we open a new session
132+
// to ensure we have the latest context window and other details
133+
// since LM Studio models can chance their context windows on reload
134+
await flushModels("lmstudio")
135+
const models = await getModels({ provider: "lmstudio", baseUrl: this.getBaseUrl() })
136+
if (models && models[this.getModel().id]) {
137+
this.cachedModelInfo = models[this.getModel().id]
138+
} else {
139+
// If model info is not found, use sane defaults
140+
this.cachedModelInfo = {
141+
...openAiModelInfoSaneDefaults,
142+
description: "Fake description to avoid recache",
143+
}
144+
}
145+
}
146+
121147
yield {
122148
type: "usage",
123149
inputTokens,
@@ -133,7 +159,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
133159
override getModel(): { id: string; info: ModelInfo } {
134160
return {
135161
id: this.options.lmStudioModelId || "",
136-
info: openAiModelInfoSaneDefaults,
162+
info: this.cachedModelInfo,
137163
}
138164
}
139165

@@ -161,17 +187,3 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
161187
}
162188
}
163189
}
164-
165-
export async function getLmStudioModels(baseUrl = "http://localhost:1234") {
166-
try {
167-
if (!URL.canParse(baseUrl)) {
168-
return []
169-
}
170-
171-
const response = await axios.get(`${baseUrl}/v1/models`)
172-
const modelsArray = response.data?.data?.map((model: any) => model.id) || []
173-
return [...new Set<string>(modelsArray)]
174-
} catch (error) {
175-
return []
176-
}
177-
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 31 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,56 @@
1-
import { safeWriteJson } from "../../utils/safeWriteJson"
2-
import * as path from "path"
3-
import * as os from "os"
41
import * as fs from "fs/promises"
2+
import * as os from "os"
53
import pWaitFor from "p-wait-for"
4+
import * as path from "path"
65
import * as vscode from "vscode"
7-
import * as yaml from "yaml"
6+
import { safeWriteJson } from "../../utils/safeWriteJson"
87

8+
import { CloudService } from "@roo-code/cloud"
9+
import { TelemetryService } from "@roo-code/telemetry"
910
import {
11+
type ClineMessage,
12+
type GlobalState,
1013
type Language,
1114
type ProviderSettings,
12-
type GlobalState,
13-
type ClineMessage,
1415
TelemetryEventName,
1516
} from "@roo-code/types"
16-
import { CloudService } from "@roo-code/cloud"
17-
import { TelemetryService } from "@roo-code/telemetry"
1817
import { type ApiMessage } from "../task-persistence/apiMessages"
1918

20-
import { ClineProvider } from "./ClineProvider"
2119
import { changeLanguage, t } from "../../i18n"
20+
import { ModelRecord, RouterName, toRouterName } from "../../shared/api"
2221
import { Package } from "../../shared/package"
23-
import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
2422
import { supportPrompt } from "../../shared/support-prompt"
23+
import { ClineProvider } from "./ClineProvider"
2524

26-
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
27-
import { checkExistKey } from "../../shared/checkExistApiConfig"
28-
import { experimentDefault } from "../../shared/experiments"
29-
import { Terminal } from "../../integrations/terminal/Terminal"
30-
import { openFile } from "../../integrations/misc/open-file"
25+
import { flushModels, getModels } from "../../api/providers/fetchers/modelCache"
26+
import { getOpenAiModels } from "../../api/providers/openai"
27+
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
3128
import { openImage, saveImage } from "../../integrations/misc/image-handler"
29+
import { openFile } from "../../integrations/misc/open-file"
3230
import { selectImages } from "../../integrations/misc/process-images"
31+
import { Terminal } from "../../integrations/terminal/Terminal"
3332
import { getTheme } from "../../integrations/theme/getTheme"
3433
import { discoverChromeHostUrl, tryChromeHostUrl } from "../../services/browser/browserDiscovery"
3534
import { searchWorkspaceFiles } from "../../services/search/file-search"
35+
import { TelemetrySetting } from "../../shared/TelemetrySetting"
36+
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
37+
import { GetModelsOptions } from "../../shared/api"
38+
import { checkExistKey } from "../../shared/checkExistApiConfig"
39+
import { experimentDefault } from "../../shared/experiments"
40+
import { defaultModeSlug, Mode } from "../../shared/modes"
41+
import { getCommand } from "../../utils/commands"
3642
import { fileExistsAtPath } from "../../utils/fs"
37-
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
38-
import { singleCompletionHandler } from "../../utils/single-completion-handler"
3943
import { searchCommits } from "../../utils/git"
44+
import { getWorkspacePath } from "../../utils/path"
45+
import { singleCompletionHandler } from "../../utils/single-completion-handler"
46+
import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
4047
import { exportSettings, importSettingsWithFeedback } from "../config/importExport"
41-
import { getOpenAiModels } from "../../api/providers/openai"
42-
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
4348
import { openMention } from "../mentions"
44-
import { TelemetrySetting } from "../../shared/TelemetrySetting"
45-
import { getWorkspacePath } from "../../utils/path"
46-
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
47-
import { Mode, defaultModeSlug } from "../../shared/modes"
48-
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
49-
import { GetModelsOptions } from "../../shared/api"
5049
import { generateSystemPrompt } from "./generateSystemPrompt"
51-
import { getCommand } from "../../utils/commands"
5250

5351
const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])
5452

55-
import { MarketplaceManager, MarketplaceItemType } from "../../services/marketplace"
53+
import { MarketplaceItemType, MarketplaceManager } from "../../services/marketplace"
5654
import { setPendingTodoList } from "../tools/updateTodoListTool"
5755

5856
export const webviewMessageHandler = async (
@@ -555,6 +553,12 @@ export const webviewMessageHandler = async (
555553
})
556554
}
557555

556+
const lmStudioBaseUrl = apiConfiguration.lmStudioBaseUrl || message?.values?.lmStudioBaseUrl
557+
modelFetchPromises.push({
558+
key: "lmstudio",
559+
options: { provider: "lmstudio", baseUrl: lmStudioBaseUrl },
560+
})
561+
558562
const results = await Promise.allSettled(
559563
modelFetchPromises.map(async ({ key, options }) => {
560564
const models = await safeGetModels(options)
@@ -633,30 +637,6 @@ export const webviewMessageHandler = async (
633637
}
634638
break
635639
}
636-
case "requestLmStudioModels": {
637-
// Specific handler for LM Studio models only
638-
const { apiConfiguration: lmStudioApiConfig } = await provider.getState()
639-
try {
640-
// Flush cache first to ensure fresh models
641-
await flushModels("lmstudio")
642-
643-
const lmStudioModels = await getModels({
644-
provider: "lmstudio",
645-
baseUrl: lmStudioApiConfig.lmStudioBaseUrl,
646-
})
647-
648-
if (Object.keys(lmStudioModels).length > 0) {
649-
provider.postMessageToWebview({
650-
type: "lmStudioModels",
651-
lmStudioModels: Object.keys(lmStudioModels),
652-
})
653-
}
654-
} catch (error) {
655-
// Silently fail - user hasn't configured LM Studio yet
656-
console.debug("LM Studio models fetch failed:", error)
657-
}
658-
break
659-
}
660640
case "requestOpenAiModels":
661641
if (message?.values?.baseUrl && message?.values?.apiKey) {
662642
const openAiModels = await getOpenAiModels(

src/shared/WebviewMessage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export interface WebviewMessage {
6565
| "requestRouterModels"
6666
| "requestOpenAiModels"
6767
| "requestOllamaModels"
68-
| "requestLmStudioModels"
6968
| "requestVsCodeLmModels"
7069
| "openImage"
7170
| "saveImage"

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,12 +193,10 @@ const ApiOptions = ({
193193
},
194194
})
195195
} else if (selectedProvider === "ollama") {
196-
vscode.postMessage({ type: "requestOllamaModels" })
197-
} else if (selectedProvider === "lmstudio") {
198-
vscode.postMessage({ type: "requestLmStudioModels" })
196+
vscode.postMessage({ type: "requestOllamaModels", text: apiConfiguration?.ollamaBaseUrl })
199197
} else if (selectedProvider === "vscode-lm") {
200198
vscode.postMessage({ type: "requestVsCodeLmModels" })
201-
} else if (selectedProvider === "litellm") {
199+
} else if (selectedProvider === "litellm" || selectedProvider === "lmstudio") {
202200
vscode.postMessage({ type: "requestRouterModels" })
203201
}
204202
},

webview-ui/src/components/settings/providers/LMStudio.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
3939
const message: ExtensionMessage = event.data
4040

4141
switch (message.type) {
42-
case "lmStudioModels":
42+
case "routerModels":
4343
{
44-
const newModels = message.lmStudioModels ?? []
44+
const newModels = Object.keys(message.routerModels?.lmstudio || {})
4545
setLmStudioModels(newModels)
4646
}
4747
break
@@ -53,7 +53,7 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
5353
// Refresh models on mount
5454
useEffect(() => {
5555
// Request fresh models - the handler now flushes cache automatically
56-
vscode.postMessage({ type: "requestLmStudioModels" })
56+
vscode.postMessage({ type: "requestRouterModels" })
5757
}, [])
5858

5959
// Check if the selected model exists in the fetched models

0 commit comments

Comments
 (0)