Skip to content

Commit 25ee84a

Browse files
committed
Preserve Moonshot Kimi reasoning history
1 parent f6989d3 commit 25ee84a

3 files changed

Lines changed: 122 additions & 16 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, it } from 'bun:test'
2+
3+
import { buildMoonshotRequestBody } from '../moonshot'
4+
5+
describe('buildMoonshotRequestBody', () => {
6+
it('enables preserved thinking by default for Kimi K2.6', () => {
7+
const body = buildMoonshotRequestBody(
8+
{
9+
model: 'moonshotai/kimi-k2.6',
10+
messages: [
11+
{
12+
role: 'assistant',
13+
content: 'I will inspect the files.',
14+
reasoning_content: 'Need to understand the repo first.',
15+
},
16+
{
17+
role: 'user',
18+
content: 'Continue.',
19+
},
20+
],
21+
},
22+
'moonshotai/kimi-k2.6',
23+
)
24+
25+
expect(body.model).toBe('kimi-k2.6')
26+
expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
27+
expect(body.messages).toEqual([
28+
{
29+
role: 'assistant',
30+
content: 'I will inspect the files.',
31+
reasoning_content: 'Need to understand the repo first.',
32+
},
33+
{
34+
role: 'user',
35+
content: 'Continue.',
36+
},
37+
])
38+
})
39+
40+
it('keeps historical reasoning when thinking is explicitly enabled', () => {
41+
const body = buildMoonshotRequestBody(
42+
{
43+
model: 'moonshotai/kimi-k2.6',
44+
messages: [{ role: 'user', content: 'hello' }],
45+
reasoning: { enabled: true },
46+
},
47+
'moonshotai/kimi-k2.6',
48+
)
49+
50+
expect(body.thinking).toEqual({ type: 'enabled', keep: 'all' })
51+
expect(body.reasoning).toBeUndefined()
52+
})
53+
54+
it('does not preserve thinking when reasoning is explicitly disabled', () => {
55+
const body = buildMoonshotRequestBody(
56+
{
57+
model: 'moonshotai/kimi-k2.6',
58+
messages: [
59+
{
60+
role: 'assistant',
61+
content: 'Done.',
62+
reasoning_content: 'Used the tool result.',
63+
},
64+
{ role: 'user', content: 'next' },
65+
],
66+
reasoning: { enabled: false },
67+
},
68+
'moonshotai/kimi-k2.6',
69+
)
70+
71+
expect(body.thinking).toEqual({ type: 'disabled' })
72+
expect(body.reasoning).toBeUndefined()
73+
})
74+
})

web/src/llm-api/moonshot.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,24 @@ function createMoonshotRequest(params: {
8989
fetch: typeof globalThis.fetch
9090
}) {
9191
const { body, originalModel, fetch } = params
92+
const moonshotBody = buildMoonshotRequestBody(body, originalModel)
93+
94+
return fetch(`${MOONSHOT_BASE_URL}/chat/completions`, {
95+
method: 'POST',
96+
headers: {
97+
Authorization: `Bearer ${getMoonshotApiKey()}`,
98+
'Content-Type': 'application/json',
99+
},
100+
body: JSON.stringify(moonshotBody),
101+
// @ts-expect-error - dispatcher is a valid undici option not in fetch types
102+
dispatcher: moonshotAgent,
103+
})
104+
}
105+
106+
export function buildMoonshotRequestBody(
107+
body: ChatCompletionRequestBody,
108+
originalModel: string,
109+
): Record<string, unknown> {
92110
const moonshotCompatibleBody = addKimiToolCompatibilityFields(body)
93111
const moonshotBody: Record<string, unknown> = {
94112
...moonshotCompatibleBody,
@@ -97,12 +115,7 @@ function createMoonshotRequest(params: {
97115
model: getMoonshotModelId(originalModel),
98116
}
99117

100-
if (moonshotBody.reasoning && typeof moonshotBody.reasoning === 'object') {
101-
const reasoning = moonshotBody.reasoning as { enabled?: boolean }
102-
moonshotBody.thinking = {
103-
type: reasoning.enabled === false ? 'disabled' : 'enabled',
104-
}
105-
}
118+
moonshotBody.thinking = createMoonshotThinking(moonshotBody)
106119

107120
delete moonshotBody.reasoning
108121
delete moonshotBody.reasoning_effort
@@ -115,16 +128,33 @@ function createMoonshotRequest(params: {
115128
moonshotBody.stream_options = { include_usage: true }
116129
}
117130

118-
return fetch(`${MOONSHOT_BASE_URL}/chat/completions`, {
119-
method: 'POST',
120-
headers: {
121-
Authorization: `Bearer ${getMoonshotApiKey()}`,
122-
'Content-Type': 'application/json',
123-
},
124-
body: JSON.stringify(moonshotBody),
125-
// @ts-expect-error - dispatcher is a valid undici option not in fetch types
126-
dispatcher: moonshotAgent,
127-
})
131+
return moonshotBody
132+
}
133+
134+
function createMoonshotThinking(
135+
moonshotBody: Record<string, unknown>,
136+
): Record<string, unknown> {
137+
const reasoning =
138+
moonshotBody.reasoning && typeof moonshotBody.reasoning === 'object'
139+
? (moonshotBody.reasoning as { enabled?: boolean })
140+
: undefined
141+
if (reasoning?.enabled === false) {
142+
return { type: 'disabled' }
143+
}
144+
145+
const existingThinking =
146+
moonshotBody.thinking && typeof moonshotBody.thinking === 'object'
147+
? (moonshotBody.thinking as Record<string, unknown>)
148+
: {}
149+
if (existingThinking.type === 'disabled') {
150+
return { type: 'disabled' }
151+
}
152+
153+
return {
154+
...existingThinking,
155+
type: 'enabled',
156+
keep: 'all',
157+
}
128158
}
129159

130160
function normalizeMoonshotMessages(

web/src/llm-api/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface CodebuffMetadata {
1616
export interface ChatMessage {
1717
role: 'system' | 'user' | 'assistant' | 'tool'
1818
content?: string | ChatCompletionContentPart[] | null
19+
reasoning_content?: string | null
20+
reasoning?: string | null
1921
name?: string
2022
tool_calls?: Array<{
2123
id: string

0 commit comments

Comments
 (0)