Skip to content

Commit 37ed013

Browse files
fix context length for lmstudio and ollama (#2462) (#4314)
Co-authored-by: Daniel Riccio <[email protected]>
1 parent a24b721 commit 37ed013

File tree

23 files changed

+865
-44
lines changed

23 files changed

+865
-44
lines changed

packages/types/src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from "./groq.js"
88
export * from "./lite-llm.js"
99
export * from "./lm-studio.js"
1010
export * from "./mistral.js"
11+
export * from "./ollama.js"
1112
export * from "./openai.js"
1213
export * from "./openrouter.js"
1314
export * from "./requesty.js"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,19 @@
1+
import type { ModelInfo } from "../model.js"
2+
13
export const LMSTUDIO_DEFAULT_TEMPERATURE = 0
4+
5+
// LM Studio
6+
// https://lmstudio.ai/docs/cli/ls
7+
export const lMStudioDefaultModelId = "mistralai/devstral-small-2505"
8+
export const lMStudioDefaultModelInfo: ModelInfo = {
9+
maxTokens: 8192,
10+
contextWindow: 200_000,
11+
supportsImages: true,
12+
supportsComputerUse: true,
13+
supportsPromptCache: true,
14+
inputPrice: 0,
15+
outputPrice: 0,
16+
cacheWritesPrice: 0,
17+
cacheReadsPrice: 0,
18+
description: "LM Studio hosted models",
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { ModelInfo } from "../model.js"
2+
3+
// Ollama
4+
// https://ollama.com/models
5+
export const ollamaDefaultModelId = "devstral:24b"
6+
export const ollamaDefaultModelInfo: ModelInfo = {
7+
maxTokens: 4096,
8+
contextWindow: 200_000,
9+
supportsImages: true,
10+
supportsComputerUse: true,
11+
supportsPromptCache: true,
12+
inputPrice: 0,
13+
outputPrice: 0,
14+
cacheWritesPrice: 0,
15+
cacheReadsPrice: 0,
16+
description: "Ollama hosted models",
17+
}

pnpm-lock.yaml

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"mistralai/devstral-small-2505": {
3+
"type": "llm",
4+
"modelKey": "mistralai/devstral-small-2505",
5+
"format": "safetensors",
6+
"displayName": "Devstral Small 2505",
7+
"path": "mistralai/devstral-small-2505",
8+
"sizeBytes": 13277565112,
9+
"architecture": "mistral",
10+
"vision": false,
11+
"trainedForToolUse": false,
12+
"maxContextLength": 131072
13+
}
14+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"qwen3-2to16:latest": {
3+
"license": " Apache License\\n Version 2.0, January 2004\\n...",
4+
"modelfile": "model.modelfile,# To build a new Modelfile based on this, replace FROM with:...",
5+
"parameters": "repeat_penalty 1\\nstop \\\\nstop...",
6+
"template": "{{- if .Messages }}\\n{{- if or .System .Tools }}<|im_start|>system...",
7+
"details": {
8+
"parent_model": "/Users/brad/.ollama/models/blobs/sha256-3291abe70f16ee9682de7bfae08db5373ea9d6497e614aaad63340ad421d6312",
9+
"format": "gguf",
10+
"family": "qwen3",
11+
"families": ["qwen3"],
12+
"parameter_size": "32.8B",
13+
"quantization_level": "Q4_K_M"
14+
},
15+
"model_info": {
16+
"general.architecture": "qwen3",
17+
"general.basename": "Qwen3",
18+
"general.file_type": 15,
19+
"general.parameter_count": 32762123264,
20+
"general.quantization_version": 2,
21+
"general.size_label": "32B",
22+
"general.type": "model",
23+
"qwen3.attention.head_count": 64,
24+
"qwen3.attention.head_count_kv": 8,
25+
"qwen3.attention.key_length": 128,
26+
"qwen3.attention.layer_norm_rms_epsilon": 0.000001,
27+
"qwen3.attention.value_length": 128,
28+
"qwen3.block_count": 64,
29+
"qwen3.context_length": 40960,
30+
"qwen3.embedding_length": 5120,
31+
"qwen3.feed_forward_length": 25600,
32+
"qwen3.rope.freq_base": 1000000,
33+
"tokenizer.ggml.add_bos_token": false,
34+
"tokenizer.ggml.bos_token_id": 151643,
35+
"tokenizer.ggml.eos_token_id": 151645,
36+
"tokenizer.ggml.merges": null,
37+
"tokenizer.ggml.model": "gpt2",
38+
"tokenizer.ggml.padding_token_id": 151643,
39+
"tokenizer.ggml.pre": "qwen2",
40+
"tokenizer.ggml.token_type": null,
41+
"tokenizer.ggml.tokens": null
42+
},
43+
"tensors": [
44+
{
45+
"name": "output.weight",
46+
"type": "Q6_K",
47+
"shape": [5120, 151936]
48+
},
49+
{
50+
"name": "output_norm.weight",
51+
"type": "F32",
52+
"shape": [5120]
53+
}
54+
],
55+
"capabilities": ["completion", "tools"],
56+
"modified_at": "2025-06-02T22:16:13.644123606-04:00"
57+
}
58+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import axios from "axios"
2+
import { vi, describe, it, expect, beforeEach } from "vitest"
3+
import { LMStudioClient, LLM, LLMInstanceInfo } from "@lmstudio/sdk" // LLMInfo is a type
4+
import { getLMStudioModels, parseLMStudioModel } from "../lmstudio"
5+
import { ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" // ModelInfo is a type
6+
7+
// Mock axios
8+
vi.mock("axios")
9+
const mockedAxios = axios as any
10+
11+
// Mock @lmstudio/sdk
12+
const mockGetModelInfo = vi.fn()
13+
const mockListLoaded = vi.fn()
14+
vi.mock("@lmstudio/sdk", () => {
15+
return {
16+
LMStudioClient: vi.fn().mockImplementation(() => ({
17+
llm: {
18+
listLoaded: mockListLoaded,
19+
},
20+
})),
21+
}
22+
})
23+
const MockedLMStudioClientConstructor = LMStudioClient as any
24+
25+
describe("LMStudio Fetcher", () => {
26+
beforeEach(() => {
27+
vi.clearAllMocks()
28+
MockedLMStudioClientConstructor.mockClear()
29+
mockListLoaded.mockClear()
30+
mockGetModelInfo.mockClear()
31+
})
32+
33+
describe("parseLMStudioModel", () => {
34+
it("should correctly parse raw LLMInfo to ModelInfo", () => {
35+
const rawModel: LLMInstanceInfo = {
36+
type: "llm",
37+
modelKey: "mistralai/devstral-small-2505",
38+
format: "safetensors",
39+
displayName: "Devstral Small 2505",
40+
path: "mistralai/devstral-small-2505",
41+
sizeBytes: 13277565112,
42+
architecture: "mistral",
43+
identifier: "mistralai/devstral-small-2505",
44+
instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ",
45+
vision: false,
46+
trainedForToolUse: false,
47+
maxContextLength: 131072,
48+
contextLength: 7161,
49+
}
50+
51+
const expectedModelInfo: ModelInfo = {
52+
...lMStudioDefaultModelInfo,
53+
description: `${rawModel.displayName} - ${rawModel.path}`,
54+
contextWindow: rawModel.contextLength,
55+
supportsPromptCache: true,
56+
supportsImages: rawModel.vision,
57+
supportsComputerUse: false,
58+
maxTokens: rawModel.contextLength,
59+
inputPrice: 0,
60+
outputPrice: 0,
61+
cacheWritesPrice: 0,
62+
cacheReadsPrice: 0,
63+
}
64+
65+
const result = parseLMStudioModel(rawModel)
66+
expect(result).toEqual(expectedModelInfo)
67+
})
68+
})
69+
70+
describe("getLMStudioModels", () => {
71+
const baseUrl = "http://localhost:1234"
72+
const lmsUrl = "ws://localhost:1234"
73+
74+
const mockRawModel: LLMInstanceInfo = {
75+
architecture: "test-arch",
76+
identifier: "mistralai/devstral-small-2505",
77+
instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ",
78+
modelKey: "test-model-key-1",
79+
path: "/path/to/test-model-1",
80+
type: "llm",
81+
displayName: "Test Model One",
82+
maxContextLength: 2048,
83+
contextLength: 7161,
84+
paramsString: "1B params, 2k context",
85+
vision: true,
86+
format: "gguf",
87+
sizeBytes: 1000000000,
88+
trainedForToolUse: false, // Added
89+
}
90+
91+
it("should fetch and parse models successfully", async () => {
92+
mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
93+
mockListLoaded.mockResolvedValueOnce([{ getModelInfo: mockGetModelInfo }])
94+
mockGetModelInfo.mockResolvedValueOnce(mockRawModel)
95+
96+
const result = await getLMStudioModels(baseUrl)
97+
98+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
99+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
100+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
101+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
102+
expect(mockListLoaded).toHaveBeenCalledTimes(1)
103+
104+
const expectedParsedModel = parseLMStudioModel(mockRawModel)
105+
expect(result).toEqual({ [mockRawModel.modelKey]: expectedParsedModel })
106+
})
107+
108+
it("should use default baseUrl if an empty string is provided", async () => {
109+
const defaultBaseUrl = "http://localhost:1234"
110+
const defaultLmsUrl = "ws://localhost:1234"
111+
mockedAxios.get.mockResolvedValueOnce({ data: {} })
112+
mockListLoaded.mockResolvedValueOnce([])
113+
114+
await getLMStudioModels("")
115+
116+
expect(mockedAxios.get).toHaveBeenCalledWith(`${defaultBaseUrl}/v1/models`)
117+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: defaultLmsUrl })
118+
})
119+
120+
it("should transform https baseUrl to wss for LMStudioClient", async () => {
121+
const httpsBaseUrl = "https://securehost:4321"
122+
const wssLmsUrl = "wss://securehost:4321"
123+
mockedAxios.get.mockResolvedValueOnce({ data: {} })
124+
mockListLoaded.mockResolvedValueOnce([])
125+
126+
await getLMStudioModels(httpsBaseUrl)
127+
128+
expect(mockedAxios.get).toHaveBeenCalledWith(`${httpsBaseUrl}/v1/models`)
129+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: wssLmsUrl })
130+
})
131+
132+
it("should return an empty object if lmsUrl is unparsable", async () => {
133+
const unparsableBaseUrl = "http://localhost:invalid:port" // Leads to ws://localhost:invalid:port
134+
135+
const result = await getLMStudioModels(unparsableBaseUrl)
136+
137+
expect(result).toEqual({})
138+
expect(mockedAxios.get).not.toHaveBeenCalled()
139+
expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
140+
})
141+
142+
it("should return an empty object and log error if axios.get fails with a generic error", async () => {
143+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
144+
const networkError = new Error("Network connection failed")
145+
mockedAxios.get.mockRejectedValueOnce(networkError)
146+
147+
const result = await getLMStudioModels(baseUrl)
148+
149+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
150+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
151+
expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
152+
expect(mockListLoaded).not.toHaveBeenCalled()
153+
expect(consoleErrorSpy).toHaveBeenCalledWith(
154+
`Error fetching LMStudio models: ${JSON.stringify(networkError, Object.getOwnPropertyNames(networkError), 2)}`,
155+
)
156+
expect(result).toEqual({})
157+
consoleErrorSpy.mockRestore()
158+
})
159+
160+
it("should return an empty object and log info if axios.get fails with ECONNREFUSED", async () => {
161+
const consoleInfoSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
162+
const econnrefusedError = new Error("Connection refused")
163+
;(econnrefusedError as any).code = "ECONNREFUSED"
164+
mockedAxios.get.mockRejectedValueOnce(econnrefusedError)
165+
166+
const result = await getLMStudioModels(baseUrl)
167+
168+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
169+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/v1/models`)
170+
expect(MockedLMStudioClientConstructor).not.toHaveBeenCalled()
171+
expect(mockListLoaded).not.toHaveBeenCalled()
172+
expect(consoleInfoSpy).toHaveBeenCalledWith(`Error connecting to LMStudio at ${baseUrl}`)
173+
expect(result).toEqual({})
174+
consoleInfoSpy.mockRestore()
175+
})
176+
177+
it("should return an empty object and log error if listDownloadedModels fails", async () => {
178+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
179+
const listError = new Error("LMStudio SDK internal error")
180+
181+
mockedAxios.get.mockResolvedValueOnce({ data: {} })
182+
mockListLoaded.mockRejectedValueOnce(listError)
183+
184+
const result = await getLMStudioModels(baseUrl)
185+
186+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
187+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledTimes(1)
188+
expect(MockedLMStudioClientConstructor).toHaveBeenCalledWith({ baseUrl: lmsUrl })
189+
expect(mockListLoaded).toHaveBeenCalledTimes(1)
190+
expect(consoleErrorSpy).toHaveBeenCalledWith(
191+
`Error fetching LMStudio models: ${JSON.stringify(listError, Object.getOwnPropertyNames(listError), 2)}`,
192+
)
193+
expect(result).toEqual({})
194+
consoleErrorSpy.mockRestore()
195+
})
196+
})
197+
})

0 commit comments

Comments
 (0)