Skip to content
Merged
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
71 changes: 70 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3168,6 +3168,7 @@ async function convertMessage(
): Promise<ConvertedMessageResult> {
const role = message.role === vscode.LanguageModelChatMessageRole.Assistant ? "assistant" : "user";
const textParts: string[] = [];
const thinkingTextParts: string[] = [];
const imageParts: OpenAiContentPart[] = [];
const toolCalls: OpenAiToolCall[] = [];
const toolResults: ApiMessage[] = [];
Expand Down Expand Up @@ -3313,6 +3314,14 @@ async function convertMessage(
continue;
}

if (part instanceof vscode.LanguageModelThinkingPart) {
const thinking = thinkingPartText(part);
if (thinking) {
thinkingTextParts.push(thinking);
}
continue;
}

const text = partToText(part);
if (text) {
textParts.push(text);
Expand All @@ -3323,6 +3332,14 @@ async function convertMessage(
const hasImages = imageParts.length > 0;
const textContent = textParts.join("\n");

// Thinking parts from the conversation history. Models like DeepSeek V4
// (OpenAI-compatible chat-completions) REQUIRE the previously emitted
// reasoning_content to be passed back unchanged on multi-turn requests —
// omitting it yields HTTP 400 "The reasoning_content in the thinking mode
// must be passed back to the API". This also enables cross-turn reasoning
// continuity for other families (Kimi, GLM, Qwen, MiniMax, Gemini).
const thinkingText = thinkingTextParts.length ? thinkingTextParts.join("\n").trim() : undefined;

let content: string | null | OpenAiContentPart[] = textContent;
if (hasImages) {
const multimodal: OpenAiContentPart[] = [];
Expand Down Expand Up @@ -3353,7 +3370,9 @@ async function convertMessage(
{
role,
content: typeof content === "string" ? content || null : content,
reasoning_content: shouldOmitReasoningEcho ? undefined : reasoningForToolCalls(toolCalls, reasoningContentByToolCallId),
reasoning_content: shouldOmitReasoningEcho
? undefined
: (reasoningForToolCalls(toolCalls, reasoningContentByToolCallId) ?? thinkingText),
tool_calls: toolCalls,
},
]);
Expand All @@ -3363,6 +3382,16 @@ async function convertMessage(
return finish(content ? [{ role, content }, ...toolResults] : toolResults);
}

if (role === "assistant") {
return finish([
{
role,
content: typeof content === "string" ? content || null : content,
reasoning_content: shouldEchoThinkingHistory(rawModelId) ? thinkingText : undefined,
},
]);
}

return finish([{ role, content }]);
}

Expand Down Expand Up @@ -3392,6 +3421,46 @@ function reasoningForToolCalls(toolCalls: OpenAiToolCall[], reasoningContentByTo
return reasoning.length ? reasoning.join("\n") : undefined;
}

/**
* Extract the raw thinking text from a history `LanguageModelThinkingPart`.
* `LanguageModelThinkingPart` is a proposed VS Code API available at runtime
* on all hosts we target (^1.125.0); `partToText` intentionally ignores it so
* the thinking text never leaks into the visible assistant `content`.
*/
function thinkingPartText(part: unknown): string {
if (!(part instanceof vscode.LanguageModelThinkingPart)) {
return "";
}
const value: string | string[] = part.value;
return Array.isArray(value) ? value.filter((chunk): chunk is string => typeof chunk === "string").join("\n") : value;
}

/**
* Whether the CURRENT model accepts `reasoning_content` echoed on assistant
* history messages.
*
* CONTRACT:
* - DeepSeek V4 (and other OpenAI-compatible reasoning models) REQUIRE the
* previous `reasoning_content` to be passed back unchanged on multi-turn
* requests; omitting it 400s (upstream DeepSeek V4 issue #36354, same class
* of error as MiMo's validator in issue #38).
* - Gemini maps it to `thought: true` parts and needs the echo too.
* - GLM / Kimi / Qwen / MiniMax tolerate the echo (see processAssistantMessage
* CONTRACT above) and benefit from cross-turn reasoning continuity.
* - Excluded families reject or ignore the field:
* - MiMo: strict Pydantic-style validator rejects `reasoning_content` (issue #38).
* - GPT: OpenAI Responses API — messages carry no `reasoning_content` field.
* - Claude: Anthropic Messages API — no `reasoning_content` field.
* - Unknown families are left untouched (no echo).
*/
function shouldEchoThinkingHistory(rawModelId: string | undefined): boolean {
if (rawModelId === undefined) return false;
if (/^mimo-/i.test(rawModelId)) return false;
if (/^gpt-/i.test(rawModelId)) return false;
if (/^claude-/i.test(rawModelId)) return false;
return thinkingFamily(rawModelId) !== null || /^gemini-/i.test(rawModelId);
}

function messageText(message: vscode.LanguageModelChatRequestMessage): string {
return message.content.map(partToText).filter(Boolean).join("\n");
}
Expand Down
Loading