Skip to content

Commit 7dc253c

Browse files
authored
fix(agent-core-v2): restore thinking for the openrouter reasoning dialect (#3910)
Keep string reasoning fields when a reasoning_details array is present, stamp each think part by source, and replay those fields on the next request.
1 parent 92c3c59 commit 7dc253c

10 files changed

Lines changed: 296 additions & 48 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Restore thinking display for OpenAI-compatible providers that send a reasoning string next to an unrecognized reasoning_details array.

‎packages/agent-core-v2/docs/state-manifest.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ export interface AgentStateSnapshot {
730730
encrypted?: string;
731731
detailsIndex?: number;
732732
hidden?: boolean;
733+
reasoningKey?: string;
733734
} | /* ImageURLPart — packages/agent-core-v2/src/human/llm/message.ts */ {
734735
type: 'image_url';
735736
imageUrl: {
@@ -1027,6 +1028,7 @@ export interface AgentStateSnapshot {
10271028
encrypted?: string;
10281029
detailsIndex?: number;
10291030
hidden?: boolean;
1031+
reasoningKey?: string;
10301032
} | /* ImageURLPart — packages/agent-core-v2/src/human/llm/message.ts */ {
10311033
type: 'image_url';
10321034
imageUrl: {

‎packages/agent-core-v2/src/agent/loop/loopService.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,7 @@ export class AgentLoopService extends Disposable implements IAgentLoopService {
14261426
encrypted: delta.encrypted,
14271427
detailsIndex: delta.detailsIndex,
14281428
hidden: delta.hidden,
1429+
reasoningKey: delta.reasoningKey,
14291430
});
14301431
if (part?.type === 'think' && part.hidden === true) return;
14311432
void this.dispatcher.dispatch(

‎packages/agent-core-v2/src/agent/loop/machine/engine.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type MachineEngineDelta =
4040
readonly encrypted?: string;
4141
readonly detailsIndex?: number;
4242
readonly hidden?: boolean;
43+
readonly reasoningKey?: string;
4344
}
4445
| {
4546
readonly kind: 'toolCall';
@@ -227,6 +228,7 @@ function createDeltaSplitter(): (part: StreamedMessagePart) => MachineEngineDelt
227228
encrypted: part.encrypted,
228229
detailsIndex: part.detailsIndex,
229230
hidden: part.hidden,
231+
reasoningKey: part.reasoningKey,
230232
};
231233
case 'image_url':
232234
case 'audio_url':

‎packages/agent-core-v2/src/human/agent/historySchema.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const thinkPartSchema = z.object({
1010
encrypted: z.string().optional(),
1111
detailsIndex: z.number().optional(),
1212
hidden: z.boolean().optional(),
13+
reasoningKey: z.string().optional(),
1314
});
1415
const imageUrlPartSchema = z.object({
1516
type: z.literal('image_url'),

‎packages/agent-core-v2/src/human/llm/message.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ThinkPart {
1818
encrypted?: string;
1919
detailsIndex?: number;
2020
hidden?: boolean;
21+
reasoningKey?: string;
2122
}
2223

2324
export interface ImageURLPart {
@@ -111,6 +112,9 @@ export function mergeInPlace(target: StreamedMessagePart, source: StreamedMessag
111112
if (target.hidden !== source.hidden) {
112113
return false;
113114
}
115+
if (target.reasoningKey !== source.reasoningKey) {
116+
return false;
117+
}
114118
target.think += source.think;
115119
if (source.encrypted !== undefined) {
116120
target.encrypted = source.encrypted;

‎packages/agent-core-v2/src/human/llm/requester/bases/openai/format.ts‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ import { lowerMessage } from './lower';
3939
import { extractToolMedia } from './patterns';
4040
import {
4141
convertReasoningDetails,
42-
extractReasoning,
4342
extractReasoningDetails,
43+
extractReasoningStrings,
4444
} from './reasoning-key';
4545

4646
export function responseFormatToOpenAI(format: ResponseFormat): Record<string, unknown> {
@@ -299,23 +299,20 @@ export function createOpenAIFormat(): OpenAIProtocolFormat {
299299
}
300300
const reasoningDetails =
301301
options?.reasoningKey === undefined ? extractReasoningDetails(delta) : undefined;
302-
if (reasoningDetails !== undefined) {
303-
const inline = extractReasoning(delta, 'reasoning_content');
304-
if (inline !== undefined) {
302+
for (const reasoning of extractReasoningStrings(delta)) {
303+
if (reasoning.key === 'reasoning_content') {
305304
seenReasoningContent = true;
306-
sink.onDelta({ type: 'think', think: inline.value });
307305
}
306+
sink.onDelta({
307+
type: 'think',
308+
think: reasoning.value,
309+
reasoningKey: reasoning.key,
310+
});
311+
}
312+
if (reasoningDetails !== undefined) {
308313
for (const part of convertReasoningDetails(reasoningDetails, seenReasoningContent)) {
309314
sink.onDelta(part);
310315
}
311-
} else {
312-
const reasoning = extractReasoning(delta);
313-
if (reasoning !== undefined) {
314-
if (reasoning.key === 'reasoning_content') {
315-
seenReasoningContent = true;
316-
}
317-
sink.onDelta({ type: 'think', think: reasoning.value });
318-
}
319316
}
320317
if (typeof delta.content === 'string' && delta.content.length > 0) {
321318
sink.onDelta({ type: 'text', text: delta.content });

‎packages/agent-core-v2/src/human/llm/requester/bases/openai/lower.ts‎

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,19 +121,49 @@ export function lowerMessage(message: Message, lower: OpenAILowerContext): OpenA
121121
converted = { role: message.role, content: content ?? '' };
122122
}
123123
const reasoningDetails: Record<string, unknown>[] = [];
124+
const stringFields = new Map<string, string>();
125+
let unstamped = '';
126+
let hasUnstamped = false;
124127
for (const part of message.content) {
125-
if (part.type !== 'think' || part.detailsIndex === undefined) continue;
126-
if (part.think.length > 0) {
127-
reasoningDetails.push({ type: 'summary', summary: part.think });
128+
if (part.type !== 'think') continue;
129+
if (part.detailsIndex !== undefined) {
130+
if (part.think.length > 0) {
131+
reasoningDetails.push({ type: 'summary', summary: part.think });
132+
}
133+
if (part.encrypted !== undefined) {
134+
reasoningDetails.push({ type: 'encrypted', encrypted: part.encrypted });
135+
}
136+
continue;
128137
}
129-
if (part.encrypted !== undefined) {
130-
reasoningDetails.push({ type: 'encrypted', encrypted: part.encrypted });
138+
if (part.reasoningKey !== undefined && part.reasoningKey !== REASONING_DETAILS_KEY) {
139+
const current = stringFields.get(part.reasoningKey) ?? '';
140+
stringFields.set(
141+
part.reasoningKey,
142+
part.hidden === true ? current : current + part.think,
143+
);
144+
continue;
131145
}
146+
hasUnstamped = true;
147+
if (part.hidden !== true) {
148+
unstamped += part.think;
149+
}
150+
}
151+
if (hasUnstamped) {
152+
stringFields.set(reasoningKey, (stringFields.get(reasoningKey) ?? '') + unstamped);
132153
}
133154
if (reasoningDetails.length > 0) {
134155
(converted as Record<string, unknown>)[REASONING_DETAILS_KEY] = reasoningDetails;
135-
(converted as Record<string, unknown>)[DEFAULT_REASONING_KEY] = reasoningContent;
136-
} else if (hasReasoningPart || (preserveThinking && message.role === 'assistant')) {
156+
(converted as Record<string, unknown>)[DEFAULT_REASONING_KEY] =
157+
stringFields.get(DEFAULT_REASONING_KEY) ?? reasoningContent;
158+
}
159+
for (const [key, value] of stringFields) {
160+
(converted as Record<string, unknown>)[key] = value;
161+
}
162+
if (
163+
stringFields.size === 0 &&
164+
reasoningDetails.length === 0 &&
165+
(hasReasoningPart || (preserveThinking && message.role === 'assistant'))
166+
) {
137167
(converted as Record<string, unknown>)[reasoningKey] = reasoningContent;
138168
}
139169
return [converted];

‎packages/agent-core-v2/src/human/llm/requester/bases/openai/reasoning-key.ts‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,29 @@ export type ReasoningKey = (typeof KNOWN_REASONING_KEYS)[number];
1010

1111
export const DEFAULT_REASONING_KEY: ReasoningKey = KNOWN_REASONING_KEYS[0];
1212

13+
export function extractReasoningStrings(
14+
source: unknown,
15+
): { key: string; value: string }[] {
16+
if (typeof source !== 'object' || source === null) return [];
17+
const record = source as Record<string, unknown>;
18+
const found: { key: string; value: string }[] = [];
19+
for (const key of KNOWN_REASONING_KEYS) {
20+
const value = record[key];
21+
if (typeof value === 'string') found.push({ key, value });
22+
}
23+
return found;
24+
}
25+
1326
export function extractReasoning(
1427
source: unknown,
1528
explicitKey?: string,
1629
): { key: string; value: string } | undefined {
17-
if (typeof source !== 'object' || source === null) return undefined;
18-
const record = source as Record<string, unknown>;
19-
const keys: readonly string[] = explicitKey !== undefined ? [explicitKey] : KNOWN_REASONING_KEYS;
20-
for (const key of keys) {
21-
const value = record[key];
22-
if (typeof value === 'string') return { key, value };
30+
if (explicitKey !== undefined) {
31+
if (typeof source !== 'object' || source === null) return undefined;
32+
const value = (source as Record<string, unknown>)[explicitKey];
33+
return typeof value === 'string' ? { key: explicitKey, value } : undefined;
2334
}
24-
return undefined;
35+
return extractReasoningStrings(source)[0];
2536
}
2637

2738
export class ReasoningKeyDialect {
@@ -32,7 +43,7 @@ export class ReasoningKeyDialect {
3243
observe(source: unknown): string | undefined {
3344
const found = extractReasoning(source, this._explicitKey);
3445
if (found === undefined) return undefined;
35-
if (this._explicitKey === undefined) {
46+
if (this._explicitKey === undefined && this._detected === undefined) {
3647
this._detected = found.key;
3748
}
3849
return found.value;
@@ -92,6 +103,7 @@ export function convertReasoningDetails(
92103
think: element.summary,
93104
detailsIndex: element.index,
94105
hidden: hiddenSummary ? true : undefined,
106+
reasoningKey: REASONING_DETAILS_KEY,
95107
} satisfies ThinkPart);
96108
}
97109
if (element.type !== 'summary' && element.encrypted !== undefined && element.encrypted.length > 0) {
@@ -100,6 +112,7 @@ export function convertReasoningDetails(
100112
think: '',
101113
encrypted: element.encrypted,
102114
detailsIndex: element.index,
115+
reasoningKey: REASONING_DETAILS_KEY,
103116
} satisfies ThinkPart);
104117
}
105118
}

0 commit comments

Comments
 (0)