diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 54f1f24e3..598019779 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed `Agent` runs so explicit reasoning disablement is forwarded to provider stream options. + ## [15.10.11] - 2026-06-10 ### Changed diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 5f48c7832..3845c7a9c 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -265,6 +265,7 @@ export class Agent { systemPrompt: [], model: getBundledModel("google", "gemini-2.5-flash-lite-preview-06-17"), thinkingLevel: undefined, + disableReasoning: false, tools: [], messages: [], isStreaming: false, @@ -658,6 +659,10 @@ export class Agent { this.#state.thinkingLevel = l; } + setDisableReasoning(disabled: boolean) { + this.#state.disableReasoning = disabled; + } + setSteeringMode(mode: "all" | "one-at-a-time") { this.#steeringMode = mode; } @@ -942,6 +947,7 @@ export class Agent { const config: AgentLoopConfig = { model, reasoning, + disableReasoning: this.#state.disableReasoning, temperature: this.#temperature, topP: this.#topP, topK: this.#topK, diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index 5777a9b82..de1ca0788 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -358,6 +358,7 @@ export interface AgentState { systemPrompt: string[]; model: Model; thinkingLevel?: Effort; + disableReasoning?: boolean; tools: AgentTool[]; messages: AgentMessage[]; // Can include attachments + custom message types isStreaming: boolean; diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index db95e43df..6f227b0ec 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -354,6 +354,22 @@ describe("Agent", () => { expect(reasoningPerCall).toEqual([ThinkingLevel.Low, ThinkingLevel.High]); }); + it("forwards explicit reasoning disablement to the stream", async () => { + const mock = createMockModel({ responses: [{ content: ["ok"] }] }); + const agent = new Agent({ + initialState: { + model: mock.model, + messages: [], + disableReasoning: true, + }, + streamFn: mock.stream, + }); + + await agent.prompt("run"); + + expect(mock.calls[0]?.options?.disableReasoning).toBe(true); + }); + it("forwards distinct provider session id and prompt cache key to the stream", async () => { const mock = createMockModel({ responses: [{ content: ["ok"] }] }); const agent = new Agent({ diff --git a/packages/ai/test/ollama-thinking-disable.test.ts b/packages/ai/test/ollama-thinking-disable.test.ts new file mode 100644 index 000000000..8ceccdb73 --- /dev/null +++ b/packages/ai/test/ollama-thinking-disable.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "bun:test"; +import type { Context } from "@oh-my-pi/pi-ai"; +import { streamOllama } from "@oh-my-pi/pi-ai/providers/ollama"; +import { buildModel } from "@oh-my-pi/pi-catalog/build"; + +function createReasoningOllamaModel() { + return buildModel({ + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + api: "ollama-chat", + provider: "ollama-cloud", + baseUrl: "https://ollama.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 8192, + }); +} + +describe("Ollama chat thinking controls", () => { + it("sends think false when reasoning is explicitly disabled", async () => { + let payload: object | undefined; + const fetchMock = async (_input: string | URL | Request, init?: RequestInit): Promise => { + const parsed: unknown = JSON.parse(String(init?.body)); + if (parsed === null || typeof parsed !== "object") { + throw new Error("Expected Ollama payload object"); + } + payload = parsed; + return new Response('{"message":{"content":"391"},"done":true,"prompt_eval_count":1,"eval_count":1}\n', { + status: 200, + }); + }; + const context: Context = { + messages: [{ role: "user", content: "What is 17*23?", timestamp: 0 }], + }; + + await streamOllama(createReasoningOllamaModel(), context, { + apiKey: "test-key", + disableReasoning: true, + fetch: fetchMock, + }).result(); + + expect(payload ? Reflect.get(payload, "think") : undefined).toBe(false); + }); +}); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3d140791e..07f3c0fa7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -21,6 +21,7 @@ ### Fixed +- Fixed Ollama chat turns using the `:off` thinking selector so requests explicitly send reasoning disablement instead of falling back to the provider default ([#2239](https://github.com/can1357/oh-my-pi/issues/2239)). - Fixed long-running sessions becoming sluggish because the status line recomputed context usage by walking the full message history on every refresh. Message-token totals are now cached incrementally, and status lines that do not render context segments skip context accounting entirely. ([#2089](https://github.com/can1357/oh-my-pi/issues/2089)) - Fixed exiting plan mode without confirmation only when neither the default plan file nor slug-named local plan files contain draft content ([#2024](https://github.com/can1357/oh-my-pi/issues/2024)). - Fixed `enabledModels` being ignored by the ACP model picker (Zed and other ACP clients) — `AgentSession.getAvailableModels()` now applies the configured allow-list, so only the models listed in `enabledModels` appear in the UI. Also applies consistently to the RPC `get_available_models` endpoint and the `/model` slash command. diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index ea358a56f..5f5ef9cb5 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -135,6 +135,7 @@ import { parseThinkingLevel, resolveProvisionalAutoLevel, resolveThinkingLevelForModel, + shouldDisableReasoning, toReasoningEffort, } from "./thinking"; import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode"; @@ -2176,6 +2177,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} systemPrompt, model, thinkingLevel: toReasoningEffort(effectiveThinkingLevel), + disableReasoning: shouldDisableReasoning(effectiveThinkingLevel), tools: initialTools, }, convertToLlm: convertToLlmFinal, diff --git a/packages/coding-agent/src/session/agent-session.ts b/packages/coding-agent/src/session/agent-session.ts index 5b1e50eb7..92cfa9d3c 100644 --- a/packages/coding-agent/src/session/agent-session.ts +++ b/packages/coding-agent/src/session/agent-session.ts @@ -198,6 +198,7 @@ import { clampAutoThinkingEffort, resolveProvisionalAutoLevel, resolveThinkingLevelForModel, + shouldDisableReasoning, toReasoningEffort, } from "../thinking"; import { shutdownTinyTitleClient } from "../tiny/title-client"; @@ -1139,6 +1140,7 @@ export class AgentSession { } else { this.#thinkingLevel = config.thinkingLevel; } + this.#applyThinkingLevelToAgent(this.#thinkingLevel); this.#promptTemplates = config.promptTemplates ?? []; this.#slashCommands = config.slashCommands ?? []; this.#extensionRunner = config.extensionRunner; @@ -5770,6 +5772,11 @@ export class AgentSession { // Thinking Level Management // ========================================================================= + #applyThinkingLevelToAgent(level: ThinkingLevel | undefined): void { + this.agent.setThinkingLevel(toReasoningEffort(level)); + this.agent.setDisableReasoning(shouldDisableReasoning(level)); + } + /** * Set the thinking level. `auto` enables per-turn classification; the selector * itself is never written to the session log, but resolved concrete levels are @@ -5783,7 +5790,7 @@ export class AgentSession { this.#autoThinking = true; this.#autoResolvedLevel = undefined; this.#thinkingLevel = provisional; - this.agent.setThinkingLevel(toReasoningEffort(provisional)); + this.#applyThinkingLevelToAgent(provisional); if (persist) { this.settings.set("defaultThinkingLevel", AUTO_THINKING); } @@ -5799,7 +5806,7 @@ export class AgentSession { const isChanging = effectiveLevel !== this.#thinkingLevel; this.#thinkingLevel = effectiveLevel; - this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel)); + this.#applyThinkingLevelToAgent(effectiveLevel); if (isChanging) { this.sessionManager.appendThinkingLevelChange(effectiveLevel); @@ -5889,7 +5896,7 @@ export class AgentSession { const shouldPersistResolution = this.#autoResolvedLevel !== effort; this.#autoResolvedLevel = effort; this.#thinkingLevel = effort; - this.agent.setThinkingLevel(toReasoningEffort(effort)); + this.#applyThinkingLevelToAgent(effort); if (shouldPersistResolution) { this.sessionManager.appendThinkingLevelChange(effort); } @@ -9065,6 +9072,7 @@ export class AgentSession { promptCacheKey: cacheSessionId, preferWebsockets: false, reasoning: toReasoningEffort(this.thinkingLevel), + disableReasoning: shouldDisableReasoning(this.thinkingLevel), hideThinkingSummary: this.agent.hideThinkingSummary, serviceTier: this.serviceTier, signal: args.signal, @@ -9353,7 +9361,7 @@ export class AgentSession { this.#autoResolvedLevel = undefined; this.#thinkingLevel = resolveThinkingLevelForModel(this.model, restoredThinkingLevel); } - this.agent.setThinkingLevel(toReasoningEffort(this.#thinkingLevel)); + this.#applyThinkingLevelToAgent(this.#thinkingLevel); this.agent.serviceTier = hasServiceTierEntry ? sessionContext.serviceTier : configuredServiceTier === "none" @@ -9410,7 +9418,7 @@ export class AgentSession { this.#thinkingLevel = previousThinkingLevel; this.#autoThinking = previousAutoThinking; this.#autoResolvedLevel = previousAutoResolvedLevel; - this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel)); + this.#applyThinkingLevelToAgent(previousThinkingLevel); this.agent.serviceTier = previousServiceTier; this.#syncTodoPhasesFromBranch(); this.#reconnectToAgent(); diff --git a/packages/coding-agent/src/thinking.ts b/packages/coding-agent/src/thinking.ts index 8c470aa7f..f7361dd63 100644 --- a/packages/coding-agent/src/thinking.ts +++ b/packages/coding-agent/src/thinking.ts @@ -71,6 +71,13 @@ export function toReasoningEffort(level: ThinkingLevel | undefined): Effort | un return level; } +/** + * True when a selector explicitly requests provider-side reasoning disablement. + */ +export function shouldDisableReasoning(level: ThinkingLevel | undefined): boolean { + return level === ThinkingLevel.Off; +} + /** * Resolves a selector against the current model while preserving explicit "off". */ diff --git a/packages/coding-agent/test/agent-session-role-thinking.test.ts b/packages/coding-agent/test/agent-session-role-thinking.test.ts index 80e33f065..569a7bdaa 100644 --- a/packages/coding-agent/test/agent-session-role-thinking.test.ts +++ b/packages/coding-agent/test/agent-session-role-thinking.test.ts @@ -239,9 +239,11 @@ describe("AgentSession role model thinking behavior", () => { expect(session.cycleThinkingLevel()).toBe("off"); expect(session.thinkingLevel).toBe("off"); + expect(agent.state.disableReasoning).toBe(true); expect(session.cycleThinkingLevel()).toBe(AUTO_THINKING); expect(session.configuredThinkingLevel()).toBe(AUTO_THINKING); expect(session.thinkingLevel).toBe(resolveProvisionalAutoLevel(model)); + expect(agent.state.disableReasoning).toBe(false); expect(session.cycleThinkingLevel()).toBe(Effort.Minimal); expect(session.thinkingLevel).toBe(Effort.Minimal); });