kimi-vercel-ai-sdk-provider 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +567 -17
- package/dist/index.d.mts +1750 -3
- package/dist/index.d.ts +1750 -3
- package/dist/index.js +2317 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2292 -160
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/auto-detect.test.ts +140 -0
- package/src/__tests__/code-validation.test.ts +267 -0
- package/src/__tests__/ensemble.test.ts +242 -0
- package/src/__tests__/file-cache.test.ts +310 -0
- package/src/__tests__/model-config.test.ts +120 -0
- package/src/__tests__/multi-agent.test.ts +201 -0
- package/src/__tests__/project-tools.test.ts +181 -0
- package/src/__tests__/reasoning-utils.test.ts +164 -0
- package/src/__tests__/tools.test.ts +76 -8
- package/src/chat/kimi-chat-language-model.ts +21 -2
- package/src/chat/kimi-chat-settings.ts +15 -1
- package/src/code-validation/detector.ts +319 -0
- package/src/code-validation/index.ts +31 -0
- package/src/code-validation/types.ts +291 -0
- package/src/code-validation/validator.ts +547 -0
- package/src/core/errors.ts +91 -0
- package/src/core/index.ts +15 -3
- package/src/core/types.ts +57 -2
- package/src/core/utils.ts +138 -0
- package/src/ensemble/index.ts +17 -0
- package/src/ensemble/multi-sampler.ts +433 -0
- package/src/ensemble/types.ts +279 -0
- package/src/files/attachment-processor.ts +51 -4
- package/src/files/file-cache.ts +260 -0
- package/src/files/index.ts +16 -1
- package/src/index.ts +102 -3
- package/src/kimi-provider.ts +354 -1
- package/src/multi-agent/index.ts +21 -0
- package/src/multi-agent/types.ts +312 -0
- package/src/multi-agent/workflows.ts +539 -0
- package/src/project-tools/index.ts +16 -0
- package/src/project-tools/scaffolder.ts +494 -0
- package/src/project-tools/types.ts +244 -0
- package/src/tools/auto-detect.ts +276 -0
- package/src/tools/index.ts +6 -2
- package/src/tools/prepare-tools.ts +179 -4
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ProjectScaffolder, createEmptyScaffoldResult } from '../project-tools/scaffolder';
|
|
3
|
+
|
|
4
|
+
describe('Project Tools', () => {
|
|
5
|
+
describe('ProjectScaffolder', () => {
|
|
6
|
+
const createMockGenerator = (response: string) => vi.fn().mockResolvedValue({ text: response });
|
|
7
|
+
|
|
8
|
+
it('scaffolds a project from JSON response', async () => {
|
|
9
|
+
const jsonResponse = `
|
|
10
|
+
Here's your project:
|
|
11
|
+
|
|
12
|
+
\`\`\`json
|
|
13
|
+
{
|
|
14
|
+
"projectName": "my-api",
|
|
15
|
+
"projectType": "express",
|
|
16
|
+
"technologies": ["Express.js", "TypeScript"],
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"path": "package.json",
|
|
20
|
+
"content": "{\\"name\\": \\"my-api\\"}",
|
|
21
|
+
"description": "Package manifest"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "src/index.ts",
|
|
25
|
+
"content": "import express from 'express';",
|
|
26
|
+
"description": "Entry point"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"setupCommands": ["npm install", "npm run dev"],
|
|
30
|
+
"estimatedSetupTime": "5 minutes"
|
|
31
|
+
}
|
|
32
|
+
\`\`\`
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const scaffolder = new ProjectScaffolder({
|
|
36
|
+
generateText: createMockGenerator(jsonResponse)
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = await scaffolder.scaffold('A REST API for todos', {
|
|
40
|
+
type: 'express'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.files).toHaveLength(2);
|
|
44
|
+
expect(result.files[0].path).toBe('package.json');
|
|
45
|
+
expect(result.metadata.projectName).toBe('my-api');
|
|
46
|
+
expect(result.metadata.projectType).toBe('express');
|
|
47
|
+
expect(result.setupCommands).toContain('npm install');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('extracts files from code blocks when JSON parsing fails', async () => {
|
|
51
|
+
const codeBlockResponse = `
|
|
52
|
+
Here are your files:
|
|
53
|
+
|
|
54
|
+
\`\`\`package.json
|
|
55
|
+
{
|
|
56
|
+
"name": "test-project"
|
|
57
|
+
}
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
\`\`\`src/index.ts
|
|
61
|
+
console.log("Hello");
|
|
62
|
+
\`\`\`
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
const scaffolder = new ProjectScaffolder({
|
|
66
|
+
generateText: createMockGenerator(codeBlockResponse)
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const result = await scaffolder.scaffold('A simple project', {
|
|
70
|
+
type: 'node'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.files.length).toBeGreaterThan(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('includes configuration options in prompt', async () => {
|
|
77
|
+
let capturedPrompt = '';
|
|
78
|
+
const scaffolder = new ProjectScaffolder({
|
|
79
|
+
generateText: vi.fn().mockImplementation(async (prompt: string) => {
|
|
80
|
+
capturedPrompt = prompt;
|
|
81
|
+
return { text: '{"files": [], "projectName": "test"}' };
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await scaffolder.scaffold('A project', {
|
|
86
|
+
type: 'nextjs',
|
|
87
|
+
includeTests: true,
|
|
88
|
+
includeCI: true,
|
|
89
|
+
includeDocker: true,
|
|
90
|
+
includeDocs: true,
|
|
91
|
+
includeLinting: true,
|
|
92
|
+
useTypeScript: true
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(capturedPrompt).toContain('nextjs');
|
|
96
|
+
expect(capturedPrompt).toContain('test files');
|
|
97
|
+
expect(capturedPrompt).toContain('GitHub Actions');
|
|
98
|
+
expect(capturedPrompt).toContain('Dockerfile');
|
|
99
|
+
expect(capturedPrompt).toContain('README');
|
|
100
|
+
expect(capturedPrompt).toContain('ESLint');
|
|
101
|
+
expect(capturedPrompt).toContain('TypeScript');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('generates instructions', async () => {
|
|
105
|
+
const scaffolder = new ProjectScaffolder({
|
|
106
|
+
generateText: createMockGenerator(`{
|
|
107
|
+
"projectName": "my-project",
|
|
108
|
+
"projectType": "node",
|
|
109
|
+
"files": [{"path": "index.js", "content": "// code"}],
|
|
110
|
+
"setupCommands": ["npm install"],
|
|
111
|
+
"estimatedSetupTime": "2 minutes"
|
|
112
|
+
}`)
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const result = await scaffolder.scaffold('A project', {});
|
|
116
|
+
|
|
117
|
+
expect(result.instructions).toContain('Quick Start');
|
|
118
|
+
expect(result.instructions).toContain('my-project');
|
|
119
|
+
expect(result.instructions).toContain('npm install');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles custom template', async () => {
|
|
123
|
+
let capturedPrompt = '';
|
|
124
|
+
const scaffolder = new ProjectScaffolder({
|
|
125
|
+
generateText: vi.fn().mockImplementation(async (prompt: string) => {
|
|
126
|
+
capturedPrompt = prompt;
|
|
127
|
+
return { text: '{"files": []}' };
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await scaffolder.scaffold('A project', {
|
|
132
|
+
customTemplate: 'Must use Redux and React Query'
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(capturedPrompt).toContain('Redux');
|
|
136
|
+
expect(capturedPrompt).toContain('React Query');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('detects project type from files', async () => {
|
|
140
|
+
const scaffolder = new ProjectScaffolder({
|
|
141
|
+
generateText: createMockGenerator(`{
|
|
142
|
+
"files": [
|
|
143
|
+
{"path": "package.json", "content": "{\\"dependencies\\": {\\"react\\": \\"^18.0.0\\"}}"},
|
|
144
|
+
{"path": "src/App.tsx", "content": "export const App = () => <div>Hello</div>"}
|
|
145
|
+
]
|
|
146
|
+
}`)
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = await scaffolder.scaffold('A project', { type: 'auto' });
|
|
150
|
+
|
|
151
|
+
expect(result.metadata.projectType).toBe('react');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('returns empty files for instructions-only format', async () => {
|
|
155
|
+
const scaffolder = new ProjectScaffolder({
|
|
156
|
+
generateText: createMockGenerator(`{
|
|
157
|
+
"projectName": "test",
|
|
158
|
+
"files": [{"path": "index.js", "content": "code"}]
|
|
159
|
+
}`)
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = await scaffolder.scaffold('A project', {
|
|
163
|
+
outputFormat: 'instructions'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(result.files).toHaveLength(0);
|
|
167
|
+
expect(result.instructions).toBeTruthy();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('createEmptyScaffoldResult', () => {
|
|
172
|
+
it('creates empty result with error message', () => {
|
|
173
|
+
const result = createEmptyScaffoldResult('Failed to generate project');
|
|
174
|
+
|
|
175
|
+
expect(result.files).toHaveLength(0);
|
|
176
|
+
expect(result.instructions).toContain('Failed to generate project');
|
|
177
|
+
expect(result.setupCommands).toHaveLength(0);
|
|
178
|
+
expect(result.metadata.fileCount).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for multi-turn reasoning utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { analyzeReasoningPreservation, recommendThinkingModel } from '../core';
|
|
7
|
+
|
|
8
|
+
describe('analyzeReasoningPreservation', () => {
|
|
9
|
+
it('should detect messages with reasoning content', () => {
|
|
10
|
+
const messages = [
|
|
11
|
+
{ role: 'user', content: 'What is 2+2?' },
|
|
12
|
+
{
|
|
13
|
+
role: 'assistant',
|
|
14
|
+
content: 'The answer is 4.',
|
|
15
|
+
reasoning_content: 'Let me calculate: 2+2 = 4'
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
20
|
+
|
|
21
|
+
expect(analysis.messagesWithReasoning).toBe(1);
|
|
22
|
+
expect(analysis.isPreserved).toBe(true);
|
|
23
|
+
expect(analysis.missingReasoningIndices).toHaveLength(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should handle reasoning field variant', () => {
|
|
27
|
+
const messages = [
|
|
28
|
+
{ role: 'user', content: 'Test' },
|
|
29
|
+
{
|
|
30
|
+
role: 'assistant',
|
|
31
|
+
content: 'Answer',
|
|
32
|
+
reasoning: 'My reasoning process...'
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
37
|
+
|
|
38
|
+
expect(analysis.messagesWithReasoning).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should estimate reasoning tokens', () => {
|
|
42
|
+
const messages = [
|
|
43
|
+
{ role: 'user', content: 'Test' },
|
|
44
|
+
{
|
|
45
|
+
role: 'assistant',
|
|
46
|
+
content: 'Answer',
|
|
47
|
+
reasoning_content: 'A'.repeat(400) // 400 chars ≈ 100 tokens
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
52
|
+
|
|
53
|
+
expect(analysis.estimatedReasoningTokens).toBe(100);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should detect missing reasoning after tool calls', () => {
|
|
57
|
+
const messages = [
|
|
58
|
+
{ role: 'user', content: 'Search for X' },
|
|
59
|
+
{
|
|
60
|
+
role: 'assistant',
|
|
61
|
+
content: null,
|
|
62
|
+
reasoning_content: 'I need to search...',
|
|
63
|
+
tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'search', arguments: '{}' } }]
|
|
64
|
+
},
|
|
65
|
+
{ role: 'tool', tool_call_id: 'call_1', content: 'Search results...' },
|
|
66
|
+
{
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
content: 'Here are the results', // Missing reasoning!
|
|
69
|
+
reasoning_content: null
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
74
|
+
|
|
75
|
+
expect(analysis.isPreserved).toBe(false);
|
|
76
|
+
expect(analysis.missingReasoningIndices).toContain(3);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should handle proper reasoning preservation in tool loops', () => {
|
|
80
|
+
const messages = [
|
|
81
|
+
{ role: 'user', content: 'Search for X' },
|
|
82
|
+
{
|
|
83
|
+
role: 'assistant',
|
|
84
|
+
content: null,
|
|
85
|
+
reasoning_content: 'I need to search...',
|
|
86
|
+
tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'search', arguments: '{}' } }]
|
|
87
|
+
},
|
|
88
|
+
{ role: 'tool', tool_call_id: 'call_1', content: 'Search results...' },
|
|
89
|
+
{
|
|
90
|
+
role: 'assistant',
|
|
91
|
+
content: 'Here are the results',
|
|
92
|
+
reasoning_content: 'Based on the search results, I can now answer...'
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
97
|
+
|
|
98
|
+
expect(analysis.isPreserved).toBe(true);
|
|
99
|
+
expect(analysis.messagesWithReasoning).toBe(2);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle empty conversations', () => {
|
|
103
|
+
const analysis = analyzeReasoningPreservation([]);
|
|
104
|
+
|
|
105
|
+
expect(analysis.messagesWithReasoning).toBe(0);
|
|
106
|
+
expect(analysis.isPreserved).toBe(true);
|
|
107
|
+
expect(analysis.estimatedReasoningTokens).toBe(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle conversations without assistant messages', () => {
|
|
111
|
+
const messages = [
|
|
112
|
+
{ role: 'user', content: 'Hello' },
|
|
113
|
+
{ role: 'system', content: 'You are helpful' }
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const analysis = analyzeReasoningPreservation(messages);
|
|
117
|
+
|
|
118
|
+
expect(analysis.messagesWithReasoning).toBe(0);
|
|
119
|
+
expect(analysis.isPreserved).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('recommendThinkingModel', () => {
|
|
124
|
+
it('should recommend for high complexity tasks', () => {
|
|
125
|
+
const recommendation = recommendThinkingModel(1, false, 0.8);
|
|
126
|
+
|
|
127
|
+
expect(recommendation.recommended).toBe(true);
|
|
128
|
+
expect(recommendation.reason).toContain('High complexity');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should recommend for multi-turn tool usage', () => {
|
|
132
|
+
const recommendation = recommendThinkingModel(5, true, 0.3);
|
|
133
|
+
|
|
134
|
+
expect(recommendation.recommended).toBe(true);
|
|
135
|
+
expect(recommendation.reason).toContain('tool usage');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should recommend for moderate complexity', () => {
|
|
139
|
+
const recommendation = recommendThinkingModel(1, false, 0.6);
|
|
140
|
+
|
|
141
|
+
expect(recommendation.recommended).toBe(true);
|
|
142
|
+
expect(recommendation.reason).toContain('Moderate complexity');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should not recommend for simple tasks', () => {
|
|
146
|
+
const recommendation = recommendThinkingModel(1, false, 0.2);
|
|
147
|
+
|
|
148
|
+
expect(recommendation.recommended).toBe(false);
|
|
149
|
+
expect(recommendation.reason).toContain('Standard model sufficient');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should not recommend for short tool conversations', () => {
|
|
153
|
+
const recommendation = recommendThinkingModel(2, true, 0.3);
|
|
154
|
+
|
|
155
|
+
expect(recommendation.recommended).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should prioritize high complexity over other factors', () => {
|
|
159
|
+
const recommendation = recommendThinkingModel(1, false, 0.9);
|
|
160
|
+
|
|
161
|
+
expect(recommendation.recommended).toBe(true);
|
|
162
|
+
expect(recommendation.reason).toContain('High complexity');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -225,7 +225,8 @@ describe('prepareKimiTools', () => {
|
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
-
it('should
|
|
228
|
+
it('should not pass strict mode to Kimi for better compatibility', () => {
|
|
229
|
+
// Kimi doesn't fully support strict mode, so we don't pass it
|
|
229
230
|
const result = prepareKimiTools({
|
|
230
231
|
tools: [
|
|
231
232
|
{
|
|
@@ -238,13 +239,80 @@ describe('prepareKimiTools', () => {
|
|
|
238
239
|
]
|
|
239
240
|
});
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
242
|
+
// Strict should not be present in the output
|
|
243
|
+
const tool = result.tools?.[0];
|
|
244
|
+
expect(tool).toBeDefined();
|
|
245
|
+
if (tool && 'function' in tool) {
|
|
246
|
+
expect(tool.function).not.toHaveProperty('strict');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should sanitize JSON schema by removing unsupported keywords', () => {
|
|
251
|
+
const result = prepareKimiTools({
|
|
252
|
+
tools: [
|
|
253
|
+
{
|
|
254
|
+
type: 'function',
|
|
255
|
+
name: 'test',
|
|
256
|
+
description: 'Test tool',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
259
|
+
$id: 'test-schema',
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
name: { type: 'string' }
|
|
263
|
+
},
|
|
264
|
+
$defs: { unused: { type: 'string' } },
|
|
265
|
+
$comment: 'This is a comment'
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
]
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const tool = result.tools?.[0];
|
|
272
|
+
expect(tool).toBeDefined();
|
|
273
|
+
expect(tool?.type).toBe('function');
|
|
274
|
+
if (tool && tool.type === 'function') {
|
|
275
|
+
const params = tool.function.parameters as Record<string, unknown>;
|
|
276
|
+
expect(params).not.toHaveProperty('$schema');
|
|
277
|
+
expect(params).not.toHaveProperty('$id');
|
|
278
|
+
expect(params).not.toHaveProperty('$defs');
|
|
279
|
+
expect(params).not.toHaveProperty('$comment');
|
|
280
|
+
expect(params.type).toBe('object');
|
|
281
|
+
expect(params.properties).toEqual({ name: { type: 'string' } });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should preserve basic schema properties', () => {
|
|
286
|
+
const result = prepareKimiTools({
|
|
287
|
+
tools: [
|
|
288
|
+
{
|
|
289
|
+
type: 'function',
|
|
290
|
+
name: 'test',
|
|
291
|
+
description: 'Test tool',
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: 'object',
|
|
294
|
+
properties: {
|
|
295
|
+
name: { type: 'string', description: 'Name field' },
|
|
296
|
+
count: { type: 'number', minimum: 0 }
|
|
297
|
+
},
|
|
298
|
+
required: ['name']
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
]
|
|
247
302
|
});
|
|
303
|
+
|
|
304
|
+
const tool = result.tools?.[0];
|
|
305
|
+
expect(tool).toBeDefined();
|
|
306
|
+
expect(tool?.type).toBe('function');
|
|
307
|
+
if (tool && tool.type === 'function') {
|
|
308
|
+
const params = tool.function.parameters as Record<string, unknown>;
|
|
309
|
+
expect(params.type).toBe('object');
|
|
310
|
+
expect(params.required).toEqual(['name']);
|
|
311
|
+
expect((params.properties as Record<string, unknown>).name).toEqual({
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Name field'
|
|
314
|
+
});
|
|
315
|
+
}
|
|
248
316
|
});
|
|
249
317
|
});
|
|
250
318
|
|
|
@@ -394,7 +462,7 @@ describe('prepareKimiTools', () => {
|
|
|
394
462
|
details: 'Using tool choice polyfill with system message injection.'
|
|
395
463
|
});
|
|
396
464
|
expect(result.toolChoiceSystemMessage).toBeDefined();
|
|
397
|
-
expect(result.toolChoiceSystemMessage).toContain('MUST use one of the available tools');
|
|
465
|
+
expect(result.toolChoiceSystemMessage).toContain('MUST use at least one of the available tools');
|
|
398
466
|
});
|
|
399
467
|
|
|
400
468
|
it('should warn and fallback to auto for required tool choice without polyfill', () => {
|
|
@@ -205,11 +205,30 @@ export class KimiChatLanguageModel implements LanguageModelV3 {
|
|
|
205
205
|
messages.unshift({ role: 'system', content: toolChoiceSystemMessage });
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// Apply model-specific defaults and constraints
|
|
209
|
+
const caps = this.capabilities;
|
|
210
|
+
|
|
211
|
+
// Resolve temperature: thinking models require locked temperature
|
|
212
|
+
let resolvedTemperature = temperature;
|
|
213
|
+
if (caps.temperatureLocked && caps.defaultTemperature !== undefined) {
|
|
214
|
+
if (temperature !== undefined && temperature !== caps.defaultTemperature) {
|
|
215
|
+
warnings.push({
|
|
216
|
+
type: 'compatibility',
|
|
217
|
+
feature: 'temperature',
|
|
218
|
+
details: `Thinking models require temperature=${caps.defaultTemperature}. Your value (${temperature}) will be overridden.`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
resolvedTemperature = caps.defaultTemperature;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Resolve max_tokens: use model default if not specified
|
|
225
|
+
const resolvedMaxTokens = maxOutputTokens ?? caps.defaultMaxOutputTokens;
|
|
226
|
+
|
|
208
227
|
const body = removeUndefinedEntries({
|
|
209
228
|
model: this.modelId,
|
|
210
229
|
messages,
|
|
211
|
-
max_tokens:
|
|
212
|
-
temperature,
|
|
230
|
+
max_tokens: resolvedMaxTokens,
|
|
231
|
+
temperature: resolvedTemperature,
|
|
213
232
|
top_p: topP,
|
|
214
233
|
frequency_penalty: frequencyPenalty,
|
|
215
234
|
presence_penalty: presencePenalty,
|
|
@@ -116,6 +116,15 @@ export interface KimiChatSettings {
|
|
|
116
116
|
* Reduces costs by up to 90% for repeated long prompts.
|
|
117
117
|
*/
|
|
118
118
|
caching?: boolean | KimiCachingConfig;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Auto-enable tools based on prompt content analysis.
|
|
122
|
+
* When true, the provider will analyze the prompt and automatically
|
|
123
|
+
* enable webSearch or codeInterpreter if patterns match.
|
|
124
|
+
*
|
|
125
|
+
* @default false
|
|
126
|
+
*/
|
|
127
|
+
autoEnableTools?: boolean;
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
// ============================================================================
|
|
@@ -207,7 +216,12 @@ export const kimiProviderOptionsSchema = z.object({
|
|
|
207
216
|
/**
|
|
208
217
|
* Enable tool choice polyfill for this request.
|
|
209
218
|
*/
|
|
210
|
-
toolChoicePolyfill: z.boolean().optional()
|
|
219
|
+
toolChoicePolyfill: z.boolean().optional(),
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Auto-enable tools based on prompt content analysis.
|
|
223
|
+
*/
|
|
224
|
+
autoEnableTools: z.boolean().optional()
|
|
211
225
|
});
|
|
212
226
|
|
|
213
227
|
/**
|