Skip to content

Commit 80e27c2

Browse files
committed
Merge remote-tracking branch 'origin/main' into cloud_account_switcher
2 parents 27db8e6 + 12f94fc commit 80e27c2

File tree

26 files changed

+213
-12
lines changed

26 files changed

+213
-12
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ const ollamaSchema = baseProviderSettingsSchema.extend({
258258
ollamaModelId: z.string().optional(),
259259
ollamaBaseUrl: z.string().optional(),
260260
ollamaApiKey: z.string().optional(),
261+
ollamaNumCtx: z.number().int().min(128).optional(),
261262
})
262263

263264
const vsCodeLmSchema = baseProviderSettingsSchema.extend({

src/api/providers/__tests__/native-ollama.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,61 @@ describe("NativeOllamaHandler", () => {
7373
expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 })
7474
})
7575

76+
it("should not include num_ctx by default", async () => {
77+
// Mock the chat response
78+
mockChat.mockImplementation(async function* () {
79+
yield { message: { content: "Response" } }
80+
})
81+
82+
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
83+
84+
// Consume the stream
85+
for await (const _ of stream) {
86+
// consume stream
87+
}
88+
89+
// Verify that num_ctx was NOT included in the options
90+
expect(mockChat).toHaveBeenCalledWith(
91+
expect.objectContaining({
92+
options: expect.not.objectContaining({
93+
num_ctx: expect.anything(),
94+
}),
95+
}),
96+
)
97+
})
98+
99+
it("should include num_ctx when explicitly set via ollamaNumCtx", async () => {
100+
const options: ApiHandlerOptions = {
101+
apiModelId: "llama2",
102+
ollamaModelId: "llama2",
103+
ollamaBaseUrl: "http://localhost:11434",
104+
ollamaNumCtx: 8192, // Explicitly set num_ctx
105+
}
106+
107+
handler = new NativeOllamaHandler(options)
108+
109+
// Mock the chat response
110+
mockChat.mockImplementation(async function* () {
111+
yield { message: { content: "Response" } }
112+
})
113+
114+
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
115+
116+
// Consume the stream
117+
for await (const _ of stream) {
118+
// consume stream
119+
}
120+
121+
// Verify that num_ctx was included with the specified value
122+
expect(mockChat).toHaveBeenCalledWith(
123+
expect.objectContaining({
124+
options: expect.objectContaining({
125+
num_ctx: 8192,
126+
}),
127+
}),
128+
)
129+
})
130+
76131
it("should handle DeepSeek R1 models with reasoning detection", async () => {
77132
const options: ApiHandlerOptions = {
78133
apiModelId: "deepseek-r1",
@@ -120,6 +175,49 @@ describe("NativeOllamaHandler", () => {
120175
})
121176
expect(result).toBe("This is the response")
122177
})
178+
179+
it("should not include num_ctx in completePrompt by default", async () => {
180+
mockChat.mockResolvedValue({
181+
message: { content: "Response" },
182+
})
183+
184+
await handler.completePrompt("Test prompt")
185+
186+
// Verify that num_ctx was NOT included in the options
187+
expect(mockChat).toHaveBeenCalledWith(
188+
expect.objectContaining({
189+
options: expect.not.objectContaining({
190+
num_ctx: expect.anything(),
191+
}),
192+
}),
193+
)
194+
})
195+
196+
it("should include num_ctx in completePrompt when explicitly set", async () => {
197+
const options: ApiHandlerOptions = {
198+
apiModelId: "llama2",
199+
ollamaModelId: "llama2",
200+
ollamaBaseUrl: "http://localhost:11434",
201+
ollamaNumCtx: 4096, // Explicitly set num_ctx
202+
}
203+
204+
handler = new NativeOllamaHandler(options)
205+
206+
mockChat.mockResolvedValue({
207+
message: { content: "Response" },
208+
})
209+
210+
await handler.completePrompt("Test prompt")
211+
212+
// Verify that num_ctx was included with the specified value
213+
expect(mockChat).toHaveBeenCalledWith(
214+
expect.objectContaining({
215+
options: expect.objectContaining({
216+
num_ctx: 4096,
217+
}),
218+
}),
219+
)
220+
})
123221
})
124222

