Fix: Honor /think off for reasoning-capable models

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 <namei.unix@gmail.com>
This commit is contained in:
Liu Yuan
2026-02-07 13:55:33 +08:00
committed by George Pickett
parent fa21050af0
commit 97b3ee7ec0
4 changed files with 5 additions and 20 deletions

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;
}
}