kimi-vercel-ai-sdk-provider 0.2.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.
Files changed (44) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +871 -0
  3. package/dist/index.d.mts +1317 -0
  4. package/dist/index.d.ts +1317 -0
  5. package/dist/index.js +2764 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/index.mjs +2734 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +70 -0
  10. package/src/__tests__/caching.test.ts +97 -0
  11. package/src/__tests__/chat.test.ts +386 -0
  12. package/src/__tests__/code-integration.test.ts +562 -0
  13. package/src/__tests__/code-provider.test.ts +289 -0
  14. package/src/__tests__/code.test.ts +427 -0
  15. package/src/__tests__/core.test.ts +172 -0
  16. package/src/__tests__/files.test.ts +185 -0
  17. package/src/__tests__/integration.test.ts +457 -0
  18. package/src/__tests__/provider.test.ts +188 -0
  19. package/src/__tests__/tools.test.ts +519 -0
  20. package/src/chat/index.ts +42 -0
  21. package/src/chat/kimi-chat-language-model.ts +829 -0
  22. package/src/chat/kimi-chat-messages.ts +297 -0
  23. package/src/chat/kimi-chat-response.ts +84 -0
  24. package/src/chat/kimi-chat-settings.ts +216 -0
  25. package/src/code/index.ts +66 -0
  26. package/src/code/kimi-code-language-model.ts +669 -0
  27. package/src/code/kimi-code-messages.ts +303 -0
  28. package/src/code/kimi-code-provider.ts +239 -0
  29. package/src/code/kimi-code-settings.ts +193 -0
  30. package/src/code/kimi-code-types.ts +354 -0
  31. package/src/core/errors.ts +140 -0
  32. package/src/core/index.ts +36 -0
  33. package/src/core/types.ts +148 -0
  34. package/src/core/utils.ts +210 -0
  35. package/src/files/attachment-processor.ts +276 -0
  36. package/src/files/file-utils.ts +257 -0
  37. package/src/files/index.ts +24 -0
  38. package/src/files/kimi-file-client.ts +292 -0
  39. package/src/index.ts +122 -0
  40. package/src/kimi-provider.ts +263 -0
  41. package/src/tools/builtin-tools.ts +273 -0
  42. package/src/tools/index.ts +33 -0
  43. package/src/tools/prepare-tools.ts +306 -0
  44. package/src/version.ts +4 -0
