Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/preserve-empty-reasoning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Preserve empty model reasoning blocks across providers so multi-step tool calls can continue.
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ function convertMessage(message: Message, model: string): MessageParam {
thinking: part.think,
signature: part.encrypted,
} satisfies ThinkingBlockParam);
} else if (part.think !== '' && shouldPreserveUnsignedThinking(model)) {
} else if (shouldPreserveUnsignedThinking(model)) {
blocks.push({ type: 'thinking', thinking: part.think } as unknown as ThinkingBlockParam);
}
} else if (part.type === 'video_url') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChatProviderError,
normalizeAPIStatusError,
} from '../errors';
import type { Message, StreamedMessagePart, ToolCall } from '../message';
import type { Message, StreamedMessagePart, ThinkPart, ToolCall } from '../message';
import { isToolDeclarationOnlyMessage } from '../message';
import type {
ChatProvider,
Expand Down Expand Up @@ -127,6 +127,7 @@ interface GoogleContent {

interface GooglePart {
text?: string;
thought?: boolean;
functionCall?: { name: string; args: Record<string, unknown> };
functionResponse?: {
name: string;
Expand Down Expand Up @@ -218,8 +219,14 @@ function messageToGoogleGenAI(message: Message): GoogleContent {
case 'text':
parts.push({ text: part.text });
break;
case 'think':
case 'think': {
const thoughtPart: GooglePart = { text: part.think, thought: true };
if (part.encrypted !== undefined && part.encrypted.length > 0) {
thoughtPart.thoughtSignature = part.encrypted;
}
parts.push(thoughtPart);
break;
}
case 'image_url':
parts.push(convertMediaUrl(part.imageUrl.url, 'image/jpeg'));
break;
Expand Down Expand Up @@ -489,8 +496,13 @@ export class GoogleGenAIStreamedMessage implements StreamedMessage {

for (const part of contentParts) {
const p = part as Record<string, unknown>;
if (p['thought'] === true && p['text']) {
parts.push({ type: 'think', think: p['text'] as string });
if (p['thought'] === true && typeof p['text'] === 'string') {
const thoughtSignature = p['thoughtSignature'] ?? p['thought_signature'];
const thinkPart: ThinkPart = { type: 'think', think: p['text'] };
if (typeof thoughtSignature === 'string' && thoughtSignature.length > 0) {
thinkPart.encrypted = thoughtSignature;
}
parts.push(thinkPart);
} else if (p['text']) {
parts.push({ type: 'text', text: p['text'] as string });
} else if (p['functionCall'] || p['function_call']) {
Expand All @@ -500,15 +512,16 @@ export class GoogleGenAIStreamedMessage implements StreamedMessage {
const id_ = (fc['id'] as string) ?? crypto.randomUUID();
const toolCallId = `${name}_${id_}`;
const thoughtSigB64 = p['thoughtSignature'] ?? p['thought_signature'];
parts.push({
const toolCall: ToolCall = {
type: 'function',
id: toolCallId,
name,
arguments: fc['args'] ? JSON.stringify(fc['args']) : '{}',
...(thoughtSigB64
? { extras: { thought_signature_b64: thoughtSigB64 as string } }
: {}),
} satisfies ToolCall);
};
if (typeof thoughtSigB64 === 'string' && thoughtSigB64.length > 0) {
toolCall.extras = { thought_signature_b64: thoughtSigB64 };
}
parts.push(toolCall);
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions packages/agent-core-v2/src/app/llmProtocol/providers/kimi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ function isEffectivelyEmptyContent(parts: ContentPart[]): boolean {

function convertMessage(message: Message): OpenAIMessage {
let reasoningContent = '';
let hasReasoningPart = false;
const nonThinkParts: ContentPart[] = [];

for (const part of message.content) {
if (part.type === 'think') {
hasReasoningPart = true;
reasoningContent += part.think;
} else {
nonThinkParts.push(part);
Expand Down Expand Up @@ -154,7 +156,7 @@ function convertMessage(message: Message): OpenAIMessage {
result.tool_call_id = message.toolCallId;
}

if (reasoningContent) {
if (hasReasoningPart) {
result.reasoning_content = reasoningContent;
}

Expand Down Expand Up @@ -280,7 +282,7 @@ class KimiStreamedMessage implements StreamedMessage {
if (!message) return;

const rc = (message as unknown as Record<string, unknown>)['reasoning_content'];
if (typeof rc === 'string' && rc) {
if (typeof rc === 'string') {
yield { type: 'think', think: rc } satisfies StreamedMessagePart;
}

Expand Down Expand Up @@ -332,7 +334,7 @@ class KimiStreamedMessage implements StreamedMessage {
const delta = choice.delta;

const rc = (delta as unknown as Record<string, unknown>)['reasoning_content'];
if (typeof rc === 'string' && rc) {
if (typeof rc === 'string') {
yield { type: 'think', think: rc } satisfies StreamedMessagePart;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function extractReasoningContent(
const keys: readonly string[] = explicitKey !== undefined ? [explicitKey] : KNOWN_REASONING_KEYS;
for (const key of keys) {
const value = record[key];
if (typeof value === 'string' && value.length > 0) return value;
if (typeof value === 'string') return value;
}
return undefined;
}
Expand Down Expand Up @@ -155,10 +155,12 @@ function convertMessage(
toolMessageConversion: ToolMessageConversion,
): OpenAIMessage {
let reasoningContent = '';
let hasReasoningPart = false;
const nonThinkParts: ContentPart[] = [];

for (const part of message.content) {
if (part.type === 'think') {
hasReasoningPart = true;
reasoningContent += part.think;
} else {
nonThinkParts.push(part);
Expand Down Expand Up @@ -212,7 +214,7 @@ function convertMessage(
result.tool_call_id = message.toolCallId;
}

if (reasoningContent) {
if (hasReasoningPart) {
result[reasoningKey ?? DEFAULT_OUTBOUND_REASONING_KEY] = reasoningContent;
}

Expand Down Expand Up @@ -354,7 +356,7 @@ export class OpenAILegacyStreamedMessage implements StreamedMessage {
if (!message) return;

const reasoning = extractReasoningContent(message, reasoningKey);
if (reasoning) {
if (reasoning !== undefined) {
yield { type: 'think', think: reasoning } satisfies StreamedMessagePart;
}

Expand Down Expand Up @@ -405,7 +407,7 @@ export class OpenAILegacyStreamedMessage implements StreamedMessage {
const delta = choice.delta;

const reasoning = extractReasoningContent(delta, reasoningKey);
if (reasoning) {
if (reasoning !== undefined) {
yield { type: 'think', think: reasoning } satisfies StreamedMessagePart;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,14 +534,14 @@ function convertMessage(
if (part.type === 'think') {
flushPendingParts();
const encryptedValue = part.encrypted;
const summaries: unknown[] = [{ type: 'summary_text', text: part.think || '' }];
const summaries: unknown[] = [{ type: 'summary_text', text: part.think }];
i += 1;
while (i < n) {
const nextPart = message.content[i];
if (nextPart === undefined) break;
if (nextPart.type !== 'think') break;
if (nextPart.encrypted !== encryptedValue) break;
summaries.push({ type: 'summary_text', text: nextPart.think || '' });
summaries.push({ type: 'summary_text', text: nextPart.think });
i += 1;
}
result.push({
Expand Down Expand Up @@ -707,9 +707,11 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
arguments: outputItem.arguments ?? null,
} satisfies ToolCall;
} else if (outputItem.type === 'reasoning') {
let hasReasoningSummary = false;
for (const summary of outputItem.summary) {
const text = readStringField(summary, 'text');
if (text === undefined) continue;
hasReasoningSummary = true;
const thinkPart: StreamedMessagePart = {
type: 'think',
think: text,
Expand All @@ -719,6 +721,13 @@ export class OpenAIResponsesStreamedMessage implements StreamedMessage {
}
yield thinkPart;
}
if (!hasReasoningSummary) {
const thinkPart: StreamedMessagePart = { type: 'think', think: '' };
if (outputItem.encryptedContent !== undefined) {
(thinkPart as { encrypted: string }).encrypted = outputItem.encryptedContent;
}
yield thinkPart;
}
}
}
}
Expand Down
Loading
Loading