Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ export const clineSays = [
"codebase_search_result",
"user_edit_todos",
"too_many_tools_warning",
"mode_switch_compatibility_warning",
"tool",
] as const

Expand Down
33 changes: 26 additions & 7 deletions src/api/providers/deepseek.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"

import { logger } from "../../utils/logging"

import {
deepSeekModels,
deepSeekDefaultModelId,
Expand All @@ -14,6 +16,8 @@ import type { ApiHandlerOptions } from "../../shared/api"
import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
import { getModelParams } from "../transform/model-params"
import { convertToR1Format } from "../transform/r1-format"
import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"
import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"
Comment on lines +19 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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


import { OpenAiHandler } from "./openai"
import { extractReasoningFromDelta } from "./utils/extract-reasoning"
Expand Down Expand Up @@ -96,12 +100,6 @@ export class DeepSeekHandler extends OpenAiHandler {
const { info: modelInfo, temperature, reasoningEffort, maxTokens } = this.getModel()

const isThinkingModel = isDeepSeekThinkingEnabled(modelId, this.options)
const thinking = supportsDeepSeekThinkingToggle(modelId)
? ({ type: isThinkingModel ? "enabled" : "disabled" } as const)
: isThinkingModel
? ({ type: "enabled" } as const)
: undefined
const deepSeekReasoningEffort = isThinkingModel ? normalizeDeepSeekReasoningEffort(reasoningEffort) : undefined

// Convert messages to R1 format (merges consecutive same-role messages)
// This is required for DeepSeek which does not support successive messages with the same role
Expand All @@ -113,9 +111,30 @@ export class DeepSeekHandler extends OpenAiHandler {
mergeToolResultText: isThinkingModel,
})

// Layer 1 Guard: detect incompatible history (tool_calls without reasoning_content)
// and disable thinking mode for this request to prevent a 400 error from the API.
const hasIncompatibleHistory = historyHasToolCallsWithoutReasoning(convertedMessages)
const effectiveThinkingEnabled = isThinkingModel && !hasIncompatibleHistory

if (hasIncompatibleHistory) {
logger.warn("provider_reasoning_guard_triggered", {
ctx: "deepseek",
provider: "deepseek",
modelId,
taskId: metadata?.taskId,
})
}

const thinking = supportsDeepSeekThinkingToggle(modelId)
? ({ type: effectiveThinkingEnabled ? "enabled" : "disabled" } as const)
: effectiveThinkingEnabled
? ({ type: "enabled" } as const)
: undefined
const deepSeekReasoningEffort = effectiveThinkingEnabled ? normalizeDeepSeekReasoningEffort(reasoningEffort) : undefined

const requestOptions: DeepSeekChatCompletionParams = {
model: modelId,
...(!isThinkingModel && { temperature: temperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE }),
...(!effectiveThinkingEnabled && { temperature: temperature ?? DEEP_SEEK_DEFAULT_TEMPERATURE }),
messages: convertedMessages,
stream: true as const,
stream_options: { include_usage: true },
Expand Down
18 changes: 17 additions & 1 deletion src/api/providers/mimo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { extractReasoningFromDelta } from "./utils/extract-reasoning"
import { OpenAiHandler } from "./openai"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { sanitizeOpenAiCallId } from "../../utils/tool-id"
import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"
import { logger } from "../../utils/logging"

/**
* MiMoHandler extends OpenAiHandler with MiMo-specific adaptations.
Expand Down Expand Up @@ -81,6 +83,20 @@ export class MimoHandler extends OpenAiHandler {

const tools = metadata?.tools

// Layer 1 Guard: detect incompatible history (tool_calls without reasoning_content)
// and disable thinking mode for this request to prevent a 400 error from the API.
// MiMo previously had NO disable path — this closes that gap.
const hasIncompatibleHistory = historyHasToolCallsWithoutReasoning(convertedMessages)

if (hasIncompatibleHistory) {
logger.warn("provider_reasoning_guard_triggered", {
ctx: "mimo",
provider: "mimo",
modelId,
taskId: metadata?.taskId,
})
}

// Build request per MiMo's OpenAI-compatible API
// https://developer.puter.com/ai/xiaomi/mimo-v2.5-pro/
// Note: temperature is omitted because MiMo forces it to 1.0 when thinking mode
Expand All @@ -91,7 +107,7 @@ export class MimoHandler extends OpenAiHandler {
stream: true,
stream_options: { include_usage: true },
// MiMo requires thinking to be enabled via extra_body
extra_body: { thinking: { type: "enabled" } },
extra_body: { thinking: { type: hasIncompatibleHistory ? "disabled" : "enabled" } },
}

if (tools && tools.length > 0) {
Expand Down
107 changes: 107 additions & 0 deletions src/api/providers/utils/__tests__/reasoning-history-guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// npx vitest run api/providers/utils/__tests__/reasoning-history-guard.spec.ts

import { historyHasToolCallsWithoutReasoning } from "../reasoning-history-guard"

describe("historyHasToolCallsWithoutReasoning", () => {
it("returns false for empty messages", () => {
expect(historyHasToolCallsWithoutReasoning([])).toBe(false)
})

it("returns false when no assistant messages have tool_calls", () => {
const messages = [
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi there" },
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false)
})

it("returns false when assistant messages have tool_calls with reasoning_content", () => {
const messages = [
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", function: { name: "test" } }],
reasoning_content: "I should call test because...",
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false)
})

it("returns true when assistant messages have tool_calls but no reasoning_content field", () => {
const messages = [
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", function: { name: "test" } }],
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(true)
})

it("returns true when assistant messages have tool_calls with empty reasoning_content", () => {
const messages = [
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", function: { name: "test" } }],
reasoning_content: "",
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(true)
})

it("returns true when assistant messages have empty tool_calls array", () => {
const messages = [
{
role: "assistant",
content: "hello",
tool_calls: [],
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false)
})
Comment on lines +53 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.


it("returns false for non-assistant messages with tool_calls", () => {
const messages = [
{
role: "user",
content: "hello",
tool_calls: [{ id: "call_1" }],
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false)
})

it("returns true when at least one assistant message is missing reasoning_content despite tool_calls", () => {
const messages = [
{
role: "assistant",
content: "Let me think...",
reasoning_content: "thinking step 1",
},
{
role: "assistant",
content: null,
tool_calls: [{ id: "call_1", function: { name: "read_file" } }],
// no reasoning_content — this is the problematic message
},
{
role: "tool",
content: "file content",
tool_call_id: "call_1",
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(true)
})

it("handles non-array tool_calls gracefully", () => {
const messages = [
{
role: "assistant",
content: null,
tool_calls: "not-an-array" as unknown as unknown[],
},
]
expect(historyHasToolCallsWithoutReasoning(messages)).toBe(false)
})
})
26 changes: 26 additions & 0 deletions src/api/providers/utils/reasoning-history-guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Detects whether a converted OpenAI-format message history contains any
* assistant message with `tool_calls` but no non-empty `reasoning_content`.
* Used as a guard before enabling strict provider "thinking" modes that
* require reasoning_content to accompany every tool-call turn.
*
* This function operates on messages *after* conversion to the provider's
* OpenAI-compatible format (e.g. `convertToR1Format`, `convertToZAiFormat`),
* because it is only in the converted format that `reasoning_content`
* presence can be reliably determined.
*
* @param messages - Array of converted messages in OpenAI-compatible format
* @returns `true` if any assistant message has tool_calls but lacks
* non-empty reasoning_content
*/
export function historyHasToolCallsWithoutReasoning(
messages: Array<{ role?: string; tool_calls?: unknown[]; reasoning_content?: unknown }>,
): boolean {
return messages.some(
(m) =>
m.role === "assistant" &&
Array.isArray(m.tool_calls) &&
m.tool_calls.length > 0 &&
(typeof m.reasoning_content !== "string" || m.reasoning_content.length === 0),
)
}
22 changes: 20 additions & 2 deletions src/api/providers/zai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import {

import { type ApiHandlerOptions, getModelMaxOutputTokens } from "../../shared/api"
import { convertToZAiFormat } from "../transform/zai-format"
import { historyHasToolCallsWithoutReasoning } from "./utils/reasoning-history-guard"

import type { ApiHandlerCreateMessageMetadata } from "../index"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
import { handleOpenAIError } from "./utils/error-handler"
import { logger } from "../../utils/logging"

// Custom interface for Z.ai params to support thinking mode and reasoning effort tiers.
// Z.ai accepts the standard `reasoning_effort` ladder (none/minimal/low/medium/high/xhigh/max)
Expand Down Expand Up @@ -107,6 +109,22 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
// Use Z.ai format to preserve reasoning_content and merge post-tool text into tool messages
const convertedMessages = convertToZAiFormat(messages, { mergeToolResultText: true })

// Layer 1 Guard: detect incompatible history (tool_calls without reasoning_content)
// and disable thinking mode for this request to prevent a 400 error from the API.
const hasIncompatibleHistory = historyHasToolCallsWithoutReasoning(convertedMessages)

if (hasIncompatibleHistory) {
logger.warn("provider_reasoning_guard_triggered", {
ctx: "zai",
provider: "zai",
model,
taskId: metadata?.taskId,
})
}

// When incompatible history is detected, force reasoning off regardless of user settings.
const effectiveReasoning = hasIncompatibleHistory ? false : useReasoning

const params: ZAiChatCompletionParams = {
model,
max_tokens,
Expand All @@ -115,8 +133,8 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
stream: true,
stream_options: { include_usage: true },
// Thinking is ON by default for these models, so explicitly disable it when needed.
thinking: useReasoning ? { type: "enabled" } : { type: "disabled" },
reasoning_effort: reasoningEffort,
thinking: effectiveReasoning ? { type: "enabled" } : { type: "disabled" },
reasoning_effort: effectiveReasoning ? reasoningEffort : undefined,
tools: this.convertToolsForOpenAI(metadata?.tools),
tool_choice: metadata?.tool_choice,
parallel_tool_calls: metadata?.parallelToolCalls ?? true,
Expand Down
3 changes: 2 additions & 1 deletion src/core/tools/SwitchModeTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ export class SwitchModeTool extends BaseTool<"switch_mode"> {
}

// Switch the mode using shared handler
await task.providerRef.deref()?.handleModeSwitch(mode_slug)
// via: "switch_mode" — explicit tool call
await task.providerRef.deref()?.handleModeSwitch(mode_slug, "switch_mode")

pushToolResult(
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
Expand Down
58 changes: 57 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ import { t } from "../../i18n"
import { buildApiHandler } from "../../api"
import { forceFullModelDetailsLoad, hasLoadedFullDetails } from "../../api/providers/fetchers/lmstudio"

import { logger } from "../../utils/logging"
import { modeSwitchRisksReasoningIncompatibility } from "../../shared/reasoning-mode-compatibility"
import { ContextProxy } from "../config/ContextProxy"
import { ProviderSettingsManager } from "../config/ProviderSettingsManager"
import { CustomModesManager } from "../config/CustomModesManager"
Expand Down Expand Up @@ -1481,14 +1483,68 @@ export class ClineProvider
/**
* Handle switching to a new mode, including updating the associated API configuration
* @param newMode The mode to switch to
* @param via - The origin of the mode switch:
* "switch_mode" — explicit switch_mode tool call
* "new_task_delegation" — delegation via new_task
* "unknown_bypass" — cannot be attributed to explicit tool call (default)
*/
public async handleModeSwitch(newMode: Mode) {
public async handleModeSwitch(newMode: Mode, via: "switch_mode" | "new_task_delegation" | "unknown_bypass" = "unknown_bypass") {
const task = this.getCurrentTask()
const fromMode = (await this.getState())?.mode ?? "unknown"

if (task) {
TelemetryService.instance.captureModeSwitch(task.taskId, newMode)
task.emit(RooCodeEventName.TaskModeSwitched, task.taskId, newMode)

// Layer 2 — Orchestration safety: check for reasoning-mode incompatibility
// when switching modes without proper delegation (only relevant for bypass switches).
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)}\`,
)
}
Comment on lines +1540 to +1545

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
} 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

}
Comment on lines +1501 to +1546

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.


try {
// Update the task history with the new mode first.
const taskHistoryItem =
Expand Down
Loading
Loading