@@ -0,0 +1,289 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import {
3
+ createKimiCode,
4
+ kimiCode,
5
+ KimiCodeLanguageModel,
6
+ KIMI_CODE_BASE_URL,
7
+ KIMI_CODE_DEFAULT_MODEL,
8
+ KIMI_CODE_THINKING_MODEL,
9
+ inferKimiCodeCapabilities
10
+ } from '../code';
11
+
12
+ describe('createKimiCode', () => {
13
+ const originalEnv = process.env;
14
+
15
+ beforeEach(() => {
16
+ vi.resetModules();
17
+ process.env = { ...originalEnv };
18
+ });
19
+
20
+ afterEach(() => {
21
+ process.env = originalEnv;
22
+ });
23
+
24
+ describe('API key handling', () => {
25
+ it('should use provided apiKey', () => {
26
+ const provider = createKimiCode({ apiKey: 'sk-test-key' });
27
+ const model = provider();
28
+
29
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
30
+ expect(model.modelId).toBe(KIMI_CODE_DEFAULT_MODEL);
31
+ });
32
+
33
+ it('should use KIMI_CODE_API_KEY env var', () => {
34
+ process.env.KIMI_CODE_API_KEY = 'sk-env-key';
35
+
36
+ const provider = createKimiCode();
37
+ const model = provider();
38
+
39
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
40
+ });
41
+
42
+ it('should fallback to KIMI_API_KEY env var', () => {
43
+ delete process.env.KIMI_CODE_API_KEY;
44
+ process.env.KIMI_API_KEY = 'sk-fallback-key';
45
+
46
+ const provider = createKimiCode();
47
+ const model = provider();
48
+
49
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
50
+ });
51
+
52
+ it('should throw error when no API key is available', () => {
53
+ delete process.env.KIMI_CODE_API_KEY;
54
+ delete process.env.KIMI_API_KEY;
55
+
56
+ const provider = createKimiCode();
57
+ const model = provider();
58
+
59
+ // The error is thrown when headers are accessed, not when creating the model
60
+ // This happens during actual API calls
61
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
62
+ });
63
+ });
64
+
65
+ describe('model creation', () => {
66
+ it('should create model with default model ID', () => {
67
+ const provider = createKimiCode({ apiKey: 'sk-test' });
68
+ const model = provider();
69
+
70
+ expect(model.modelId).toBe(KIMI_CODE_DEFAULT_MODEL);
71
+ });
72
+
73
+ it('should create model with specified model ID', () => {
74
+ const provider = createKimiCode({ apiKey: 'sk-test' });
75
+ const model = provider(KIMI_CODE_THINKING_MODEL);
76
+
77
+ expect(model.modelId).toBe(KIMI_CODE_THINKING_MODEL);
78
+ });
79
+
80
+ it('should create model with custom model ID', () => {
81
+ const provider = createKimiCode({ apiKey: 'sk-test' });
82
+ const model = provider('custom-model');
83
+
84
+ expect(model.modelId).toBe('custom-model');
85
+ });
86
+
87
+ it('should pass settings to model', () => {
88
+ const provider = createKimiCode({ apiKey: 'sk-test' });
89
+ const model = provider(KIMI_CODE_DEFAULT_MODEL, {
90
+ extendedThinking: { enabled: true, effort: 'high' }
91
+ }) as KimiCodeLanguageModel;
92
+
93
+ expect(model.capabilities.extendedThinking).toBe(false); // inferred from model ID
94
+ });
95
+
96
+ it('should override capabilities when provided in settings', () => {
97
+ const provider = createKimiCode({ apiKey: 'sk-test' });
98
+ const model = provider(KIMI_CODE_DEFAULT_MODEL, {
99
+ capabilities: { extendedThinking: true }
100
+ }) as KimiCodeLanguageModel;
101
+
102
+ expect(model.capabilities.extendedThinking).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe('provider interface', () => {
107
+ it('should have specificationVersion v3', () => {
108
+ const provider = createKimiCode({ apiKey: 'sk-test' });
109
+
110
+ expect(provider.specificationVersion).toBe('v3');
111
+ });
112
+
113
+ it('should have languageModel method', () => {
114
+ const provider = createKimiCode({ apiKey: 'sk-test' });
115
+
116
+ expect(typeof provider.languageModel).toBe('function');
117
+ });
118
+
119
+ it('should have chat method (alias)', () => {
120
+ const provider = createKimiCode({ apiKey: 'sk-test' });
121
+
122
+ expect(typeof provider.chat).toBe('function');
123
+ });
124
+
125
+ it('should throw NoSuchModelError for embeddingModel', () => {
126
+ const provider = createKimiCode({ apiKey: 'sk-test' });
127
+
128
+ expect(() => provider.embeddingModel('test')).toThrow('No such embeddingModel');
129
+ });
130
+
131
+ it('should throw NoSuchModelError for imageModel', () => {
132
+ const provider = createKimiCode({ apiKey: 'sk-test' });
133
+
134
+ expect(() => provider.imageModel('test')).toThrow('No such imageModel');
135
+ });
136
+
137
+ it('should throw NoSuchModelError for rerankingModel', () => {
138
+ const provider = createKimiCode({ apiKey: 'sk-test' });
139
+
140
+ expect(() => provider.rerankingModel!('test')).toThrow('No such rerankingModel');
141
+ });
142
+
143
+ it('should throw when called with new keyword', () => {
144
+ const provider = createKimiCode({ apiKey: 'sk-test' });
145
+
146
+ expect(() => new (provider as unknown as new () => unknown)()).toThrow();
147
+ });
148
+ });
149
+
150
+ describe('base URL handling', () => {
151
+ it('should use default base URL', () => {
152
+ const provider = createKimiCode({ apiKey: 'sk-test' });
153
+ const model = provider();
154
+
155
+ expect(model.provider).toBe('kimi.code');
156
+ });
157
+
158
+ it('should use custom base URL', () => {
159
+ const provider = createKimiCode({
160
+ apiKey: 'sk-test',
161
+ baseURL: 'https://custom.api.com/v1'
162
+ });
163
+ const model = provider();
164
+
165
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
166
+ });
167
+
168
+ it('should use KIMI_CODE_BASE_URL env var', () => {
169
+ process.env.KIMI_CODE_BASE_URL = 'https://env.api.com/v1';
170
+
171
+ const provider = createKimiCode({ apiKey: 'sk-test' });
172
+ const model = provider();
173
+
174
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
175
+ });
176
+ });
177
+
178
+ describe('custom fetch', () => {
179
+ it('should accept custom fetch function', () => {
180
+ const customFetch = vi.fn();
181
+ const provider = createKimiCode({
182
+ apiKey: 'sk-test',
183
+ fetch: customFetch
184
+ });
185
+ const model = provider();
186
+
187
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
188
+ });
189
+ });
190
+
191
+ describe('generateId', () => {
192
+ it('should accept custom generateId function', () => {
193
+ const customGenerateId = vi.fn(() => 'custom-id');
194
+ const provider = createKimiCode({
195
+ apiKey: 'sk-test',
196
+ generateId: customGenerateId
197
+ });
198
+ const model = provider();
199
+
200
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
201
+ });
202
+ });
203
+ });
204
+
205
+ describe('kimiCode default instance', () => {
206
+ const originalEnv = process.env;
207
+
208
+ beforeEach(() => {
209
+ process.env = { ...originalEnv };
210
+ process.env.KIMI_CODE_API_KEY = 'sk-default-test';
211
+ });
212
+
213
+ afterEach(() => {
214
+ process.env = originalEnv;
215
+ });
216
+
217
+ it('should be a function', () => {
218
+ expect(typeof kimiCode).toBe('function');
219
+ });
220
+
221
+ it('should create model with default settings', () => {
222
+ const model = kimiCode() as KimiCodeLanguageModel;
223
+
224
+ expect(model).toBeInstanceOf(KimiCodeLanguageModel);
225
+ expect(model.modelId).toBe(KIMI_CODE_DEFAULT_MODEL);
226
+ });
227
+
228
+ it('should create thinking model', () => {
229
+ const model = kimiCode(KIMI_CODE_THINKING_MODEL) as KimiCodeLanguageModel;
230
+
231
+ expect(model.modelId).toBe(KIMI_CODE_THINKING_MODEL);
232
+ expect(model.capabilities.extendedThinking).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('KimiCodeLanguageModel', () => {
237
+ it('should have specificationVersion v3', () => {
238
+ const provider = createKimiCode({ apiKey: 'sk-test' });
239
+ const model = provider() as KimiCodeLanguageModel;
240
+
241
+ expect(model.specificationVersion).toBe('v3');
242
+ });
243
+
244
+ it('should have provider name', () => {
245
+ const provider = createKimiCode({ apiKey: 'sk-test' });
246
+ const model = provider() as KimiCodeLanguageModel;
247
+
248
+ expect(model.provider).toBe('kimi.code');
249
+ });
250
+
251
+ it('should infer capabilities for default model', () => {
252
+ const caps = inferKimiCodeCapabilities(KIMI_CODE_DEFAULT_MODEL);
253
+
254
+ expect(caps.extendedThinking).toBe(false);
255
+ expect(caps.maxOutputTokens).toBe(32768);
256
+ expect(caps.maxContextSize).toBe(262144);
257
+ expect(caps.streaming).toBe(true);
258
+ expect(caps.toolCalling).toBe(true);
259
+ expect(caps.imageInput).toBe(true);
260
+ });
261
+
262
+ it('should infer capabilities for thinking model', () => {
263
+ const caps = inferKimiCodeCapabilities(KIMI_CODE_THINKING_MODEL);
264
+
265
+ expect(caps.extendedThinking).toBe(true);
266
+ expect(caps.maxOutputTokens).toBe(32768);
267
+ expect(caps.maxContextSize).toBe(262144);
268
+ });
269
+
270
+ it('should have supportedUrls for images', async () => {
271
+ const provider = createKimiCode({ apiKey: 'sk-test' });
272
+ const model = provider() as KimiCodeLanguageModel;
273
+
274
+ const urls = await model.supportedUrls;
275
+ expect(urls).toBeDefined();
276
+ expect(urls['image/*']).toBeDefined();
277
+ });
278
+
279
+ it('should allow overriding supportedUrls in settings', async () => {
280
+ const customPatterns = { 'image/*': [/^https:\/\/example\.com/] };
281
+ const provider = createKimiCode({ apiKey: 'sk-test' });
282
+ const model = provider(KIMI_CODE_DEFAULT_MODEL, {
283
+ supportedUrls: customPatterns
284
+ }) as KimiCodeLanguageModel;
285
+
286
+ const urls = await model.supportedUrls;
287
+ expect(urls).toEqual(customPatterns);
288
+ });
289
+ });
@@ -0,0 +1,427 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ convertToKimiCodePrompt,
4
+ inferKimiCodeCapabilities,
5
+ normalizeExtendedThinkingConfig,
6
+ effortToBudgetTokens,
7
+ toAnthropicThinking,
8
+ KIMI_CODE_BASE_URL,
9
+ KIMI_CODE_DEFAULT_MODEL,
10
+ KIMI_CODE_THINKING_MODEL,
11
+ KIMI_CODE_MODELS,
12
+ KIMI_CODE_OPENAI_BASE_URL,
13
+ KIMI_CODE_DEFAULT_MAX_TOKENS,
14
+ KIMI_CODE_DEFAULT_CONTEXT_WINDOW,
15
+ KIMI_CODE_ANTHROPIC_VERSION
16
+ } from '../code';
17
+
18
+ describe('inferKimiCodeCapabilities', () => {
19
+ it('should detect kimi-for-coding model capabilities', () => {
20
+ const caps = inferKimiCodeCapabilities('kimi-for-coding');
21
+ expect(caps.extendedThinking).toBe(false);
22
+ expect(caps.maxOutputTokens).toBe(32768);
23
+ expect(caps.streaming).toBe(true);
24
+ expect(caps.toolCalling).toBe(true);
25
+ });
26
+
27
+ it('should detect kimi-k2-thinking model capabilities', () => {
28
+ const caps = inferKimiCodeCapabilities('kimi-k2-thinking');
29
+ expect(caps.extendedThinking).toBe(true);
30
+ expect(caps.maxOutputTokens).toBe(32768);
31
+ });
32
+
33
+ it('should handle unknown model', () => {
34
+ const caps = inferKimiCodeCapabilities('unknown-model');
35
+ expect(caps.extendedThinking).toBe(false);
36
+ expect(caps.maxOutputTokens).toBe(32768);
37
+ expect(caps.maxContextSize).toBe(262144);
38
+ });
39
+
40
+ it('should detect thinking model by name pattern', () => {
41
+ const caps = inferKimiCodeCapabilities('custom-thinking-model');
42
+ expect(caps.extendedThinking).toBe(true);
43
+ });
44
+ });
45
+
46
+ describe('normalizeExtendedThinkingConfig', () => {
47
+ it('should return undefined for undefined input', () => {
48
+ expect(normalizeExtendedThinkingConfig(undefined)).toBeUndefined();
49
+ });
50
+
51
+ it('should normalize boolean true to enabled config with default effort', () => {
52
+ const result = normalizeExtendedThinkingConfig(true);
53
+ expect(result).toEqual({ enabled: true, effort: 'medium' });
54
+ });
55
+
56
+ it('should normalize boolean false to disabled config', () => {
57
+ const result = normalizeExtendedThinkingConfig(false);
58
+ expect(result).toEqual({ enabled: false });
59
+ });
60
+
61
+ it('should pass through object config with defaults', () => {
62
+ const config = { enabled: true, budgetTokens: 10000 };
63
+ const result = normalizeExtendedThinkingConfig(config);
64
+ expect(result?.enabled).toBe(true);
65
+ expect(result?.budgetTokens).toBe(10000);
66
+ expect(result?.effort).toBe('medium'); // default
67
+ });
68
+
69
+ it('should preserve custom effort', () => {
70
+ const config = { enabled: true, effort: 'high' as const };
71
+ const result = normalizeExtendedThinkingConfig(config);
72
+ expect(result?.effort).toBe('high');
73
+ });
74
+ });
75
+
76
+ describe('effortToBudgetTokens', () => {
77
+ it('should return 2048 for low effort', () => {
78
+ expect(effortToBudgetTokens('low')).toBe(2048);
79
+ });
80
+
81
+ it('should return 8192 for medium effort', () => {
82
+ expect(effortToBudgetTokens('medium')).toBe(8192);
83
+ });
84
+
85
+ it('should return 16384 for high effort', () => {
86
+ expect(effortToBudgetTokens('high')).toBe(16384);
87
+ });
88
+ });
89
+
90
+ describe('Kimi Code Constants', () => {
91
+ it('should have correct base URL', () => {
92
+ expect(KIMI_CODE_BASE_URL).toBe('https://api.kimi.com/coding/v1');
93
+ });
94
+
95
+ it('should have correct OpenAI base URL', () => {
96
+ expect(KIMI_CODE_OPENAI_BASE_URL).toBe('https://api.kimi.com/coding/v1');
97
+ });
98
+
99
+ it('should have correct default model', () => {
100
+ expect(KIMI_CODE_DEFAULT_MODEL).toBe('kimi-for-coding');
101
+ });
102
+
103
+ it('should have correct thinking model', () => {
104
+ expect(KIMI_CODE_THINKING_MODEL).toBe('kimi-k2-thinking');
105
+ });
106
+
107
+ it('should have correct model list', () => {
108
+ expect(KIMI_CODE_MODELS).toContain('kimi-for-coding');
109
+ expect(KIMI_CODE_MODELS).toContain('kimi-k2-thinking');
110
+ expect(KIMI_CODE_MODELS).toHaveLength(2);
111
+ });
112
+
113
+ it('should have correct default max tokens (per Roo Code docs)', () => {
114
+ expect(KIMI_CODE_DEFAULT_MAX_TOKENS).toBe(32768);
115
+ });
116
+
117
+ it('should have correct default context window (per Roo Code docs)', () => {
118
+ expect(KIMI_CODE_DEFAULT_CONTEXT_WINDOW).toBe(262144);
119
+ });
120
+
121
+ it('should have correct Anthropic version', () => {
122
+ expect(KIMI_CODE_ANTHROPIC_VERSION).toBe('2023-06-01');
123
+ });
124
+ });
125
+
126
+ describe('toAnthropicThinking', () => {
127
+ it('should return undefined for undefined input', () => {
128
+ expect(toAnthropicThinking(undefined)).toBeUndefined();
129
+ });
130
+
131
+ it('should return enabled with default budget for boolean true', () => {
132
+ const result = toAnthropicThinking(true);
133
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 8192 });
134
+ });
135
+
136
+ it('should return disabled for boolean false', () => {
137
+ const result = toAnthropicThinking(false);
138
+ expect(result).toEqual({ type: 'disabled' });
139
+ });
140
+
141
+ it('should return disabled for config with enabled: false', () => {
142
+ const result = toAnthropicThinking({ enabled: false });
143
+ expect(result).toEqual({ type: 'disabled' });
144
+ });
145
+
146
+ it('should use low effort budget tokens', () => {
147
+ const result = toAnthropicThinking({ enabled: true, effort: 'low' });
148
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 2048 });
149
+ });
150
+
151
+ it('should use medium effort budget tokens', () => {
152
+ const result = toAnthropicThinking({ enabled: true, effort: 'medium' });
153
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 8192 });
154
+ });
155
+
156
+ it('should use high effort budget tokens', () => {
157
+ const result = toAnthropicThinking({ enabled: true, effort: 'high' });
158
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 16384 });
159
+ });
160
+
161
+ it('should prioritize explicit budgetTokens over effort', () => {
162
+ const result = toAnthropicThinking({ enabled: true, effort: 'low', budgetTokens: 10000 });
163
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 10000 });
164
+ });
165
+
166
+ it('should use default medium effort when no effort specified', () => {
167
+ const result = toAnthropicThinking({ enabled: true });
168
+ expect(result).toEqual({ type: 'enabled', budget_tokens: 8192 });
169
+ });
170
+ });
171
+
172
+ describe('convertToKimiCodePrompt', () => {
173
+ it('should convert simple user message', async () => {
174
+ const result = await convertToKimiCodePrompt([
175
+ { role: 'user', content: [{ type: 'text', text: 'Hello!' }] }
176
+ ]);
177
+
178
+ expect(result.system).toBeUndefined();
179
+ expect(result.messages).toEqual([{ role: 'user', content: 'Hello!' }]);
180
+ });
181
+
182
+ it('should extract system message', async () => {
183
+ const result = await convertToKimiCodePrompt([
184
+ { role: 'system', content: 'You are a coding assistant.' },
185
+ { role: 'user', content: [{ type: 'text', text: 'Help me write code' }] }
186
+ ]);
187
+
188
+ expect(result.system).toBe('You are a coding assistant.');
189
+ expect(result.messages).toEqual([{ role: 'user', content: 'Help me write code' }]);
190
+ });
191
+
192
+ it('should combine multiple system messages', async () => {
193
+ const result = await convertToKimiCodePrompt([
194
+ { role: 'system', content: 'First instruction' },
195
+ { role: 'system', content: 'Second instruction' },
196
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] }
197
+ ]);
198
+
199
+ expect(result.system).toBe('First instruction\n\nSecond instruction');
200
+ });
201
+
202
+ it('should convert assistant message with text', async () => {
203
+ const result = await convertToKimiCodePrompt([
204
+ { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
205
+ { role: 'assistant', content: [{ type: 'text', text: 'Hi there!' }] }
206
+ ]);
207
+
208
+ expect(result.messages).toHaveLength(2);
209
+ expect(result.messages[1]).toEqual({ role: 'assistant', content: 'Hi there!' });
210
+ });
211
+
212
+ it('should convert assistant message with tool call', async () => {
213
+ const result = await convertToKimiCodePrompt([
214
+ { role: 'user', content: [{ type: 'text', text: 'Calculate 2+2' }] },
215
+ {
216
+ role: 'assistant',
217
+ content: [
218
+ {
219
+ type: 'tool-call',
220
+ toolCallId: 'call-123',
221
+ toolName: 'calculator',
222
+ input: { a: 2, b: 2 }
223
+ }
224
+ ]
225
+ }
226
+ ]);
227
+
228
+ expect(result.messages).toHaveLength(2);
229
+ expect(result.messages[1]).toEqual({
230
+ role: 'assistant',
231
+ content: [
232
+ {
233
+ type: 'tool_use',
234
+ id: 'call-123',
235
+ name: 'calculator',
236
+ input: { a: 2, b: 2 }
237
+ }
238
+ ]
239
+ });
240
+ });
241
+
242
+ it('should convert tool result as user message after assistant', async () => {
243
+ const result = await convertToKimiCodePrompt([
244
+ { role: 'user', content: [{ type: 'text', text: 'Calculate' }] },
245
+ {
246
+ role: 'assistant',
247
+ content: [
248
+ {
249
+ type: 'tool-call',
250
+ toolCallId: 'call-123',
251
+ toolName: 'calculator',
252
+ input: { a: 2, b: 2 }
253
+ }
254
+ ]
255
+ },
256
+ {
257
+ role: 'tool',
258
+ content: [
259
+ {
260
+ type: 'tool-result',
261
+ toolCallId: 'call-123',
262
+ toolName: 'calculator',
263
+ output: { type: 'text', value: '4' }
264
+ }
265
+ ]
266
+ }
267
+ ]);
268
+
269
+ expect(result.messages).toHaveLength(3);
270
+ expect(result.messages[2]).toEqual({
271
+ role: 'user',
272
+ content: [
273
+ {
274
+ type: 'tool_result',
275
+ tool_use_id: 'call-123',
276
+ content: '4',
277
+ is_error: undefined
278
+ }
279
+ ]
280
+ });
281
+ });
282
+
283
+ it('should handle tool result with error', async () => {
284
+ const result = await convertToKimiCodePrompt([
285
+ { role: 'user', content: [{ type: 'text', text: 'Calculate' }] },
286
+ {
287
+ role: 'assistant',
288
+ content: [
289
+ {
290
+ type: 'tool-call',
291
+ toolCallId: 'call-123',
292
+ toolName: 'calculator',
293
+ input: { a: 2, b: 2 }
294
+ }
295
+ ]
296
+ },
297
+ {
298
+ role: 'tool',
299
+ content: [
300
+ {
301
+ type: 'tool-result',
302
+ toolCallId: 'call-123',
303
+ toolName: 'calculator',
304
+ output: { type: 'error-text', value: 'Error: Division by zero' }
305
+ }
306
+ ]
307
+ }
308
+ ]);
309
+
310
+ expect(result.messages[2]).toEqual({
311
+ role: 'user',
312
+ content: [
313
+ {
314
+ type: 'tool_result',
315
+ tool_use_id: 'call-123',
316
+ content: 'Error: Division by zero',
317
+ is_error: true
318
+ }
319
+ ]
320
+ });
321
+ });
322
+
323
+ it('should convert user message with image URL', async () => {
324
+ const result = await convertToKimiCodePrompt([
325
+ {
326
+ role: 'user',
327
+ content: [
328
+ { type: 'text', text: 'What is in this image?' },
329
+ {
330
+ type: 'file',
331
+ mediaType: 'image/png',
332
+ data: new URL('https://example.com/image.png')
333
+ }
334
+ ]
335
+ }
336
+ ]);
337
+
338
+ expect(result.messages).toEqual([
339
+ {
340
+ role: 'user',
341
+ content: [
342
+ { type: 'text', text: 'What is in this image?' },
343
+ {
344
+ type: 'image',
345
+ source: { type: 'url', url: 'https://example.com/image.png' }
346
+ }
347
+ ]
348
+ }
349
+ ]);
350
+ });
351
+
352
+ it('should convert user message with base64 image', async () => {
353
+ const result = await convertToKimiCodePrompt([
354
+ {
355
+ role: 'user',
356
+ content: [
357
+ { type: 'text', text: 'What is this?' },
358
+ {
359
+ type: 'file',
360
+ mediaType: 'image/jpeg',
361
+ data: 'SGVsbG8gV29ybGQ=' // base64 of "Hello World"
362
+ }
363
+ ]
364
+ }
365
+ ]);
366
+
367
+ expect(result.messages).toEqual([
368
+ {
369
+ role: 'user',
370
+ content: [
371
+ { type: 'text', text: 'What is this?' },
372
+ {
373
+ type: 'image',
374
+ source: {
375
+ type: 'base64',
376
+ media_type: 'image/jpeg',
377
+ data: 'SGVsbG8gV29ybGQ='
378
+ }
379
+ }
380
+ ]
381
+ }
382
+ ]);
383
+ });
384
+
385
+ it('should handle assistant message with reasoning', async () => {
386
+ const result = await convertToKimiCodePrompt([
387
+ { role: 'user', content: [{ type: 'text', text: 'Think about this' }] },
388
+ {
389
+ role: 'assistant',
390
+ content: [
391
+ { type: 'reasoning', text: 'Let me think...' },
392
+ { type: 'text', text: 'Here is my answer' }
393
+ ]
394
+ }
395
+ ]);
396
+
397
+ expect(result.messages[1]).toEqual({
398
+ role: 'assistant',
399
+ content: [
400
+ { type: 'text', text: '<thinking>Let me think...</thinking>' },
401
+ { type: 'text', text: 'Here is my answer' }
402
+ ]
403
+ });
404
+ });
405
+
406
+ it('should handle multipart user message with text only', async () => {
407
+ const result = await convertToKimiCodePrompt([
408
+ {
409
+ role: 'user',
410
+ content: [
411
+ { type: 'text', text: 'Part 1' },
412
+ { type: 'text', text: 'Part 2' }
413
+ ]
414
+ }
415
+ ]);
416
+
417
+ expect(result.messages).toEqual([
418
+ {
419
+ role: 'user',
420
+ content: [
421
+ { type: 'text', text: 'Part 1' },
422
+ { type: 'text', text: 'Part 2' }
423
+ ]
424
+ }
425
+ ]);
426
+ });
427
+ });