From afbaab42aba2d460dfbe1aa0b21440fc9b06afb5 Mon Sep 17 00:00:00 2001 From: Liu Yuan Date: Sat, 7 Feb 2026 13:55:33 +0800 Subject: [PATCH] Fix: Honor `/think off` for reasoning-capable models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: When users execute `/think off`, they still receive `reasoning_content` from models configured with `reasoning: true` (e.g., GLM-4.7, GLM-4.6, Kimi K2.5, MiniMax-M2.1). Expected: `/think off` should completely disable reasoning content. Actual: Reasoning content is still returned. Root Cause: The directive handlers delete `sessionEntry.thinkingLevel` when user executes `/think off`. This causes the thinking level to become undefined, and the system falls back to `resolveThinkingDefault()`, which checks the model catalog and returns "low" for reasoning-capable models, ignoring the user's explicit intent. Why We Must Persist "off" (Design Rationale): 1. **Model-dependent defaults**: Unlike other directives where "off" means use a global default, `thinkingLevel` has model-dependent defaults: - Reasoning-capable models (GLM-4.7, etc.) → default "low" - Other models → default "off" 2. **Existing pattern**: The codebase already follows this pattern for `elevatedLevel`, which persists "off" explicitly to override defaults that may be "on". The comment explains: "Persist 'off' explicitly so `/elevated off` actually overrides defaults." 3. **User intent**: When a user explicitly executes `/think off`, they want to disable thinking regardless of the model's capabilities. Deleting the field breaks this intent by falling back to the model's default. Solution: Persist "off" value instead of deleting the field in all internal directive handlers: - `src/auto-reply/reply/directive-handling.impl.ts`: Directive-only messages - `src/auto-reply/reply/directive-handling.persist.ts`: Inline directives - `src/commands/agent.ts`: CLI command-line flags Gateway API Backward Compatibility: The original implementation incorrectly mapped `null` to "off" in `sessions-patch.ts` for consistency with internal handlers. This was a breaking change because: - Previously, `null` cleared the override (deleted the field) - API clients lost the ability to "clear to default" via `null` - This contradicts standard JSON semantics where `null` means "no value" Restored original null semantics in `src/gateway/sessions-patch.ts`: - `null` → delete field, fall back to model default (clear override) - `"off"` → persist explicit override - Other values → normalize and persist This ensures backward compatibility for API clients while fixing the `/think off` issue in internal handlers. Signed-off-by: Liu Yuan --- src/auto-reply/reply/directive-handling.impl.ts | 6 +----- src/auto-reply/reply/directive-handling.persist.ts | 6 +----- src/commands/agent.ts | 6 +----- src/gateway/sessions-patch.ts | 7 ++----- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/auto-reply/reply/directive-handling.impl.ts b/src/auto-reply/reply/directive-handling.impl.ts index 463cb42d67..4b07073272 100644 --- a/src/auto-reply/reply/directive-handling.impl.ts +++ b/src/auto-reply/reply/directive-handling.impl.ts @@ -309,11 +309,7 @@ export async function handleDirectiveOnly(params: { let reasoningChanged = directives.hasReasoningDirective && directives.reasoningLevel !== undefined; if (directives.hasThinkDirective && directives.thinkLevel) { - if (directives.thinkLevel === "off") { - delete sessionEntry.thinkingLevel; - } else { - sessionEntry.thinkingLevel = directives.thinkLevel; - } + sessionEntry.thinkingLevel = directives.thinkLevel; } if (shouldDowngradeXHigh) { sessionEntry.thinkingLevel = "high"; diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 0e700238b3..225cae0814 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -82,11 +82,7 @@ export async function persistInlineDirectives(params: { let updated = false; if (directives.hasThinkDirective && directives.thinkLevel) { - if (directives.thinkLevel === "off") { - delete sessionEntry.thinkingLevel; - } else { - sessionEntry.thinkingLevel = directives.thinkLevel; - } + sessionEntry.thinkingLevel = directives.thinkLevel; updated = true; } if (directives.hasVerboseDirective && directives.verboseLevel) { diff --git a/src/commands/agent.ts b/src/commands/agent.ts index 4c08d75df6..023ca94b46 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -222,11 +222,7 @@ export async function agentCommand( sessionEntry ?? { sessionId, updatedAt: Date.now() }; const next: SessionEntry = { ...entry, sessionId, updatedAt: Date.now() }; if (thinkOverride) { - if (thinkOverride === "off") { - delete next.thinkingLevel; - } else { - next.thinkingLevel = thinkOverride; - } + next.thinkingLevel = thinkOverride; } applyVerboseOverride(next, verboseOverride); sessionStore[sessionKey] = next; diff --git a/src/gateway/sessions-patch.ts b/src/gateway/sessions-patch.ts index ba2d7bbc03..c5240b5d17 100644 --- a/src/gateway/sessions-patch.ts +++ b/src/gateway/sessions-patch.ts @@ -124,6 +124,7 @@ export async function applySessionsPatchToStore(params: { if ("thinkingLevel" in patch) { const raw = patch.thinkingLevel; if (raw === null) { + // Clear the override and fall back to model default delete next.thinkingLevel; } else if (raw !== undefined) { const normalized = normalizeThinkLevel(String(raw)); @@ -134,11 +135,7 @@ export async function applySessionsPatchToStore(params: { `invalid thinkingLevel (use ${formatThinkingLevels(hintProvider, hintModel, "|")})`, ); } - if (normalized === "off") { - delete next.thinkingLevel; - } else { - next.thinkingLevel = normalized; - } + next.thinkingLevel = normalized; } }