Skip to content

Commit 4452eab

Browse files
committed
scope kimi tool compatibility
1 parent b1d8b31 commit 4452eab

5 files changed

Lines changed: 190 additions & 2 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { describe, expect, it } from 'bun:test'
2+
3+
import { addKimiToolCompatibilityFields } from '../kimi-tool-compat'
4+
5+
import type { ChatCompletionRequestBody } from '../types'
6+
7+
describe('addKimiToolCompatibilityFields', () => {
8+
it('adds declaration ids and tool-result names without mutating input', () => {
9+
const body: ChatCompletionRequestBody = {
10+
model: 'moonshotai/kimi-k2.6',
11+
messages: [
12+
{
13+
role: 'assistant',
14+
content: '',
15+
tool_calls: [
16+
{
17+
id: 'call_123',
18+
type: 'function',
19+
function: {
20+
name: 'read_files',
21+
arguments: JSON.stringify({ paths: ['README.md'] }),
22+
},
23+
},
24+
],
25+
},
26+
{
27+
role: 'tool',
28+
tool_call_id: 'call_123',
29+
content: JSON.stringify({ message: 'ok' }),
30+
},
31+
],
32+
tools: [
33+
{
34+
type: 'function',
35+
function: {
36+
name: 'read_files',
37+
description: 'Read files',
38+
parameters: { type: 'object' },
39+
},
40+
},
41+
],
42+
}
43+
44+
const result = addKimiToolCompatibilityFields(body)
45+
46+
expect(result.tools?.[0]).toEqual({
47+
id: 'tool_1',
48+
type: 'function',
49+
function: {
50+
name: 'read_files',
51+
description: 'Read files',
52+
parameters: { type: 'object' },
53+
},
54+
})
55+
expect(result.messages[1]).toEqual({
56+
role: 'tool',
57+
tool_call_id: 'call_123',
58+
name: 'read_files',
59+
content: JSON.stringify({ message: 'ok' }),
60+
})
61+
expect(body.tools?.[0]).not.toHaveProperty('id')
62+
expect(body.messages[1]).not.toHaveProperty('name')
63+
})
64+
65+
it('preserves existing ids and names', () => {
66+
const body: ChatCompletionRequestBody = {
67+
model: 'moonshotai/kimi-k2.6',
68+
messages: [
69+
{
70+
role: 'assistant',
71+
content: '',
72+
tool_calls: [
73+
{
74+
id: 'call_456',
75+
type: 'function',
76+
function: {
77+
name: 'write_todos',
78+
arguments: JSON.stringify({ todos: [] }),
79+
},
80+
},
81+
],
82+
},
83+
{
84+
role: 'tool',
85+
tool_call_id: 'call_456',
86+
name: 'existing_name',
87+
content: '{}',
88+
},
89+
],
90+
tools: [
91+
{
92+
id: 'existing_tool_id',
93+
type: 'function',
94+
function: {
95+
name: 'write_todos',
96+
parameters: { type: 'object' },
97+
},
98+
},
99+
],
100+
}
101+
102+
expect(addKimiToolCompatibilityFields(body)).toEqual(body)
103+
})
104+
})

web/src/llm-api/canopywave.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
extractRequestMetadata,
1010
insertMessageToBigQuery,
1111
} from './helpers'
12+
import { addKimiToolCompatibilityFields } from './kimi-tool-compat'
1213

