diff --git a/src/api/providers/__tests__/native-ollama.spec.ts b/src/api/providers/__tests__/native-ollama.spec.ts index 8a6b025c8f..847a3f7f67 100644 --- a/src/api/providers/__tests__/native-ollama.spec.ts +++ b/src/api/providers/__tests__/native-ollama.spec.ts @@ -254,6 +254,431 @@ describe("NativeOllamaHandler", () => { expect(results.some((r) => r.type === "reasoning")).toBe(true) expect(results.some((r) => r.type === "text")).toBe(true) }) + + it("should surface Ollama's native message.thinking field as reasoning", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "", thinking: "Reasoning step one" } } + yield { message: { content: "", thinking: " step two" } } + yield { message: { content: "The answer" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Question?" }]) + const results = [] + + for await (const chunk of stream) { + results.push(chunk) + } + + const reasoningChunks = results.filter((r) => r.type === "reasoning") + expect(reasoningChunks).toHaveLength(2) + expect(reasoningChunks[0]).toEqual({ type: "reasoning", text: "Reasoning step one" }) + expect(reasoningChunks[1]).toEqual({ type: "reasoning", text: " step two" }) + expect(results.some((r) => r.type === "text" && r.text === "The answer")).toBe(true) + }) + + it("should send think parameter when reasoningEffort is set", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok", thinking: "hmm" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: "high", + }), + ) + }) + + it("should map reasoningEffort levels to Ollama think values", async () => { + const cases: Array< + [NonNullable, boolean | "high" | "medium" | "low"] + > = [ + ["low", "low"], + ["medium", "medium"], + ["high", "high"], + ["xhigh", "high"], + ["max", "high"], + ["none", true], + ["minimal", true], + ["disable", false], + ] + + for (const [effort, expected] of cases) { + vitest.clearAllMocks() + mockGetOllamaModels.mockResolvedValue({ + qwen3: { contextWindow: 4096, maxTokens: 4096, supportsImages: false, supportsPromptCache: false }, + }) + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: effort, + } + + handler = new NativeOllamaHandler(options) + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: expected, + }), + ) + } + }) + + it("should not send think parameter when reasoningEffort is undefined", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter when enableReasoningEffort is false", async () => { + // When the Ollama UI checkbox is unchecked, enableReasoningEffort + // is false. The handler must not send any think param (undefined), + // leaving the model/Modelfile in control rather than forcing + // thinking off. A stale reasoningEffort value must not override + // the explicit opt-out. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: false, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter when enableReasoningEffort is undefined but reasoningEffort is set", async () => { + // This guards against a stale reasoningEffort inherited from + // another provider config. Without an explicit Ollama opt-in, + // the handler must not emit a think param. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + // enableReasoningEffort intentionally undefined + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should send think=false when reasoningEffort is disable and enableReasoningEffort is true", async () => { + // The only way to explicitly force thinking off via the think + // parameter is to set reasoningEffort to "disable" while opted in. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "disable", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: false, + }), + ) + }) + + it("should round-trip reasoning blocks as the thinking field on assistant messages", async () => { + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "Prior reasoning", summary: [] } as any, + { type: "text", text: "Prior answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "Prior reasoning", + }), + ]), + }), + ) + }) + it("should round-trip Anthropic-protocol thinking blocks as the thinking field on assistant messages", async () => { + // Covers the `block.type === "thinking"` branch in the assistant + // message converter. Anthropic-protocol thinking blocks carry the + // reasoning text in a `thinking` field (not `text`). + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "thinking", thinking: "Anthropic thinking text" } as any, + { type: "text", text: "Prior answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "Anthropic thinking text", + }), + ]), + }), + ) + }) + + it("should concatenate multiple reasoning and thinking blocks into the thinking field", async () => { + // Multiple reasoning/thinking blocks are joined with newlines so the + // full thinking context is preserved across turns. + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "First reasoning", summary: [] } as any, + { type: "thinking", thinking: "Second thinking" } as any, + { type: "text", text: "Answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: "First reasoning\nSecond thinking", + }), + ]), + }), + ) + }) + + it("should not set thinking field when assistant reasoning/thinking blocks are empty", async () => { + // Covers the `block.text.length > 0` and `block.thinking.length > 0` + // false branches, and the `reasoningText || undefined` falsy branch. + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "reasoning", text: "", summary: [] } as any, + { type: "thinking", thinking: "" } as any, + { type: "text", text: "Answer" }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: undefined, + }), + ]), + }), + ) + }) + + it("should not set thinking field on assistant messages without reasoning blocks", async () => { + // Covers the `reasoningText || undefined` falsy branch for a plain + // assistant text+tool_use message (no reasoning/thinking blocks). + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "assistant", + content: [ + { type: "text", text: "Answer" }, + { + type: "tool_use", + id: "tool-1", + name: "get_weather", + input: { location: "SF" }, + }, + ], + }, + { role: "user" as const, content: "Follow up" }, + ] + + const stream = handler.createMessage("System", messages) + for await (const _ of stream) { + // consume + } + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: "assistant", + thinking: undefined, + }), + ]), + }), + ) + }) + + it("should not send think parameter for an unknown reasoningEffort value", async () => { + // Covers the `default` branch of getOllamaThinkParam's switch, + // which returns undefined for unrecognized effort values. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "bogus" as any, + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + }) + + it("should not send think parameter when enableReasoningEffort is true but reasoningEffort is undefined", async () => { + // This is the state the UI checkbox would produce if it only set + // enableReasoningEffort without a default reasoningEffort. The + // handler must not send a think param in that case. + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + // reasoningEffort intentionally undefined + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockImplementation(async function* () { + yield { message: { content: "ok" } } + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Hi" }]) + for await (const _ of stream) { + // consume + } + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() }) describe("completePrompt", () => { @@ -319,6 +744,70 @@ describe("NativeOllamaHandler", () => { }) }) + it("should send think parameter in completePrompt when reasoningEffort is set", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: true, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + expect(mockChat).toHaveBeenCalledWith( + expect.objectContaining({ + think: "high", + }), + ) + }) + + it("should not send think parameter in completePrompt when reasoningEffort is undefined", async () => { + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should not send think parameter in completePrompt when enableReasoningEffort is false", async () => { + const options: ApiHandlerOptions = { + apiModelId: "qwen3", + ollamaModelId: "qwen3", + ollamaBaseUrl: "http://localhost:11434", + enableReasoningEffort: false, + reasoningEffort: "high", + } + + handler = new NativeOllamaHandler(options) + + mockChat.mockResolvedValue({ + message: { content: "Response" }, + }) + + await handler.completePrompt("Test prompt") + + const callArgs = mockChat.mock.calls[0][0] as Record + expect(callArgs.think).toBeUndefined() + }) + + it("should wrap non-Error throws from completePrompt", async () => { + // Covers the `throw error` branch when the rejected value is not an + // Error instance (e.g. a plain object or string). + mockChat.mockRejectedValue("boom") + + await expect(handler.completePrompt("Test prompt")).rejects.toBe("boom") + }) + describe("error handling", () => { it("should handle connection refused errors", async () => { const error = new Error("ECONNREFUSED") as any @@ -347,6 +836,58 @@ describe("NativeOllamaHandler", () => { } }).rejects.toThrow("Model llama2 not found in Ollama") }) + + it("should wrap stream processing errors with a descriptive message", async () => { + // Covers the `catch (streamError)` branch: the chat() call + // resolves and returns an async iterable, but iterating it throws. + // The handler must wrap the error with "Ollama stream processing + // error: ..." and rethrow. + mockChat.mockImplementation(async function* () { + yield { message: { content: "partial" } } + throw new Error("stream blew up") + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Ollama stream processing error: stream blew up") + }) + + it("should wrap stream processing errors with unknown message fallback", async () => { + // Covers the `streamError.message || "Unknown error"` fallback in + // the stream processing catch block when the error has no message. + mockChat.mockImplementation(async function* () { + yield { message: { content: "partial" } } + throw {} + }) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("Ollama stream processing error: Unknown error") + }) + + it("should rethrow non-ECONNREFUSED non-404 errors from chat()", async () => { + // Covers the fall-through `throw error` branch in the outer catch + // when the error is neither ECONNREFUSED nor a 404. + const error = new Error("something else") as any + error.status = 500 + mockChat.mockRejectedValue(error) + + const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }]) + + await expect(async () => { + for await (const _ of stream) { + // consume stream + } + }).rejects.toThrow("something else") + }) }) describe("getModel", () => { diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts index 4574c184f2..51d45981b4 100644 --- a/src/api/providers/native-ollama.ts +++ b/src/api/providers/native-ollama.ts @@ -14,6 +14,15 @@ interface OllamaChatOptions { num_ctx?: number } +// Narrow local types for non-Anthropic content blocks that may be carried in +// the conversation history. The Anthropic SDK union does not include the +// custom `reasoning` block (used by non-Anthropic protocols) or the +// Anthropic-protocol `thinking` block, so we declare them here to keep type +// checking intact for the rest of the union instead of falling back to `any`. +type ReasoningContentBlock = { type: "reasoning"; text: string } +type ThinkingContentBlock = { type: "thinking"; thinking: string } +type AssistantContentBlock = Anthropic.ContentBlock | ReasoningContentBlock | ThinkingContentBlock + function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] { const ollamaMessages: Message[] = [] @@ -97,32 +106,64 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa }) } } else if (anthropicMessage.role === "assistant") { - const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{ - nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] + // Assistant message conversion: only `text`, `tool_use`, and the + // custom `reasoning`/`thinking` blocks are relevant here. + // + // Note on the removed `image` branch: the previous code checked + // `block.type === "image"` and typed `nonToolMessages` as + // `(Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]`. This + // was removed because: + // 1. The Anthropic API only accepts image blocks in *user* + // messages — assistants cannot produce or send images, so + // the branch was dead code (the original comment even said + // "impossible as the assistant cannot send images"). + // 2. `Anthropic.ContentBlock` (the response/output union) does + // not include an `image` variant, so the comparison + // `block.type === "image"` triggered TS2367 ("this comparison + // appears to be unintentional because the types have no + // overlap"). + // 3. Image handling for *user* messages (where images are + // actually sent to the model) is preserved unchanged in the + // `anthropicMessage.role === "user"` branch above. + const { nonToolMessages, toolMessages, reasoningText } = anthropicMessage.content.reduce<{ + nonToolMessages: Anthropic.TextBlockParam[] toolMessages: Anthropic.ToolUseBlockParam[] + reasoningText: string }>( (acc, part) => { - if (part.type === "tool_use") { - acc.toolMessages.push(part) - } else if (part.type === "text" || part.type === "image") { - acc.nonToolMessages.push(part) + // `part` is typed as an Anthropic content block, but the + // conversation history may also carry custom `reasoning` + // blocks (used by non-Anthropic protocols) or Anthropic + // `thinking` blocks that are not part of the SDK union. + // Cast to the augmented union to access them while + // preserving type safety for the rest of the block. + const block = part as AssistantContentBlock + if (block.type === "tool_use") { + acc.toolMessages.push(block) + } else if (block.type === "text") { + acc.nonToolMessages.push(block) + } else if (block.type === "reasoning") { + // Non-Anthropic protocols store reasoning as a block + // with a `text` field. Pass it back so Ollama can + // preserve thinking context across turns. + if (block.text.length > 0) { + acc.reasoningText += (acc.reasoningText ? "\n" : "") + block.text + } + } else if (block.type === "thinking") { + // Anthropic-protocol thinking blocks carry `thinking`. + if (block.thinking.length > 0) { + acc.reasoningText += (acc.reasoningText ? "\n" : "") + block.thinking + } } // assistant cannot send tool_result messages return acc }, - { nonToolMessages: [], toolMessages: [] }, + { nonToolMessages: [], toolMessages: [], reasoningText: "" }, ) // Process non-tool messages let content: string = "" if (nonToolMessages.length > 0) { - content = nonToolMessages - .map((part) => { - if (part.type === "image") { - return "" // impossible as the assistant cannot send images - } - return part.text - }) - .join("\n") + content = nonToolMessages.map((part) => part.text).join("\n") } // Convert tool_use blocks to Ollama tool_calls format @@ -140,6 +181,8 @@ function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessagePa role: "assistant", content, tool_calls: toolCalls, + // Round-trip prior reasoning so multi-turn thinking is preserved. + thinking: reasoningText || undefined, }) } } @@ -203,6 +246,90 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio })) } + /** + * Maps the configured reasoning effort setting to Ollama's native `think` + * request parameter (boolean | "high" | "medium" | "low"). + * + * Requires an explicit Ollama opt-in (`enableReasoningEffort === true`) + * before translating `reasoningEffort`. This prevents inherited + * `apiConfiguration.reasoningEffort` values (left over from another + * provider) from silently emitting a `think` param when the Ollama UI + * checkbox is unchecked. + * + * Returns undefined when reasoning is not explicitly enabled, leaving + * the model/Modelfile in control (preserving prior behavior where models + * that emit think/thought tags in content are still handled by TagMatcher). + * + * Note: The Ollama API itself also accepts `"max"` (see + * https://docs.ollama.com/capabilities/thinking), but the installed + * `ollama` SDK (v0.6.x) only types `think` as + * `boolean | "high" | "medium" | "low"`. Until the SDK types catch up, + * "xhigh"/"max" efforts are clamped to "high". + * + * - enableReasoningEffort !== true -> undefined (no think param sent) + * - "disable" -> false (thinking off) + * - "none" / "minimal" -> true (enable thinking with default budget) + * - "low" / "medium" / "high" -> the matching effort level + * - "xhigh" / "max" -> "high" (highest level the SDK currently supports) + */ + private getOllamaThinkParam(): boolean | "high" | "medium" | "low" | undefined { + // Require an explicit Ollama opt-in before mapping reasoningEffort. + // Without this guard, a stale reasoningEffort inherited from another + // provider config could still emit a think param when the UI checkbox + // is unchecked. + if (this.options.enableReasoningEffort !== true) { + return undefined + } + + const effort = this.options.reasoningEffort + if (effort === undefined) { + return undefined + } + + switch (effort) { + case "disable": + return false + case "none": + case "minimal": + return true + case "low": + return "low" + case "medium": + return "medium" + case "high": + case "xhigh": + case "max": + return "high" + default: + return undefined + } + } + + /** + * Builds the shared chat request options (temperature, num_ctx) and the + * conditional `think` parameter used by both `createMessage` and + * `completePrompt`. Centralizing this avoids drift between the streaming + * and single-shot request paths. + * + * Returns a tuple of `[chatOptions, thinkParam]` where `thinkParam` is + * `undefined` when no `think` field should be sent to Ollama. + */ + private buildChatRequestOptions( + useR1Format: boolean, + ): [OllamaChatOptions, boolean | "high" | "medium" | "low" | undefined] { + const chatOptions: OllamaChatOptions = { + temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), + } + + // Only include num_ctx if explicitly set via ollamaNumCtx + if (this.options.ollamaNumCtx !== undefined) { + chatOptions.num_ctx = this.options.ollamaNumCtx + } + + const thinkParam = this.getOllamaThinkParam() + return [chatOptions, thinkParam] + } + override async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], @@ -227,23 +354,24 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio ) try { - // Build options object conditionally - const chatOptions: OllamaChatOptions = { - temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), - } - - // Only include num_ctx if explicitly set via ollamaNumCtx - if (this.options.ollamaNumCtx !== undefined) { - chatOptions.num_ctx = this.options.ollamaNumCtx - } - - // Create the actual API request promise + // Build the shared chat options and conditional think parameter. + // Conditionally enabling Ollama's native think parameter lets + // reasoning models (qwen3, deepseek-r1, etc.) emit thinking via + // the dedicated message.thinking field instead of (or in addition + // to) think/thought tags embedded in content. + const [chatOptions, thinkParam] = this.buildChatRequestOptions(useR1Format) + + // Create the actual API request promise. The `stream: true` literal + // is kept inline so TypeScript selects the streaming overload of + // client.chat. The `think` parameter is spread conditionally to + // avoid sending an explicit `think: undefined` to the runtime. const stream = await client.chat({ model: modelId, messages: ollamaMessages, stream: true, options: chatOptions, tools: this.convertToolsToOllama(metadata?.tools), + ...(thinkParam !== undefined ? { think: thinkParam } : {}), }) let totalInputTokens = 0 @@ -255,6 +383,18 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio try { for await (const chunk of stream) { + // Process Ollama's native thinking field. When the think + // parameter is enabled (or the model thinks by default), + // Ollama streams reasoning via message.thinking separately + // from message.content. Surface it as a reasoning chunk so + // it is rendered and preserved like other providers. + if (typeof chunk.message.thinking === "string" && chunk.message.thinking.length > 0) { + yield { + type: "reasoning", + text: chunk.message.thinking, + } + } + if (typeof chunk.message.content === "string" && chunk.message.content.length > 0) { // Process content through matcher for reasoning detection for (const matcherChunk of matcher.update(chunk.message.content)) { @@ -353,21 +493,17 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio const { id: modelId } = await this.fetchModel() const useR1Format = modelId.toLowerCase().includes("deepseek-r1") - // Build options object conditionally - const chatOptions: OllamaChatOptions = { - temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0), - } - - // Only include num_ctx if explicitly set via ollamaNumCtx - if (this.options.ollamaNumCtx !== undefined) { - chatOptions.num_ctx = this.options.ollamaNumCtx - } + // Reuse the shared request-option builder so single-shot + // completions respect the same reasoning configuration as the + // streaming path. + const [chatOptions, thinkParam] = this.buildChatRequestOptions(useR1Format) const response = await client.chat({ model: modelId, messages: [{ role: "user", content: prompt }], stream: false, options: chatOptions, + ...(thinkParam !== undefined ? { think: thinkParam } : {}), }) return response.message?.content || "" diff --git a/webview-ui/src/components/settings/providers/Ollama.tsx b/webview-ui/src/components/settings/providers/Ollama.tsx index 32f99c6025..9d92ee972b 100644 --- a/webview-ui/src/components/settings/providers/Ollama.tsx +++ b/webview-ui/src/components/settings/providers/Ollama.tsx @@ -1,8 +1,9 @@ import { useState, useCallback, useMemo, useEffect } from "react" import { useEvent } from "react-use" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" +import { Checkbox } from "vscrui" -import type { ProviderSettings, ExtensionMessage, ModelRecord } from "@roo-code/types" +import { type ProviderSettings, type ExtensionMessage, type ModelRecord, ollamaDefaultModelInfo } from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" @@ -10,6 +11,7 @@ import { vscode } from "@src/utils/vscode" import { inputEventTransform } from "../transforms" import { ModelPicker } from "../ModelPicker" +import { ThinkingBudget } from "../ThinkingBudget" type OllamaProps = { apiConfiguration: ProviderSettings @@ -131,6 +133,49 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro {t("settings:providers.ollama.numCtxHelp")} +
+ { + setApiConfigurationField("enableReasoningEffort", checked) + + if (checked) { + // Restore the last selected effort level if one was + // previously chosen; otherwise default to "medium" so + // the request actually enables Ollama's native think + // parameter. Without a value, the ThinkingBudget Select + // would show "None" (disable) and getOllamaThinkParam() + // would return undefined, sending no think parameter + // despite the checkbox being on. Preserving the prior + // value avoids wiping the user's effort choice when + // toggling the checkbox off and back on. + setApiConfigurationField("reasoningEffort", apiConfiguration.reasoningEffort ?? "medium") + } + // When unchecked, leave reasoningEffort untouched so the + // user's prior selection is preserved across toggles. The + // handler gates on enableReasoningEffort === true, so a + // stale reasoningEffort value will not emit a think param + // while the checkbox is off. + }}> + {t("settings:providers.ollama.thinking")} + +
+ {t("settings:providers.ollama.thinkingHelp")} +
+ {!!apiConfiguration.enableReasoningEffort && ( + + )} +
{t("settings:providers.ollama.description")} {t("settings:providers.ollama.warning")} diff --git a/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx new file mode 100644 index 0000000000..91025ee291 --- /dev/null +++ b/webview-ui/src/components/settings/providers/__tests__/Ollama.spec.tsx @@ -0,0 +1,223 @@ +// npx vitest src/components/settings/providers/__tests__/Ollama.spec.tsx + +import React from "react" +import { render, screen, fireEvent } from "@/utils/test-utils" +import { Ollama } from "../Ollama" +import { ProviderSettings } from "@roo-code/types" + +// Mock the vscrui Checkbox component +vi.mock("vscrui", () => ({ + Checkbox: ({ children, checked, onChange }: any) => ( + + ), +})) + +// Mock the VSCodeTextField component +vi.mock("@vscode/webview-ui-toolkit/react", () => ({ + VSCodeTextField: ({ children, value, onInput, placeholder, className, ...rest }: any) => ( +
+ {children} + onInput && onInput(e)} + placeholder={placeholder} + {...rest} + /> +
+ ), +})) + +// Mock the translation hook +vi.mock("@src/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string) => key, + }), +})) + +// Mock the ModelPicker +vi.mock("../../ModelPicker", () => ({ + ModelPicker: () =>
Model Picker
, +})) + +// Mock the ThinkingBudget +vi.mock("../../ThinkingBudget", () => ({ + ThinkingBudget: ({ modelInfo }: any) => ( +
+ Thinking Budget +
+ ), +})) + +// Mock react-use +vi.mock("react-use", () => ({ + useEvent: vi.fn(), +})) + +// Mock useRouterModels +vi.mock("@src/components/ui/hooks/useRouterModels", () => ({ + useRouterModels: () => ({ data: {}, isLoading: false, error: null }), +})) + +// Mock vscode +vi.mock("@src/utils/vscode", () => ({ + vscode: { postMessage: vi.fn() }, +})) + +describe("Ollama Component - thinking setting", () => { + const mockSetApiConfigurationField = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + it("should render the thinking checkbox unchecked by default", () => { + const apiConfiguration: Partial = {} + + render( + , + ) + + const checkbox = screen.getByTestId("checkbox-settings:providers.ollama.thinking") + expect(checkbox).toBeInTheDocument() + + const input = screen.getByTestId("checkbox-input-settings:providers.ollama.thinking") as HTMLInputElement + expect(input.checked).toBe(false) + }) + + it("should render the thinking checkbox checked when enableReasoningEffort is true", () => { + const apiConfiguration: Partial = { + enableReasoningEffort: true, + } + + render( + , + ) + + const input = screen.getByTestId("checkbox-input-settings:providers.ollama.thinking") as HTMLInputElement + expect(input.checked).toBe(true) + }) + + it("should render the thinking help text", () => { + render( + , + ) + + expect(screen.getByText("settings:providers.ollama.thinkingHelp")).toBeInTheDocument() + }) + + it("should enable reasoning effort and default reasoningEffort to medium when the checkbox is toggled on with no prior value", () => { + render( + , + ) + + const input = screen.getByTestId("checkbox-input-settings:providers.ollama.thinking") + fireEvent.click(input) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", true) + // Defaulting to "medium" ensures getOllamaThinkParam() actually sends a + // think parameter instead of leaving reasoningEffort undefined. + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", "medium") + }) + + it("should restore the prior reasoningEffort value when re-enabled after being toggled off", () => { + // The user previously selected "high", toggled the checkbox off (which + // preserves reasoningEffort), and is now toggling it back on. The + // prior effort level should be restored rather than reset to "medium". + const apiConfiguration: Partial = { + enableReasoningEffort: false, + reasoningEffort: "high", + } + + render( + , + ) + + const input = screen.getByTestId("checkbox-input-settings:providers.ollama.thinking") + fireEvent.click(input) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", true) + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", "high") + }) + + it("should disable reasoning effort and preserve reasoningEffort when toggled off", () => { + // Toggling the checkbox off no longer wipes the user's prior effort + // choice. The handler gates on enableReasoningEffort === true, so a + // stale reasoningEffort value will not emit a think param while the + // checkbox is off, and the value is preserved for re-enabling. + const apiConfiguration: Partial = { + enableReasoningEffort: true, + reasoningEffort: "high", + } + + render( + , + ) + + const input = screen.getByTestId("checkbox-input-settings:providers.ollama.thinking") + fireEvent.click(input) + + expect(mockSetApiConfigurationField).toHaveBeenCalledWith("enableReasoningEffort", false) + // reasoningEffort is intentionally left untouched so the user's prior + // selection survives across toggles. + expect(mockSetApiConfigurationField).not.toHaveBeenCalledWith("reasoningEffort", expect.anything()) + }) + + it("should render ThinkingBudget with supportsReasoningEffort when thinking is enabled", () => { + const apiConfiguration: Partial = { + enableReasoningEffort: true, + } + + render( + , + ) + + const thinkingBudget = screen.getByTestId("thinking-budget") + expect(thinkingBudget).toBeInTheDocument() + expect(thinkingBudget.getAttribute("data-supports")).toBe("true") + }) + + it("should not render ThinkingBudget when thinking is disabled", () => { + const apiConfiguration: Partial = { + enableReasoningEffort: false, + } + + render( + , + ) + + expect(screen.queryByTestId("thinking-budget")).toBeNull() + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 779d87b5ee..ae324e5fa0 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Clau API opcional per a instàncies d'Ollama autenticades o serveis al núvol. Deixa-ho buit per a instal·lacions locals.", "numCtx": "Mida de la finestra de context (num_ctx)", "numCtxHelp": "Sobreescriu la mida de la finestra de context per defecte del model. Deixeu-ho en blanc per utilitzar la configuració del Modelfile del model. El valor mínim és 128.", + "thinking": "Activa el raonament", + "thinkingHelp": "Activa el raonament natiu d'Ollama per a models de raonament (p. ex. qwen3, deepseek-r1). Quan està activat, el raonament del model es transmet i es mostra per separat. Els models no compatibles ignoraran aquesta configuració.", "description": "Ollama permet executar models localment al vostre ordinador. Per a instruccions sobre com començar, consulteu la Guia d'inici ràpid.", "warning": "Nota: Zoo Code utilitza prompts complexos i funciona millor amb models Claude. Els models menys capaços poden no funcionar com s'espera." }, diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c4a3d4b257..ec7195403d 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Optionaler API-Schlüssel für authentifizierte Ollama-Instanzen oder Cloud-Services. Leer lassen für lokale Installationen.", "numCtx": "Kontextfenstergröße (num_ctx)", "numCtxHelp": "Überschreibt die Standard-Kontextfenstergröße des Modells. Lassen Sie das Feld leer, um die Modelfile-Konfiguration des Modells zu verwenden. Der Mindestwert ist 128.", + "thinking": "Denken aktivieren", + "thinkingHelp": "Aktiviere das native Denken von Ollama für Reasoning-Modelle (z. B. qwen3, deepseek-r1). Wenn aktiviert, wird das Reasoning des Modells separat gestreamt und angezeigt. Nicht unterstützte Modelle ignorieren diese Einstellung.", "description": "Ollama ermöglicht es dir, Modelle lokal auf deinem Computer auszuführen. Eine Anleitung zum Einstieg findest du im Schnellstart-Guide.", "warning": "Hinweis: Zoo Code verwendet komplexe Prompts und funktioniert am besten mit Claude-Modellen. Weniger leistungsfähige Modelle funktionieren möglicherweise nicht wie erwartet." }, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index e5b0bf7cb3..0d9cf7975f 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -621,6 +621,8 @@ "apiKeyHelp": "Optional API key for authenticated Ollama instances or cloud services. Leave empty for local installations.", "numCtx": "Context Window Size (num_ctx)", "numCtxHelp": "Override the model's default context window size. Leave empty to use the model's Modelfile configuration. Minimum value is 128.", + "thinking": "Enable Thinking", + "thinkingHelp": "Enable Ollama's native thinking for reasoning models (e.g. qwen3, deepseek-r1). When enabled, the model's reasoning is streamed and displayed separately. Unsupported models will ignore this setting.", "description": "Ollama allows you to run models locally on your computer. For instructions on how to get started, see their quickstart guide.", "warning": "Note: Zoo Code uses complex prompts and works best with Claude models. Less capable models may not work as expected." }, diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index bbb9e32f45..be915aa0e7 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Clave API opcional para instancias de Ollama autenticadas o servicios en la nube. Deja vacío para instalaciones locales.", "numCtx": "Tamaño de la ventana de contexto (num_ctx)", "numCtxHelp": "Sobrescribe el tamaño de la ventana de contexto predeterminado del modelo. Déjelo vacío para usar la configuración del Modelfile del modelo. El valor mínimo es 128.", + "thinking": "Activar razonamiento", + "thinkingHelp": "Activa el razonamiento nativo de Ollama para modelos de razonamiento (p. ej. qwen3, deepseek-r1). Cuando está activado, el razonamiento del modelo se transmite y se muestra por separado. Los modelos no compatibles ignorarán esta configuración.", "description": "Ollama le permite ejecutar modelos localmente en su computadora. Para obtener instrucciones sobre cómo comenzar, consulte la guía de inicio rápido.", "warning": "Nota: Zoo Code utiliza prompts complejos y funciona mejor con modelos Claude. Los modelos menos capaces pueden no funcionar como se espera." }, diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 844140300c..54a7d01032 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Clé API optionnelle pour les instances Ollama authentifiées ou les services cloud. Laissez vide pour les installations locales.", "numCtx": "Taille de la fenêtre de contexte (num_ctx)", "numCtxHelp": "Remplace la taille de la fenêtre de contexte par défaut du modèle. Laissez vide pour utiliser la configuration du Modelfile du modèle. La valeur minimale est 128.", + "thinking": "Activer le raisonnement", + "thinkingHelp": "Active le raisonnement natif d'Ollama pour les modèles de raisonnement (par ex. qwen3, deepseek-r1). Lorsqu'il est activé, le raisonnement du modèle est diffusé et affiché séparément. Les modèles non compatibles ignoreront ce paramètre.", "description": "Ollama vous permet d'exécuter des modèles localement sur votre ordinateur. Pour obtenir des instructions sur la mise en route, consultez le guide de démarrage rapide.", "warning": "Remarque : Zoo Code utilise des prompts complexes et fonctionne mieux avec les modèles Claude. Les modèles moins performants peuvent ne pas fonctionner comme prévu." }, diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 9d092f5078..7daa0e467c 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "प्रमाणित Ollama इंस्टेंसेस या क्लाउड सेवाओं के लिए वैकल्पिक API key। स्थानीय इंस्टॉलेशन के लिए खाली छोड़ें।", "numCtx": "संदर्भ विंडो आकार (num_ctx)", "numCtxHelp": "मॉडल के डिफ़ॉल्ट संदर्भ विंडो आकार को ओवरराइड करें। मॉडल की मॉडलफ़ाइल कॉन्फ़िगरेशन का उपयोग करने के लिए खाली छोड़ दें। न्यूनतम मान 128 है।", + "thinking": "थिंकिंग सक्षम करें", + "thinkingHelp": "रीजनिंग मॉडल (जैसे qwen3, deepseek-r1) के लिए Ollama की नेटिव थिंकिंग सक्षम करें। सक्षम होने पर, मॉडल का रीजनिंग अलग से स्ट्रीम और प्रदर्शित होता है। असमर्थित मॉडल इस सेटिंग को अनदेखा कर देंगे।", "description": "Ollama आपको अपने कंप्यूटर पर स्थानीय रूप से मॉडल चलाने की अनुमति देता है। आरंभ करने के निर्देशों के लिए, उनकी क्विकस्टार्ट गाइड देखें।", "warning": "नोट: Zoo Code जटिल प्रॉम्प्ट्स का उपयोग करता है और Claude मॉडल के साथ सबसे अच्छा काम करता है। कम क्षमता वाले मॉडल अपेक्षित रूप से काम नहीं कर सकते हैं।" }, diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 03216a2ff2..7c13297d17 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "API key opsional untuk instance Ollama yang terautentikasi atau layanan cloud. Biarkan kosong untuk instalasi lokal.", "numCtx": "Ukuran Jendela Konteks (num_ctx)", "numCtxHelp": "Ganti ukuran jendela konteks default model. Biarkan kosong untuk menggunakan konfigurasi Modelfile model. Nilai minimum adalah 128.", + "thinking": "Aktifkan Thinking", + "thinkingHelp": "Aktifkan thinking native Ollama untuk model reasoning (mis. qwen3, deepseek-r1). Saat diaktifkan, reasoning model dialirkan dan ditampilkan secara terpisah. Model yang tidak didukung akan mengabaikan pengaturan ini.", "description": "Ollama memungkinkan kamu menjalankan model secara lokal di komputer. Untuk instruksi cara memulai, lihat panduan quickstart mereka.", "warning": "Catatan: Zoo Code menggunakan prompt kompleks dan bekerja terbaik dengan model Claude. Model yang kurang mampu mungkin tidak bekerja seperti yang diharapkan." }, diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index cb0bd254a9..16d990fae1 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Chiave API opzionale per istanze Ollama autenticate o servizi cloud. Lascia vuoto per installazioni locali.", "numCtx": "Dimensione della finestra di contesto (num_ctx)", "numCtxHelp": "Sovrascrive la dimensione predefinita della finestra di contesto del modello. Lasciare vuoto per utilizzare la configurazione del Modelfile del modello. Il valore minimo è 128.", + "thinking": "Abilita ragionamento", + "thinkingHelp": "Abilita il ragionamento nativo di Ollama per i modelli di ragionamento (es. qwen3, deepseek-r1). Quando abilitato, il ragionamento del modello viene trasmesso e mostrato separatamente. I modelli non supportati ignoreranno questa impostazione.", "description": "Ollama ti permette di eseguire modelli localmente sul tuo computer. Per iniziare, consulta la guida rapida.", "warning": "Nota: Zoo Code utiliza prompt complessi e funziona meglio con i modelli Claude. I modelli con capacità inferiori potrebbero non funzionare come previsto." }, diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 7aeadb71e3..0f95109ddd 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "認証されたOllamaインスタンスやクラウドサービス用のオプションAPIキー。ローカルインストールの場合は空のままにしてください。", "numCtx": "コンテキストウィンドウサイズ (num_ctx)", "numCtxHelp": "モデルのデフォルトのコンテキストウィンドウサイズを上書きします。モデルのModelfile構成を使用するには、空のままにします。最小値は128です。", + "thinking": "思考を有効化", + "thinkingHelp": "推論モデル(例: qwen3、deepseek-r1)に対してOllamaのネイティブな思考を有効にします。有効にすると、モデルの推論が個別にストリーミングおよび表示されます。サポートされていないモデルはこの設定を無視します。", "description": "Ollamaを使用すると、ローカルコンピューターでモデルを実行できます。始め方については、クイックスタートガイドをご覧ください。", "warning": "注意:Zoo Codeは複雑なプロンプトを使用し、Claudeモデルで最適に動作します。能力の低いモデルは期待通りに動作しない場合があります。" }, diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index cafcfaaa81..eb7b59a4d0 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "인증된 Ollama 인스턴스나 클라우드 서비스용 선택적 API 키. 로컬 설치의 경우 비워두세요.", "numCtx": "컨텍스트 창 크기(num_ctx)", "numCtxHelp": "모델의 기본 컨텍스트 창 크기를 재정의합니다. 모델의 Modelfile 구성을 사용하려면 비워 둡니다. 최소값은 128입니다.", + "thinking": "추론 활성화", + "thinkingHelp": "추론 모델(예: qwen3, deepseek-r1)에 대해 Ollama의 네이티브 추론을 활성화합니다. 활성화하면 모델의 추론이 별도로 스트리밍되고 표시됩니다. 지원하지 않는 모델은 이 설정을 무시합니다.", "description": "Ollama를 사용하면 컴퓨터에서 로컬로 모델을 실행할 수 있습니다. 시작하는 방법은 빠른 시작 가이드를 참조하세요.", "warning": "참고: Zoo Code는 복잡한 프롬프트를 사용하며 Claude 모델에서 가장 잘 작동합니다. 덜 강력한 모델은 예상대로 작동하지 않을 수 있습니다." }, diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index fae892592c..9c7ecf511f 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Optionele API-sleutel voor geauthenticeerde Ollama-instanties of cloudservices. Laat leeg voor lokale installaties.", "numCtx": "Contextvenstergrootte (num_ctx)", "numCtxHelp": "Overschrijft de standaard contextvenstergrootte van het model. Laat leeg om de Modelfile-configuratie van het model te gebruiken. De minimumwaarde is 128.", + "thinking": "Redeneren inschakelen", + "thinkingHelp": "Schakel Ollama's native redeneren in voor redeneringsmodellen (bijv. qwen3, deepseek-r1). Indien ingeschakeld, wordt de redenering van het model apart gestreamd en weergegeven. Niet-ondersteunde modellen negeren deze instelling.", "description": "Ollama laat je modellen lokaal op je computer draaien. Zie hun quickstart-gids voor instructies.", "warning": "Let op: Zoo Code gebruikt complexe prompts en werkt het beste met Claude-modellen. Minder krachtige modellen werken mogelijk niet zoals verwacht." }, diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 27b90e98d5..3eb443a368 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Opcjonalny klucz API dla uwierzytelnionych instancji Ollama lub usług chmurowych. Pozostaw puste dla instalacji lokalnych.", "numCtx": "Rozmiar okna kontekstu (num_ctx)", "numCtxHelp": "Zastępuje domyślny rozmiar okna kontekstu modelu. Pozostaw puste, aby użyć konfiguracji Modelfile modelu. Minimalna wartość to 128.", + "thinking": "Włącz wnioskowanie", + "thinkingHelp": "Włącz natywne wnioskowanie Ollama dla modeli wnioskujących (np. qwen3, deepseek-r1). Po włączeniu wnioskowanie modelu jest przesyłane i wyświetlane oddzielnie. Nieobsługiwane modele zignorują to ustawienie.", "description": "Ollama pozwala na lokalne uruchamianie modeli na twoim komputerze. Aby rozpocząć, zapoznaj się z przewodnikiem szybkiego startu.", "warning": "Uwaga: Zoo Code używa złożonych podpowiedzi i działa najlepiej z modelami Claude. Modele o niższych możliwościach mogą nie działać zgodnie z oczekiwaniami." }, diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 7c3165c3de..4a460a7c14 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Chave API opcional para instâncias Ollama autenticadas ou serviços em nuvem. Deixe vazio para instalações locais.", "numCtx": "Tamanho da janela de contexto (num_ctx)", "numCtxHelp": "Substitui o tamanho da janela de contexto padrão do modelo. Deixe em branco para usar a configuração do Modelfile do modelo. O valor mínimo é 128.", + "thinking": "Ativar raciocínio", + "thinkingHelp": "Ativa o raciocínio nativo do Ollama para modelos de raciocínio (ex.: qwen3, deepseek-r1). Quando ativado, o raciocínio do modelo é transmitido e exibido separadamente. Modelos não compatíveis ignorarão esta configuração.", "description": "O Ollama permite que você execute modelos localmente em seu computador. Para instruções sobre como começar, veja o guia de início rápido deles.", "warning": "Nota: O Zoo Code usa prompts complexos e funciona melhor com modelos Claude. Modelos menos capazes podem não funcionar como esperado." }, diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 6fb07bd8bf..83ae9df63a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Опциональный API-ключ для аутентифицированных экземпляров Ollama или облачных сервисов. Оставьте пустым для локальных установок.", "numCtx": "Размер контекстного окна (num_ctx)", "numCtxHelp": "Переопределяет размер контекстного окна модели по умолчанию. Оставьте пустым, чтобы использовать конфигурацию Modelfile модели. Минимальное значение — 128.", + "thinking": "Включить рассуждение", + "thinkingHelp": "Включает собственное рассуждение Ollama для моделей рассуждения (например, qwen3, deepseek-r1). При включении рассуждение модели передаётся и отображается отдельно. Неподдерживаемые модели проигнорируют эту настройку.", "description": "Ollama позволяет запускать модели локально на вашем компьютере. Для начала ознакомьтесь с кратким руководством.", "warning": "Примечание: Zoo Code использует сложные подсказки и лучше всего работает с моделями Claude. Менее мощные модели могут работать некорректно." }, diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 346ee6b568..08cc505037 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Kimlik doğrulamalı Ollama örnekleri veya bulut hizmetleri için isteğe bağlı API anahtarı. Yerel kurulumlar için boş bırakın.", "numCtx": "Bağlam Penceresi Boyutu (num_ctx)", "numCtxHelp": "Modelin varsayılan bağlam penceresi boyutunu geçersiz kılar. Modelin Modelfile yapılandırmasını kullanmak için boş bırakın. Minimum değer 128'dir.", + "thinking": "Akıl yürütmeyi etkinleştir", + "thinkingHelp": "Akıl yürütme modelleri (örn. qwen3, deepseek-r1) için Ollama'nın yerel akıl yürütmesini etkinleştirir. Etkinleştirildiğinde, modelin akıl yürütmesi ayrı olarak akışla aktarılır ve görüntülenir. Desteklenmeyen modeller bu ayarı yok sayar.", "description": "Ollama, modelleri bilgisayarınızda yerel olarak çalıştırmanıza olanak tanır. Başlamak için hızlı başlangıç kılavuzlarına bakın.", "warning": "Not: Zoo Code karmaşık istemler kullanır ve Claude modelleriyle en iyi şekilde çalışır. Daha az yetenekli modeller beklendiği gibi çalışmayabilir." }, diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 4e42c9bc03..450c385170 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "Khóa API tùy chọn cho các phiên bản Ollama đã xác thực hoặc dịch vụ đám mây. Để trống cho cài đặt cục bộ.", "numCtx": "Kích thước cửa sổ ngữ cảnh (num_ctx)", "numCtxHelp": "Ghi đè kích thước cửa sổ ngữ cảnh mặc định của mô hình. Để trống để sử dụng cấu hình Modelfile của mô hình. Giá trị tối thiểu là 128.", + "thinking": "Bật suy luận", + "thinkingHelp": "Bật suy luận gốc của Ollama cho các mô hình suy luận (ví dụ: qwen3, deepseek-r1). Khi được bật, suy luận của mô hình được truyền và hiển thị riêng biệt. Các mô hình không được hỗ trợ sẽ bỏ qua cài đặt này.", "description": "Ollama cho phép bạn chạy các mô hình cục bộ trên máy tính của bạn. Để biết hướng dẫn về cách bắt đầu, xem hướng dẫn nhanh của họ.", "warning": "Lưu ý: Zoo Code sử dụng các lời nhắc phức tạp và hoạt động tốt nhất với các mô hình Claude. Các mô hình kém mạnh hơn có thể không hoạt động như mong đợi." }, diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 90b683b86b..01d0a59298 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -541,6 +541,8 @@ "apiKeyHelp": "用于已认证 Ollama 实例或云服务的可选 API 密钥。本地安装请留空。", "numCtx": "上下文窗口大小 (num_ctx)", "numCtxHelp": "覆盖模型的默认上下文窗口大小。留空以使用模型的 Modelfile 配置。最小值为 128。", + "thinking": "启用思考", + "thinkingHelp": "为推理模型(例如 qwen3、deepseek-r1)启用 Ollama 的原生思考。启用后,模型的推理将单独流式传输和显示。不支持的模型将忽略此设置。", "description": "Ollama 允许您在本地计算机上运行模型。有关如何开始使用的说明,请参阅其快速入门指南。", "warning": "注意:Zoo Code 使用复杂的提示,与 Claude 模型配合最佳。功能较弱的模型可能无法按预期工作。" }, diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 3f0defb886..58e91789b0 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -568,6 +568,8 @@ "apiKeyHelp": "用於已驗證 Ollama 執行個體或雲端服務的選用 API 金鑰。本機安裝請留空。", "numCtx": "上下文視窗大小(num_ctx)", "numCtxHelp": "覆寫模型的預設上下文視窗大小。留空以使用模型的 Modelfile 設定。最小值為 128。", + "thinking": "啟用思考", + "thinkingHelp": "為推理模型(例如 qwen3、deepseek-r1)啟用 Ollama 的原生思考。啟用後,模型的推理將單獨串流和顯示。不支援的模型將忽略此設定。", "description": "Ollama 允許您在本機電腦執行模型。請參閱快速入門指南。", "warning": "注意:Zoo Code 使用複雜提示詞,與 Claude 模型搭配最佳。功能較弱的模型可能無法正常運作。" },