diff --git a/src/api/providers/moonshot.ts b/src/api/providers/moonshot.ts index d29a10a3b3e..174fb99b10a 100644 --- a/src/api/providers/moonshot.ts +++ b/src/api/providers/moonshot.ts @@ -38,7 +38,7 @@ export class MoonshotHandler extends OpenAiHandler { } } - // Override to always include max_tokens for Moonshot (not max_completion_tokens) + // Override to add Kimi-specific thinking parameter format protected override addMaxTokensIfNeeded( requestOptions: | OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming @@ -47,5 +47,13 @@ export class MoonshotHandler extends OpenAiHandler { ): void { // Moonshot uses max_tokens instead of max_completion_tokens requestOptions.max_tokens = this.options.modelMaxTokens || modelInfo.maxTokens + + // For Kimi models with reasoning budget, use { type: "enabled" } instead of { max_tokens: ... } + const { info: model } = this.getModel() + if (this.options.enableReasoningEffort && (model as any).supportsReasoningBudget) { + // Remove the OpenAI-style reasoning parameter and use Kimi's thinking parameter + delete (requestOptions as any).reasoning + ;(requestOptions as any).thinking = { type: "enabled" } + } } } diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 8b36bce9d25..41d95acbcac 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -193,6 +193,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl let lastUsage const activeToolCallIds = new Set() + // Track reasoning content for Kimi/DeepSeek to associate with tool calls + let pendingReasoningContent: string | undefined for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta ?? {} @@ -212,6 +214,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ? delta.reasoning : undefined if (reasoningText) { + pendingReasoningContent = reasoningText yield { type: "reasoning", text: reasoningText, @@ -219,7 +222,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl } // kilocode_change end - yield* this.processToolCalls(delta, finishReason, activeToolCallIds) + yield* this.processToolCalls(delta, finishReason, activeToolCallIds, pendingReasoningContent) + pendingReasoningContent = undefined if (chunk.usage) { lastUsage = chunk.usage @@ -269,6 +273,12 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl text: message.reasoning, } } + if ("reasoning_content" in message && typeof message.reasoning_content === "string") { + yield { + type: "reasoning", + text: message.reasoning_content, + } + } if (message.content) { yield { type: "text", @@ -497,11 +507,13 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl * @param delta - The delta object from the stream chunk * @param finishReason - The finish_reason from the stream chunk * @param activeToolCallIds - Set to track active tool call IDs (mutated in place) + * @param reasoningContent - Optional reasoning content to include with tool calls (for Kimi/DeepSeek) */ private *processToolCalls( delta: OpenAI.Chat.Completions.ChatCompletionChunk.Choice.Delta | undefined, finishReason: string | null | undefined, activeToolCallIds: Set, + reasoningContent?: string, ): Generator< | { type: "tool_call_partial"; index: number; id?: string; name?: string; arguments?: string } | { type: "tool_call_end"; id: string } diff --git a/src/api/transform/openai-format.ts b/src/api/transform/openai-format.ts index 5d96a3de36f..1636605eac8 100644 --- a/src/api/transform/openai-format.ts +++ b/src/api/transform/openai-format.ts @@ -317,6 +317,10 @@ export function convertToOpenAiMessages( if (mapped) { ;(baseMessage as any).reasoning_details = mapped } + // Preserve reasoning_content for Kimi/DeepSeek interleaved thinking + if (messageWithDetails.reasoning_content) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } } openAiMessages.push(baseMessage) @@ -502,6 +506,11 @@ export function convertToOpenAiMessages( baseMessage.reasoning_details = mapped } + // Preserve reasoning_content for Kimi/DeepSeek interleaved thinking + if (messageWithDetails.reasoning_content) { + ;(baseMessage as any).reasoning_content = messageWithDetails.reasoning_content + } + // Add tool_calls after reasoning_details // Cannot be an empty array. API expects an array with minimum length 1, and will respond with an error if it's empty if (tool_calls.length > 0) { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 21f0d6c5434..aa2d70ebc91 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1088,6 +1088,17 @@ export class Task extends EventEmitter implements TaskLike { } else if (!messageWithTs.content) { messageWithTs.content = [reasoningBlock] } + + // For Kimi (Moonshot), add reasoning_content as top-level property when there are tool_use blocks + // This is required because Kimi's API expects: "reasoning_content" + "tool_calls" in the same message + if ( + this.apiConfiguration.apiProvider === "moonshot" && + reasoning && + Array.isArray(messageWithTs.content) && + messageWithTs.content.some((block: any) => block.type === "tool_use") + ) { + messageWithTs.reasoning_content = reasoning + } } else if (reasoningData?.encrypted_content) { // OpenAI Native encrypted reasoning const reasoningBlock = { @@ -4847,11 +4858,19 @@ export class Task extends EventEmitter implements TaskLike { } // Create message with reasoning_details property - cleanConversationHistory.push({ + const msgWithReasoningContent: Anthropic.Messages.MessageParam & { + reasoning_content?: string + reasoning_details?: any + } = { role: "assistant", content: assistantContent, reasoning_details: msgWithDetails.reasoning_details, - } as any) + } + // Preserve reasoning_content for Kimi (Moonshot) + if (msg.reasoning_content) { + msgWithReasoningContent.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithReasoningContent) continue } @@ -4884,10 +4903,15 @@ export class Task extends EventEmitter implements TaskLike { assistantContent = rest } - cleanConversationHistory.push({ - role: "assistant", - content: assistantContent, - } satisfies Anthropic.Messages.MessageParam) + const msgWithEncryptedReasoning: Anthropic.Messages.MessageParam & { reasoning_content?: string } = + { + role: "assistant", + content: assistantContent, + } + if (msg.reasoning_content) { + msgWithEncryptedReasoning.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithEncryptedReasoning) continue } else if (hasPlainTextReasoning) { @@ -4911,10 +4935,15 @@ export class Task extends EventEmitter implements TaskLike { } } - cleanConversationHistory.push({ - role: "assistant", - content: assistantContent, - } satisfies Anthropic.Messages.MessageParam) + const msgWithPlainTextReasoning: Anthropic.Messages.MessageParam & { reasoning_content?: string } = + { + role: "assistant", + content: assistantContent, + } + if (msg.reasoning_content) { + msgWithPlainTextReasoning.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(msgWithPlainTextReasoning) continue } @@ -4922,10 +4951,15 @@ export class Task extends EventEmitter implements TaskLike { // Default path for regular messages (no embedded reasoning) if (msg.role) { - cleanConversationHistory.push({ + const baseMessage: Anthropic.Messages.MessageParam & { reasoning_content?: string } = { role: msg.role, content: msg.content as Anthropic.Messages.ContentBlockParam[] | string, - }) + } + // Preserve reasoning_content for Kimi (Moonshot) when present + if (msg.reasoning_content) { + baseMessage.reasoning_content = msg.reasoning_content + } + cleanConversationHistory.push(baseMessage) } } diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index 0fced0a9a50..7ab034455ca 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -6,13 +6,13 @@ import stripBom from "strip-bom" import { XMLBuilder } from "fast-xml-parser" import delay from "delay" -import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types" - import { createDirectoriesForFile } from "../../utils/fs" import { arePathsEqual, getReadablePath } from "../../utils/path" import { formatResponse } from "../../core/prompts/responses" import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics" +import { ClineSayTool } from "../../shared/ExtensionMessage" import { Task } from "../../core/task/Task" +import { DEFAULT_WRITE_DELAY_MS, isNativeProtocol } from "@roo-code/types" import { resolveToolProtocol } from "../../utils/resolveToolProtocol" import { DecorationController } from "./DecorationController"