Skip to content

Commit ce47375

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

2 files changed

Lines changed: 135 additions & 20 deletions

File tree

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

web/src/llm-api/moonshot.ts

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ type LineResult = {
5959
patchedLine: string
6060
}
6161

62+
type MoonshotChatMessage = ChatCompletionRequestBody['messages'][number] & {
63+
cache_control?: unknown
64+
reasoning_content?: string | null
65+
}
66+
6267
export function isMoonshotModel(model: unknown): model is string {
6368
return typeof model === 'string' && model in MOONSHOT_MODEL_MAP
6469
}
@@ -89,6 +94,24 @@ function createMoonshotRequest(params: {
8994
fetch: typeof globalThis.fetch
9095
}) {
9196
const { body, originalModel, fetch } = params
97+
const moonshotBody = buildMoonshotRequestBody(body, originalModel)
98+
99+
return fetch(`${MOONSHOT_BASE_URL}/chat/completions`, {
100+
method: 'POST',
101+
headers: {
102+
Authorization: `Bearer ${getMoonshotApiKey()}`,
103+
'Content-Type': 'application/json',
104+
},
105+
body: JSON.stringify(moonshotBody),
106+
// @ts-expect-error - dispatcher is a valid undici option not in fetch types
107+
dispatcher: moonshotAgent,
108+
})
109+
}
110+
111+
export function buildMoonshotRequestBody(
112+
body: ChatCompletionRequestBody,
113+
originalModel: string,
114+
): Record<string, unknown> {
92115
const moonshotCompatibleBody = addKimiToolCompatibilityFields(body)
93116
const moonshotBody: Record<string, unknown> = {
94117
...moonshotCompatibleBody,
@@ -97,12 +120,7 @@ function createMoonshotRequest(params: {
97120
model: getMoonshotModelId(originalModel),
98121
}
99122

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-
}
123+
moonshotBody.thinking = createMoonshotThinking(moonshotBody)
106124

107125
delete moonshotBody.reasoning
108126
delete moonshotBody.reasoning_effort
@@ -115,29 +133,44 @@ function createMoonshotRequest(params: {
115133
moonshotBody.stream_options = { include_usage: true }
116134
}
117135

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-
})
136+
return moonshotBody
137+
}
138+
139+
function createMoonshotThinking(
140+
moonshotBody: Record<string, unknown>,
141+
): Record<string, unknown> {
142+
const reasoning =
143+
moonshotBody.reasoning && typeof moonshotBody.reasoning === 'object'
144+
? (moonshotBody.reasoning as { enabled?: boolean })
145+
: undefined
146+
if (reasoning?.enabled === false) {
147+
return { type: 'disabled' }
148+
}
149+
150+
const existingThinking =
151+
moonshotBody.thinking && typeof moonshotBody.thinking === 'object'
152+
? (moonshotBody.thinking as Record<string, unknown>)
153+
: {}
154+
if (existingThinking.type === 'disabled') {
155+
return { type: 'disabled' }
156+
}
157+
158+
return {
159+
...existingThinking,
160+
type: 'enabled',
161+
keep: 'all',
162+
}
128163
}
129164

130165
function normalizeMoonshotMessages(
131166
messages: ChatCompletionRequestBody['messages'],
132-
): ChatCompletionRequestBody['messages'] {
167+
): MoonshotChatMessage[] {
133168
return messages.map((message) => {
134169
const {
135170
cache_control: _cacheControl,
136171
content,
137172
...rest
138-
} = message as typeof message & {
139-
cache_control?: unknown
140-
}
173+
} = message as MoonshotChatMessage
141174
return {
142175
...rest,
143176
...(content !== undefined && {

0 commit comments

Comments
 (0)