125223
describe("error handling", () => {

src/api/providers/native-ollama.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { getOllamaModels } from "./fetchers/ollama"
88
import { XmlMatcher } from "../../utils/xml-matcher"
99
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
1010

11+
interface OllamaChatOptions {
12+
temperature: number
13+
num_ctx?: number
14+
}
15+
1116
function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
1217
const ollamaMessages: Message[] = []
1318

@@ -184,15 +189,22 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
184189
)
185190

186191
try {
192+
// Build options object conditionally
193+
const chatOptions: OllamaChatOptions = {
194+
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
195+
}
196+
197+
// Only include num_ctx if explicitly set via ollamaNumCtx
198+
if (this.options.ollamaNumCtx !== undefined) {
199+
chatOptions.num_ctx = this.options.ollamaNumCtx
200+
}
201+
187202
// Create the actual API request promise
188203
const stream = await client.chat({
189204
model: modelId,
190205
messages: ollamaMessages,
191206
stream: true,
192-
options: {
193-
num_ctx: modelInfo.contextWindow,
194-
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
195-
},
207+
options: chatOptions,
196208
})
197209

198210
let totalInputTokens = 0
@@ -274,13 +286,21 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
274286
const { id: modelId } = await this.fetchModel()
275287
const useR1Format = modelId.toLowerCase().includes("deepseek-r1")
276288

289+
// Build options object conditionally
290+
const chatOptions: OllamaChatOptions = {
291+
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
292+
}
293+
294+
// Only include num_ctx if explicitly set via ollamaNumCtx
295+
if (this.options.ollamaNumCtx !== undefined) {
296+
chatOptions.num_ctx = this.options.ollamaNumCtx
297+
}
298+
277299
const response = await client.chat({
278300
model: modelId,
279301
messages: [{ role: "user", content: prompt }],
280302
stream: false,
281-
options: {
282-
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
283-
},
303+
options: chatOptions,
284304
})
285305

286306
return response.message?.content || ""

src/core/task/Task.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,10 +1944,22 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
19441944
}
19451945

19461946
switch (chunk.type) {
1947-
case "reasoning":
1947+
case "reasoning": {
19481948
reasoningMessage += chunk.text
1949-
await this.say("reasoning", reasoningMessage, undefined, true)
1949+
// Only apply formatting if the message contains sentence-ending punctuation followed by **
1950+
let formattedReasoning = reasoningMessage
1951+
if (reasoningMessage.includes("**")) {
1952+
// Add line breaks before **Title** patterns that appear after sentence endings
1953+
// This targets section headers like "...end of sentence.**Title Here**"
1954+
// Handles periods, exclamation marks, and question marks
1955+
formattedReasoning = reasoningMessage.replace(
1956+
/([.!?])\*\*([^*\n]+)\*\*/g,
1957+
"$1\n\n**$2**",
1958+
)
1959+
}
1960+
await this.say("reasoning", formattedReasoning, undefined, true)
19501961
break
1962+
}
19511963
case "usage":
19521964
inputTokens += chunk.inputTokens
19531965
outputTokens += chunk.outputTokens

src/shared/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ export type ApiHandlerOptions = Omit<ProviderSettings, "apiProvider"> & {
1818
* Defaults to true; set to false to disable summaries.
1919
*/
2020
enableGpt5ReasoningSummary?: boolean
21+
/**
22+
* Optional override for Ollama's num_ctx parameter.
23+
* When set, this value will be used in Ollama chat requests.
24+
* When undefined, Ollama will use the model's default num_ctx from the Modelfile.
25+
*/
26+
ollamaNumCtx?: number
2127
}
2228

2329
// RouterName

webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const CheckpointSaved = ({ checkpoint, ...props }: CheckpointSavedProps)
3636

3737
return (
3838
<div className="group flex items-center justify-between gap-2 pt-2 pb-3 ">
39-
<div className="flex items-center gap-2 text-blue-400">
39+
<div className="flex items-center gap-2 text-blue-400 whitespace-nowrap">
4040
<GitCommitVertical className="w-4" />
4141
<span className="font-semibold">{t("chat:checkpoint.regular")}</span>
4242
{isCurrent && <span className="text-muted">({t("chat:checkpoint.current")})</span>}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
130130
))}
131131
</VSCodeRadioGroup>
132132
)}
133+
<VSCodeTextField
134+
value={apiConfiguration?.ollamaNumCtx?.toString() || ""}
135+
onInput={(e: any) => {
136+
const value = e.target?.value
137+
if (value === "") {
138+
setApiConfigurationField("ollamaNumCtx", undefined)
139+
} else {
140+
const numValue = parseInt(value, 10)
141+
if (!isNaN(numValue) && numValue >= 128) {
142+
setApiConfigurationField("ollamaNumCtx", numValue)
143+
}
144+
}
145+
}}
146+
placeholder="e.g., 4096"
147+
className="w-full">
148+
<label className="block font-medium mb-1">{t("settings:providers.ollama.numCtx")}</label>
149+
<div className="text-xs text-vscode-descriptionForeground mt-1">
150+
{t("settings:providers.ollama.numCtxHelp")}
151+
</div>
152+
</VSCodeTextField>
133153
<div className="text-sm text-vscode-descriptionForeground">
134154
{t("settings:providers.ollama.description")}
135155
<span className="text-vscode-errorForeground ml-1">{t("settings:providers.ollama.warning")}</span>

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,17 @@ function getSelectedModel({
263263
case "ollama": {
264264
const id = apiConfiguration.ollamaModelId ?? ""
265265
const info = ollamaModels && ollamaModels[apiConfiguration.ollamaModelId!]
266+
267+
const adjustedInfo =
268+
info?.contextWindow &&
269+
apiConfiguration?.ollamaNumCtx &&
270+
apiConfiguration.ollamaNumCtx < info.contextWindow
271+
? { ...info, contextWindow: apiConfiguration.ollamaNumCtx }
272+
: info
273+
266274
return {
267275
id,
268-
info: info || undefined,
276+
info: adjustedInfo || undefined,
269277
}
270278
}
271279
case "lmstudio": {

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)