fix: prevent DeepSeek 400 error on direct mode switch from reasoning-capable model#994
fix: prevent DeepSeek 400 error on direct mode switch from reasoning-capable model#994carmonium wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughAdds shared reasoning-compatibility predicates and history guards for strict providers, updates DeepSeek, MiMo, and Z.ai request behavior, adds mode-switch safety checks with optional condensation, and renders compatibility warnings in chat. ChangesReasoning compatibility safeguards
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.3)src/core/webview/ClineProvider.tsFile contains syntax errors that prevent linting: Line 1543: unexpected token ... [truncated 28372 characters] ... ted a semicolon or an implicit semicolon after a statement, but found none; Line 4030: Illegal return statement outside of a function; Line 4040: Illegal return statement outside of a function; Line 4043: Illegal return statement outside of a function; Line 4121: Illegal use of reserved keyword 🔧 ESLint
src/core/webview/ClineProvider.tsParsing error: Invalid character. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/core/webview/ClineProvider.ts (1)
1504-1505: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRedundant config lookups.
getModeConfigId(newMode)andlistConfig()are called here and again at lines 1584-1585 for the samenewModeshortly after. Consider hoisting/reusing a single lookup to avoid duplicate storage round-trips on every mode switch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/webview/ClineProvider.ts` around lines 1504 - 1505, In the mode-switch flow of ClineProvider, avoid repeating the providerSettingsManager.getModeConfigId(newMode) and listConfig() lookups performed again near the later mode-handling block. Hoist the results or reuse shared variables so both sections use one lookup per mode switch while preserving the existing configuration-selection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/api/providers/deepseek.ts`:
- Around line 19-20: Remove the duplicate historyHasToolCallsWithoutReasoning
import in the import section of deepseek.ts, retaining a single import from
./utils/reasoning-history-guard so compilation succeeds.
In `@src/api/providers/utils/__tests__/reasoning-history-guard.spec.ts`:
- Around line 53-62: Rename the test case describing the empty tool_calls
scenario to state that it returns false, while preserving the existing messages
setup and historyHasToolCallsWithoutReasoning assertion.
In `@src/core/webview/ClineProvider.ts`:
- Around line 1540-1545: Fix the template literal passed to this.log in the
mode-switch compatibility check catch block by using plain backticks rather than
escaped backticks, matching the valid pattern in the nearby catch block and
restoring valid parsing.
- Around line 1501-1546: Update the compatibility pre-check in the mode-switch
flow around modeSwitchRisksReasoningIncompatibility to honor
lockApiConfigAcrossModes before resolving or comparing the target profile
provider. When the setting prevents the API profile switch, skip both the
warning and optional task.condenseContext() so checks reflect the provider that
will actually remain active; preserve existing behavior when switching is
allowed.
In `@webview-ui/src/components/chat/ChatRow.tsx`:
- Around line 1580-1599: The mode_switch_compatibility_warning branch in ChatRow
lacks local Vitest coverage. Add tests under webview-ui/src/**/__tests__ that
render ChatRow with valid warning payloads and verify the translated WarningRow
title/message, including parsed mode/provider values and fallback behavior for
missing fields; keep the existing rendering logic unchanged.
---
Nitpick comments:
In `@src/core/webview/ClineProvider.ts`:
- Around line 1504-1505: In the mode-switch flow of ClineProvider, avoid
repeating the providerSettingsManager.getModeConfigId(newMode) and listConfig()
lookups performed again near the later mode-handling block. Hoist the results or
reuse shared variables so both sections use one lookup per mode switch while
preserving the existing configuration-selection behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ceb7260c-bf05-4f55-b32b-7f780cbbda82
📒 Files selected for processing (12)
packages/types/src/message.tssrc/api/providers/deepseek.tssrc/api/providers/mimo.tssrc/api/providers/utils/__tests__/reasoning-history-guard.spec.tssrc/api/providers/utils/reasoning-history-guard.tssrc/api/providers/zai.tssrc/core/tools/SwitchModeTool.tssrc/core/webview/ClineProvider.tssrc/shared/__tests__/reasoning-mode-compatibility.spec.tssrc/shared/reasoning-mode-compatibility.tswebview-ui/src/components/chat/ChatRow.tsxwebview-ui/src/i18n/locales/en/chat.json
| import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard" | ||
| import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Duplicate import breaks the build.
historyHasToolCallsWithoutReasoning is imported twice from the same module. This is a duplicate identifier declaration that will fail TypeScript/esbuild compilation — matching the reported pipeline failure (esbuild.mjs --production exiting with status 1).
🐛 Proposed fix
import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"
-import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard" | |
| import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard" | |
| import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/api/providers/deepseek.ts` around lines 19 - 20, Remove the duplicate
historyHasToolCallsWithoutReasoning import in the import section of deepseek.ts,
retaining a single import from ./utils/reasoning-history-guard so compilation
succeeds.
Source: Pipeline failures
| it("returns true when assistant messages have empty tool_calls array", () => { | ||
| const messages = [ | ||
| { | ||
| role: "assistant", | ||
| content: "hello", | ||
| tool_calls: [], | ||
| }, | ||
| ] | ||
| expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false) | ||
| }) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Test title contradicts its assertion.
The test is titled "returns true..." but asserts .toBe(false) — the assertion is correct (empty tool_calls shouldn't trigger the guard), but the description is inverted and will confuse future readers.
✏️ Proposed fix
- it("returns true when assistant messages have empty tool_calls array", () => {
+ it("returns false when assistant messages have empty tool_calls array", () => {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("returns true when assistant messages have empty tool_calls array", () => { | |
| const messages = [ | |
| { | |
| role: "assistant", | |
| content: "hello", | |
| tool_calls: [], | |
| }, | |
| ] | |
| expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false) | |
| }) | |
| it("returns false when assistant messages have empty tool_calls array", () => { | |
| const messages = [ | |
| { | |
| role: "assistant", | |
| content: "hello", | |
| tool_calls: [], | |
| }, | |
| ] | |
| expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false) | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/api/providers/utils/__tests__/reasoning-history-guard.spec.ts` around
lines 53 - 62, Rename the test case describing the empty tool_calls scenario to
state that it returns false, while preserving the existing messages setup and
historyHasToolCallsWithoutReasoning assertion.
| if (via !== "new_task_delegation") { | ||
| try { | ||
| const fromProviderName = task.apiConfiguration?.apiProvider | ||
| const toConfigId = await this.providerSettingsManager.getModeConfigId(newMode) | ||
| const listApiConfig = await this.providerSettingsManager.listConfig() | ||
| const toProfile = toConfigId | ||
| ? listApiConfig.find((c) => c.id === toConfigId) | ||
| : undefined | ||
| const toProviderName = toProfile | ||
| ? (await this.providerSettingsManager.getProfile({ name: toProfile.name })).apiProvider | ||
| : fromProviderName | ||
|
|
||
| if ( | ||
| toProviderName && | ||
| modeSwitchRisksReasoningIncompatibility(fromProviderName, toProviderName) | ||
| ) { | ||
| // Behavior A — always show visible warning in chat | ||
| await task.say( | ||
| "mode_switch_compatibility_warning", | ||
| JSON.stringify({ | ||
| fromMode, | ||
| toMode: newMode, | ||
| fromProvider: fromProviderName, | ||
| toProvider: toProviderName, | ||
| via, | ||
| }), | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| undefined, | ||
| { isNonInteractive: true }, | ||
| ) | ||
|
|
||
| // Behavior B — optional auto-condense when setting is enabled | ||
| const autoCondense = this.context.workspaceState.get("autoCondenseOnRiskyModeSwitch", false) | ||
| if (autoCondense) { | ||
| await task.condenseContext() | ||
| } | ||
| } | ||
| } catch (innerError) { | ||
| // Non-fatal: log but don't block the mode switch if the check fails. | ||
| this.log( | ||
| \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`, | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Compatibility pre-check ignores lockApiConfigAcrossModes.
The guard resolves toProviderName from the target mode's saved profile and can fire the warning (and optionally auto-condense context) even when the provider will never actually change, because lockApiConfigAcrossModes (checked later, at line 1577) prevents the profile switch from being applied at all. This produces spurious warnings and can trigger an unnecessary context condensation for users who lock their API config across modes.
🛠️ Proposed fix
if (via !== "new_task_delegation") {
try {
+ const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false)
const fromProviderName = task.apiConfiguration?.apiProvider
- const toConfigId = await this.providerSettingsManager.getModeConfigId(newMode)
- const listApiConfig = await this.providerSettingsManager.listConfig()
- const toProfile = toConfigId
- ? listApiConfig.find((c) => c.id === toConfigId)
- : undefined
- const toProviderName = toProfile
- ? (await this.providerSettingsManager.getProfile({ name: toProfile.name })).apiProvider
- : fromProviderName
+ const toConfigId = lockApiConfigAcrossModes
+ ? undefined
+ : await this.providerSettingsManager.getModeConfigId(newMode)
+ const listApiConfig = await this.providerSettingsManager.listConfig()
+ const toProfile = toConfigId
+ ? listApiConfig.find((c) => c.id === toConfigId)
+ : undefined
+ const toProviderName = toProfile
+ ? (await this.providerSettingsManager.getProfile({ name: toProfile.name })).apiProvider
+ : fromProviderName📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (via !== "new_task_delegation") { | |
| try { | |
| const fromProviderName = task.apiConfiguration?.apiProvider | |
| const toConfigId = await this.providerSettingsManager.getModeConfigId(newMode) | |
| const listApiConfig = await this.providerSettingsManager.listConfig() | |
| const toProfile = toConfigId | |
| ? listApiConfig.find((c) => c.id === toConfigId) | |
| : undefined | |
| const toProviderName = toProfile | |
| ? (await this.providerSettingsManager.getProfile({ name: toProfile.name })).apiProvider | |
| : fromProviderName | |
| if ( | |
| toProviderName && | |
| modeSwitchRisksReasoningIncompatibility(fromProviderName, toProviderName) | |
| ) { | |
| // Behavior A — always show visible warning in chat | |
| await task.say( | |
| "mode_switch_compatibility_warning", | |
| JSON.stringify({ | |
| fromMode, | |
| toMode: newMode, | |
| fromProvider: fromProviderName, | |
| toProvider: toProviderName, | |
| via, | |
| }), | |
| undefined, | |
| undefined, | |
| undefined, | |
| undefined, | |
| { isNonInteractive: true }, | |
| ) | |
| // Behavior B — optional auto-condense when setting is enabled | |
| const autoCondense = this.context.workspaceState.get("autoCondenseOnRiskyModeSwitch", false) | |
| if (autoCondense) { | |
| await task.condenseContext() | |
| } | |
| } | |
| } catch (innerError) { | |
| // Non-fatal: log but don't block the mode switch if the check fails. | |
| this.log( | |
| \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`, | |
| ) | |
| } | |
| } | |
| if (via !== "new_task_delegation") { | |
| try { | |
| const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false) | |
| const fromProviderName = task.apiConfiguration?.apiProvider | |
| const toConfigId = lockApiConfigAcrossModes | |
| ? undefined | |
| : await this.providerSettingsManager.getModeConfigId(newMode) | |
| const listApiConfig = await this.providerSettingsManager.listConfig() | |
| const toProfile = toConfigId | |
| ? listApiConfig.find((c) => c.id === toConfigId) | |
| : undefined | |
| const toProviderName = toProfile | |
| ? (await this.providerSettingsManager.getProfile({ name: toProfile.name })).apiProvider | |
| : fromProviderName | |
| if ( | |
| toProviderName && | |
| modeSwitchRisksReasoningIncompatibility(fromProviderName, toProviderName) | |
| ) { | |
| // Behavior A — always show visible warning in chat | |
| await task.say( | |
| "mode_switch_compatibility_warning", | |
| JSON.stringify({ | |
| fromMode, | |
| toMode: newMode, | |
| fromProvider: fromProviderName, | |
| toProvider: toProviderName, | |
| via, | |
| }), | |
| undefined, | |
| undefined, | |
| undefined, | |
| undefined, | |
| { isNonInteractive: true }, | |
| ) | |
| // Behavior B — optional auto-condense when setting is enabled | |
| const autoCondense = this.context.workspaceState.get("autoCondenseOnRiskyModeSwitch", false) | |
| if (autoCondense) { | |
| await task.condenseContext() | |
| } | |
| } | |
| } catch (innerError) { | |
| // Non-fatal: log but don't block the mode switch if the check fails. | |
| this.log( | |
| \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`, | |
| ) | |
| } | |
| } |
🧰 Tools
🪛 Biome (2.5.3)
[error] 1543-1543: unexpected token \
(parse)
🪛 GitHub Actions: E2E Tests (Mocked) / 0_e2e-mock.txt
[error] 1543-1543: esbuild syntax error: Syntax error "". Build failed during pnpm run bundle` (node esbuild.mjs).
🪛 GitHub Actions: E2E Tests (Mocked) / e2e-mock
[error] 1543-1543: esbuild failed with Syntax error "`" at core/webview/ClineProvider.ts:1543:7.
🪛 GitHub Actions: Release Validation / 0_validate-release.txt
[error] 1543-1543: esbuild syntax error: Syntax error "`". Build failed with 1 error.
🪛 GitHub Actions: Release Validation / validate-release
[error] 1543-1543: esbuild failed while bundling with a syntax error: "Syntax error `" (ERROR at core/webview/ClineProvider.ts:1543:7). Command: "node esbuild.mjs --production"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/webview/ClineProvider.ts` around lines 1501 - 1546, Update the
compatibility pre-check in the mode-switch flow around
modeSwitchRisksReasoningIncompatibility to honor lockApiConfigAcrossModes before
resolving or comparing the target profile provider. When the setting prevents
the API profile switch, skip both the warning and optional
task.condenseContext() so checks reflect the provider that will actually remain
active; preserve existing behavior when switching is allowed.
| } catch (innerError) { | ||
| // Non-fatal: log but don't block the mode switch if the check fails. | ||
| this.log( | ||
| \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Escaped backticks produce a syntax error.
The this.log template literal is wrapped in escaped backticks (\`) instead of plain backticks, unlike the identical pattern two lines below at 1562-1564. This matches the Biome parse error reported at line 1543 and is very likely the (or one of the) cause(s) of the "esbuild --production" bundle failure in the pipeline.
🐛 Proposed fix
} catch (innerError) {
// Non-fatal: log but don't block the mode switch if the check fails.
this.log(
- \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`,
+ `Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`,
)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (innerError) { | |
| // Non-fatal: log but don't block the mode switch if the check fails. | |
| this.log( | |
| \`Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}\`, | |
| ) | |
| } | |
| } catch (innerError) { | |
| // Non-fatal: log but don't block the mode switch if the check fails. | |
| this.log( | |
| `Mode-switch compatibility check failed: ${innerError instanceof Error ? innerError.message : String(innerError)}`, | |
| ) | |
| } |
🧰 Tools
🪛 Biome (2.5.3)
[error] 1543-1543: unexpected token \
(parse)
🪛 GitHub Actions: E2E Tests (Mocked) / 0_e2e-mock.txt
[error] 1543-1543: esbuild syntax error: Syntax error "". Build failed during pnpm run bundle` (node esbuild.mjs).
🪛 GitHub Actions: E2E Tests (Mocked) / e2e-mock
[error] 1543-1543: esbuild failed with Syntax error "`" at core/webview/ClineProvider.ts:1543:7.
🪛 GitHub Actions: Release Validation / 0_validate-release.txt
[error] 1543-1543: esbuild syntax error: Syntax error "`". Build failed with 1 error.
🪛 GitHub Actions: Release Validation / validate-release
[error] 1543-1543: esbuild failed while bundling with a syntax error: "Syntax error `" (ERROR at core/webview/ClineProvider.ts:1543:7). Command: "node esbuild.mjs --production"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/core/webview/ClineProvider.ts` around lines 1540 - 1545, Fix the template
literal passed to this.log in the mode-switch compatibility check catch block by
using plain backticks rather than escaped backticks, matching the valid pattern
in the nearby catch block and restoring valid parsing.
Sources: Linters/SAST tools, Pipeline failures
| case "mode_switch_compatibility_warning": { | ||
| const warningData = safeJsonParse<{ | ||
| fromMode?: string | ||
| toMode?: string | ||
| fromProvider?: string | ||
| toProvider?: string | ||
| via?: string | ||
| }>(message.text || "{}") | ||
| if (!warningData) return null | ||
| return ( | ||
| <WarningRow | ||
| title={t("chat:modeSwitchCompatibilityWarning.title")} | ||
| message={t("chat:modeSwitchCompatibilityWarning.message", { | ||
| fromMode: warningData.fromMode ?? "?", | ||
| toMode: warningData.toMode ?? "?", | ||
| toProvider: warningData.toProvider ?? "?", | ||
| })} | ||
| /> | ||
| ) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Rendering logic is correct; add local test coverage.
The parsing/rendering itself matches the established pattern and WarningRowProps contract. No Vitest coverage for this new mode_switch_compatibility_warning branch is included in this batch.
As per coding guidelines, "Prefer local webview-ui tests for React/webview behavior such as component rendering... Add or update Vitest coverage under webview-ui/src/**/__tests__ instead of reaching for apps/vscode-e2e."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@webview-ui/src/components/chat/ChatRow.tsx` around lines 1580 - 1599, The
mode_switch_compatibility_warning branch in ChatRow lacks local Vitest coverage.
Add tests under webview-ui/src/**/__tests__ that render ChatRow with valid
warning payloads and verify the translated WarningRow title/message, including
parsed mode/provider values and fallback behavior for missing fields; keep the
existing rendering logic unchanged.
Source: Coding guidelines
Problem
When switching directly (without delegating via
new_task) from a mode using areasoning-capable model (e.g., Claude Sonnet in orchestrator mode) to a mode
using a provider with strict structural expectations (e.g., DeepSeek in code
mode), the API request fails with a deterministic 400 error:
The root cause is a three-step chain:
Mode-switch bypass preserves history.
SwitchModeTool.execute()calls
ClineProvider.handleModeSwitch(), which mutates_taskModeon thesame
Taskinstance — no new task is created, noparentTaskIdis set,and the entire conversation history built under the previous model is carried
over unchanged.
DeepSeek (and similar providers) require
reasoning_content. Whenthinking mode is enabled, DeepSeek's API requires that every assistant
message with
tool_callsin the conversation history also include areasoning_contentfield. The same requirement applies to Z.ai/GLM and MiMoproviders (documented in their respective provider files).
Inherited history never had
reasoning_content. Messages generated byproviders that don't use DeepSeek-style reasoning (e.g., Claude) never
produce this field. Even if they did,
buildCleanConversationHistory()inTask.tsstrips it in the default message path.The result is an unrecoverable error loop — the 400 is deterministic (not a
transient network failure), so automatic retries also fail.
Solution — Two-Layer Defense-in-Depth
Layer 1: Provider Guard (Unconditional Safety Net)
A new shared utility function
historyHasToolCallsWithoutReasoning()detectswhether converted message history contains assistant messages with
tool_callsbut no non-empty
reasoning_content. It is integrated into three providerswith strict
reasoning_contentrequirements:deepseek.ts: AfterconvertToR1Format(), computeseffectiveThinkingEnabled = isThinkingModel && !hasIncompatibleHistoryanduses it for all downstream thinking/effort/temperature decisions. Emits a
provider_reasoning_guard_triggeredstructured log when triggered.zai.ts: AfterconvertToZAiFormat(), forcesuseReasoning = falsewhen incompatible history is detected, disabling thinking and clearing
reasoning_effort.mimo.ts: AfterconvertToR1Format(), switchesextra_body.thinkingfrom
{ type: "enabled" }to{ type: "disabled" }when incompatiblehistory is detected — the first disable path MiMo has ever had.
Layer 2: Orchestration Guard (UI Warning + Optional Auto-Condense)
A new utility
modeSwitchRisksReasoningIncompatibility()checks whether amode switch from one provider to another risks carrying over incompatible
history (target is a strict provider, source is not).
Integrated into
ClineProvider.handleModeSwitch():Behavior A (always-on, default): When
via !== "new_task_delegation"and the check is positive, emits a visible, non-interactive chat warning
(
mode_switch_compatibility_warning) informing the user that theconversation history may be incompatible with the new provider.
Behavior B (opt-in): When
autoCondenseOnRiskyModeSwitchworkspacesetting is
true, additionally auto-condenses the conversation viatask.condenseContext()before the mode mutation, replacing structuredtool_callshistory with a plain-text summary.Files Changed
New files:
src/api/providers/utils/reasoning-history-guard.ts— Layer 1 guard functionsrc/api/providers/utils/__tests__/reasoning-history-guard.spec.ts— 9 testssrc/shared/reasoning-mode-compatibility.ts— Layer 2 utility functionssrc/shared/__tests__/reasoning-mode-compatibility.spec.ts— 14 testsModified files:
src/api/providers/deepseek.ts— Layer 1 guard integrationsrc/api/providers/zai.ts— Layer 1 guard integrationsrc/api/providers/mimo.ts— Layer 1 guard integration (first disable path)src/core/webview/ClineProvider.ts— Layer 2 guard inhandleModeSwitch()src/core/tools/SwitchModeTool.ts— passvia: "switch_mode"to handlerpackages/types/src/message.ts— newclineSaysvaluewebview-ui/src/components/chat/ChatRow.tsx— render new warning via WarningRowwebview-ui/src/i18n/locales/en/chat.json— i18n strings for warningTesting
Unit Tests
reasoning-history-guard.spec.ts: 9 tests covering empty messages, tool_callswith/without reasoning_content, empty reasoning_content, empty tool_calls,
non-assistant tool_calls, multi-message detection, non-array tool_calls.
reasoning-mode-compatibility.spec.ts: 14 tests covering strict provideridentification, same-provider switches, non-strict→strict, strict→non-strict,
strict↔strict, undefined (unknown) provider fail-safe behavior.
Manual Verification
normal thinking-mode requests are unaffected).
Code/DeepSeek) was traced through the new code path; the original 400 error
is now structurally prevented by Layer 1 independent of Layer 2.
Caveats
documented
reasoning_contentstrictness). Other providers withpreserveReasoning: true(Moonshot, MiniMax, Fireworks, Bedrock,opencode-go) share the same structural risk but are not yet covered — they
are tracked as a follow-up.
synthesizing fake
reasoning_content, which is a deliberate trade-off:deterministic safety over risk of undefined provider-side behavior.
falsebecausecondenseContext()has real latency/token cost and a fidelity trade-offthat should be opt-in.
Summary by CodeRabbit
New Features
Bug Fixes
Tests