1314
import type { UsageData } from './helpers'
1415
import type { InsertMessageBigqueryFn } from '@codebuff/common/types/contracts/bigquery'
@@ -88,8 +89,11 @@ function createCanopyWaveRequest(params: {
8889
fetch: typeof globalThis.fetch
8990
}) {
9091
const { body, originalModel, fetch } = params
92+
const providerBody = originalModel.startsWith('moonshotai/')
93+
? addKimiToolCompatibilityFields(body)
94+
: body
9195
const canopywaveBody: Record<string, unknown> = {
92-
...body,
96+
...providerBody,
9397
model: getCanopyWaveModelId(originalModel),
9498
}
9599

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { ChatCompletionRequestBody } from './types'
2+
3+
function getToolCallNamesById(
4+
messages: ChatCompletionRequestBody['messages'],
5+
): Map<string, string> {
6+
const namesById = new Map<string, string>()
7+
8+
for (const message of messages) {
9+
if (message.role !== 'assistant') {
10+
continue
11+
}
12+
for (const toolCall of message.tool_calls ?? []) {
13+
if (toolCall.id && toolCall.function.name) {
14+
namesById.set(toolCall.id, toolCall.function.name)
15+
}
16+
}
17+
}
18+
19+
return namesById
20+
}
21+
22+
/**
23+
* Kimi-compatible providers require two OpenAI-compatible extensions that are
24+
* not part of the strict Chat Completions schema: ids on tool declarations and
25+
* names on tool-result messages.
26+
*/
27+
export function addKimiToolCompatibilityFields(
28+
body: ChatCompletionRequestBody,
29+
): ChatCompletionRequestBody {
30+
const namesByToolCallId = getToolCallNamesById(body.messages)
31+
32+
return {
33+
...body,
34+
tools: body.tools?.map((tool, index) => {
35+
if (tool.type !== 'function' || tool.id) {
36+
return tool
37+
}
38+
return {
39+
...tool,
40+
id: `tool_${index + 1}`,
41+
}
42+
}),
43+
messages: body.messages.map((message) => {
44+
if (
45+
message.role !== 'tool' ||
46+
message.name ||
47+
typeof message.tool_call_id !== 'string'
48+
) {
49+
return message
50+
}
51+
52+
const name = namesByToolCallId.get(message.tool_call_id)
53+
if (!name) {
54+
return message
55+
}
56+
57+
return {
58+
...message,
59+
name,
60+
}
61+
}),
62+
}
63+
}

web/src/llm-api/openrouter.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
extractRequestMetadata,
1010
insertMessageToBigQuery,
1111
} from './helpers'
12+
import { addKimiToolCompatibilityFields } from './kimi-tool-compat'
1213
import {
1314
OpenRouterErrorResponseSchema,
1415
OpenRouterStreamChatCompletionChunkSchema,
@@ -61,6 +62,10 @@ function createOpenRouterRequest(params: {
6162
fetch: typeof globalThis.fetch
6263
}) {
6364
const { body, openrouterApiKey, fetch } = params
65+
const providerBody = body.model.startsWith('moonshotai/')
66+
? addKimiToolCompatibilityFields(body)
67+
: body
68+
6469
return fetch('https://openrouter.ai/api/v1/chat/completions', {
6570
method: 'POST',
6671
headers: {
@@ -69,7 +74,7 @@ function createOpenRouterRequest(params: {
6974
'X-Title': 'Codebuff',
7075
'Content-Type': 'application/json',
7176
},
72-
body: JSON.stringify(body),
77+
body: JSON.stringify(providerBody),
7378
// Use custom agent with extended headers timeout for deep-thinking models
7479
// @ts-expect-error - dispatcher is a valid undici option not in fetch types
7580
dispatcher: openrouterAgent,

web/src/llm-api/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,21 @@ export interface ChatMessage {
2828
tool_call_id?: string
2929
}
3030

31+
export interface ChatCompletionTool {
32+
id?: string
33+
type: string
34+
function?: {
35+
name: string
36+
description?: string
37+
parameters?: unknown
38+
strict?: boolean
39+
}
40+
}
41+
3142
export interface ChatCompletionRequestBody {
3243
model: string
3344
messages: ChatMessage[]
45+
tools?: ChatCompletionTool[]
3446
stream?: boolean
3547
temperature?: number
3648
max_tokens?: number

0 commit comments

Comments
 (0)