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.
- package/LICENSE +198 -0
- package/README.md +871 -0
- package/dist/index.d.mts +1317 -0
- package/dist/index.d.ts +1317 -0
- package/dist/index.js +2764 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2734 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
- package/src/__tests__/caching.test.ts +97 -0
- package/src/__tests__/chat.test.ts +386 -0
- package/src/__tests__/code-integration.test.ts +562 -0
- package/src/__tests__/code-provider.test.ts +289 -0
- package/src/__tests__/code.test.ts +427 -0
- package/src/__tests__/core.test.ts +172 -0
- package/src/__tests__/files.test.ts +185 -0
- package/src/__tests__/integration.test.ts +457 -0
- package/src/__tests__/provider.test.ts +188 -0
- package/src/__tests__/tools.test.ts +519 -0
- package/src/chat/index.ts +42 -0
- package/src/chat/kimi-chat-language-model.ts +829 -0
- package/src/chat/kimi-chat-messages.ts +297 -0
- package/src/chat/kimi-chat-response.ts +84 -0
- package/src/chat/kimi-chat-settings.ts +216 -0
- package/src/code/index.ts +66 -0
- package/src/code/kimi-code-language-model.ts +669 -0
- package/src/code/kimi-code-messages.ts +303 -0
- package/src/code/kimi-code-provider.ts +239 -0
- package/src/code/kimi-code-settings.ts +193 -0
- package/src/code/kimi-code-types.ts +354 -0
- package/src/core/errors.ts +140 -0
- package/src/core/index.ts +36 -0
- package/src/core/types.ts +148 -0
- package/src/core/utils.ts +210 -0
- package/src/files/attachment-processor.ts +276 -0
- package/src/files/file-utils.ts +257 -0
- package/src/files/index.ts +24 -0
- package/src/files/kimi-file-client.ts +292 -0
- package/src/index.ts +122 -0
- package/src/kimi-provider.ts +263 -0
- package/src/tools/builtin-tools.ts +273 -0
- package/src/tools/index.ts +33 -0
- package/src/tools/prepare-tools.ts +306 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { createKimiCode } from '../code';
|
|
3
|
+
|
|
4
|
+
// Mock fetch for API testing
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
|
|
7
|
+
describe('KimiCodeLanguageModel Integration', () => {
|
|
8
|
+
const originalEnv = process.env;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.resetAllMocks();
|
|
12
|
+
process.env = { ...originalEnv };
|
|
13
|
+
process.env.KIMI_CODE_API_KEY = 'sk-test-key';
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
process.env = originalEnv;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('doGenerate', () => {
|
|
21
|
+
it('should make correct API request', async () => {
|
|
22
|
+
const mockResponse = {
|
|
23
|
+
id: 'msg_123',
|
|
24
|
+
type: 'message',
|
|
25
|
+
model: 'kimi-for-coding',
|
|
26
|
+
content: [{ type: 'text', text: 'Hello!' }],
|
|
27
|
+
stop_reason: 'end_turn',
|
|
28
|
+
usage: {
|
|
29
|
+
input_tokens: 10,
|
|
30
|
+
output_tokens: 5
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
mockFetch.mockResolvedValueOnce({
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
headers: new Headers({
|
|
38
|
+
'content-type': 'application/json',
|
|
39
|
+
'x-request-id': 'req-123'
|
|
40
|
+
}),
|
|
41
|
+
json: async () => mockResponse,
|
|
42
|
+
text: async () => JSON.stringify(mockResponse)
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const provider = createKimiCode({
|
|
46
|
+
apiKey: 'sk-test-key',
|
|
47
|
+
fetch: mockFetch
|
|
48
|
+
});
|
|
49
|
+
const model = provider('kimi-for-coding');
|
|
50
|
+
|
|
51
|
+
const result = await model.doGenerate({
|
|
52
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Hello' }] }]
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
56
|
+
const [url, options] = mockFetch.mock.calls[0];
|
|
57
|
+
|
|
58
|
+
expect(url).toBe('https://api.kimi.com/coding/v1/messages');
|
|
59
|
+
expect(options.method).toBe('POST');
|
|
60
|
+
expect(options.headers['x-api-key']).toBe('sk-test-key');
|
|
61
|
+
expect(options.headers['anthropic-version']).toBe('2023-06-01');
|
|
62
|
+
|
|
63
|
+
const body = JSON.parse(options.body);
|
|
64
|
+
expect(body.model).toBe('kimi-for-coding');
|
|
65
|
+
expect(body.messages).toEqual([{ role: 'user', content: 'Hello' }]);
|
|
66
|
+
|
|
67
|
+
expect(result.content).toEqual([{ type: 'text', text: 'Hello!' }]);
|
|
68
|
+
expect(result.finishReason.unified).toBe('stop');
|
|
69
|
+
expect(result.usage.inputTokens.total).toBe(10);
|
|
70
|
+
expect(result.usage.outputTokens.total).toBe(5);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle thinking blocks in response', async () => {
|
|
74
|
+
const mockResponse = {
|
|
75
|
+
id: 'msg_123',
|
|
76
|
+
type: 'message',
|
|
77
|
+
model: 'kimi-k2-thinking',
|
|
78
|
+
content: [
|
|
79
|
+
{ type: 'thinking', thinking: 'Let me think about this...' },
|
|
80
|
+
{ type: 'text', text: 'The answer is 42.' }
|
|
81
|
+
],
|
|
82
|
+
stop_reason: 'end_turn',
|
|
83
|
+
usage: { input_tokens: 20, output_tokens: 30 }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
mockFetch.mockResolvedValueOnce({
|
|
87
|
+
ok: true,
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
90
|
+
json: async () => mockResponse,
|
|
91
|
+
text: async () => JSON.stringify(mockResponse)
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const provider = createKimiCode({
|
|
95
|
+
apiKey: 'sk-test-key',
|
|
96
|
+
fetch: mockFetch
|
|
97
|
+
});
|
|
98
|
+
const model = provider('kimi-k2-thinking', {
|
|
99
|
+
extendedThinking: { enabled: true, effort: 'medium' }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = await model.doGenerate({
|
|
103
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'What is the meaning of life?' }] }]
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.content).toHaveLength(2);
|
|
107
|
+
expect(result.content[0]).toEqual({
|
|
108
|
+
type: 'reasoning',
|
|
109
|
+
text: 'Let me think about this...'
|
|
110
|
+
});
|
|
111
|
+
expect(result.content[1]).toEqual({
|
|
112
|
+
type: 'text',
|
|
113
|
+
text: 'The answer is 42.'
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle tool use in response', async () => {
|
|
118
|
+
const mockResponse = {
|
|
119
|
+
id: 'msg_123',
|
|
120
|
+
type: 'message',
|
|
121
|
+
model: 'kimi-for-coding',
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'tool_use',
|
|
125
|
+
id: 'tool_123',
|
|
126
|
+
name: 'read_file',
|
|
127
|
+
input: { path: '/src/index.ts' }
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
stop_reason: 'tool_use',
|
|
131
|
+
usage: { input_tokens: 15, output_tokens: 25 }
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
mockFetch.mockResolvedValueOnce({
|
|
135
|
+
ok: true,
|
|
136
|
+
status: 200,
|
|
137
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
138
|
+
json: async () => mockResponse,
|
|
139
|
+
text: async () => JSON.stringify(mockResponse)
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const provider = createKimiCode({
|
|
143
|
+
apiKey: 'sk-test-key',
|
|
144
|
+
fetch: mockFetch
|
|
145
|
+
});
|
|
146
|
+
const model = provider('kimi-for-coding');
|
|
147
|
+
|
|
148
|
+
const result = await model.doGenerate({
|
|
149
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Read the index file' }] }],
|
|
150
|
+
|
|
151
|
+
tools: [
|
|
152
|
+
{
|
|
153
|
+
type: 'function',
|
|
154
|
+
name: 'read_file',
|
|
155
|
+
description: 'Read a file',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: { path: { type: 'string' } }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.content).toHaveLength(1);
|
|
165
|
+
expect(result.content[0]).toEqual({
|
|
166
|
+
type: 'tool-call',
|
|
167
|
+
toolCallId: 'tool_123',
|
|
168
|
+
toolName: 'read_file',
|
|
169
|
+
input: JSON.stringify({ path: '/src/index.ts' })
|
|
170
|
+
});
|
|
171
|
+
expect(result.finishReason.unified).toBe('tool-calls');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should send thinking parameter when enabled', async () => {
|
|
175
|
+
const mockResponse = {
|
|
176
|
+
id: 'msg_123',
|
|
177
|
+
type: 'message',
|
|
178
|
+
model: 'kimi-for-coding',
|
|
179
|
+
content: [{ type: 'text', text: 'Done!' }],
|
|
180
|
+
stop_reason: 'end_turn',
|
|
181
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
mockFetch.mockResolvedValueOnce({
|
|
185
|
+
ok: true,
|
|
186
|
+
status: 200,
|
|
187
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
188
|
+
json: async () => mockResponse,
|
|
189
|
+
text: async () => JSON.stringify(mockResponse)
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const provider = createKimiCode({
|
|
193
|
+
apiKey: 'sk-test-key',
|
|
194
|
+
fetch: mockFetch
|
|
195
|
+
});
|
|
196
|
+
const model = provider('kimi-for-coding', {
|
|
197
|
+
extendedThinking: { enabled: true, effort: 'high' }
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await model.doGenerate({
|
|
201
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Think hard' }] }],
|
|
202
|
+
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
206
|
+
expect(body.thinking).toEqual({
|
|
207
|
+
type: 'enabled',
|
|
208
|
+
budget_tokens: 16384
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle cache token usage', async () => {
|
|
213
|
+
const mockResponse = {
|
|
214
|
+
id: 'msg_123',
|
|
215
|
+
type: 'message',
|
|
216
|
+
model: 'kimi-for-coding',
|
|
217
|
+
content: [{ type: 'text', text: 'Cached response!' }],
|
|
218
|
+
stop_reason: 'end_turn',
|
|
219
|
+
usage: {
|
|
220
|
+
input_tokens: 100,
|
|
221
|
+
output_tokens: 20,
|
|
222
|
+
cache_read_input_tokens: 80,
|
|
223
|
+
cache_creation_input_tokens: 10
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
mockFetch.mockResolvedValueOnce({
|
|
228
|
+
ok: true,
|
|
229
|
+
status: 200,
|
|
230
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
231
|
+
json: async () => mockResponse,
|
|
232
|
+
text: async () => JSON.stringify(mockResponse)
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const provider = createKimiCode({
|
|
236
|
+
apiKey: 'sk-test-key',
|
|
237
|
+
fetch: mockFetch
|
|
238
|
+
});
|
|
239
|
+
const model = provider('kimi-for-coding');
|
|
240
|
+
|
|
241
|
+
const result = await model.doGenerate({
|
|
242
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
|
|
243
|
+
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(result.usage.inputTokens.total).toBe(100);
|
|
247
|
+
expect(result.usage.inputTokens.cacheRead).toBe(80);
|
|
248
|
+
expect(result.usage.inputTokens.cacheWrite).toBe(10);
|
|
249
|
+
expect(result.usage.inputTokens.noCache).toBe(20);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle API errors', async () => {
|
|
253
|
+
const errorResponse = {
|
|
254
|
+
error: {
|
|
255
|
+
type: 'invalid_request_error',
|
|
256
|
+
message: 'Invalid API key'
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
mockFetch.mockResolvedValueOnce({
|
|
261
|
+
ok: false,
|
|
262
|
+
status: 401,
|
|
263
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
264
|
+
json: async () => errorResponse,
|
|
265
|
+
text: async () => JSON.stringify(errorResponse)
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const provider = createKimiCode({
|
|
269
|
+
apiKey: 'sk-invalid-key',
|
|
270
|
+
fetch: mockFetch
|
|
271
|
+
});
|
|
272
|
+
const model = provider('kimi-for-coding');
|
|
273
|
+
|
|
274
|
+
await expect(
|
|
275
|
+
model.doGenerate({
|
|
276
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
|
|
277
|
+
|
|
278
|
+
})
|
|
279
|
+
).rejects.toThrow('Invalid API key');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle max_tokens stop reason', async () => {
|
|
283
|
+
const mockResponse = {
|
|
284
|
+
id: 'msg_123',
|
|
285
|
+
type: 'message',
|
|
286
|
+
model: 'kimi-for-coding',
|
|
287
|
+
content: [{ type: 'text', text: 'Truncated...' }],
|
|
288
|
+
stop_reason: 'max_tokens',
|
|
289
|
+
usage: { input_tokens: 10, output_tokens: 100 }
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
mockFetch.mockResolvedValueOnce({
|
|
293
|
+
ok: true,
|
|
294
|
+
status: 200,
|
|
295
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
296
|
+
json: async () => mockResponse,
|
|
297
|
+
text: async () => JSON.stringify(mockResponse)
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const provider = createKimiCode({
|
|
301
|
+
apiKey: 'sk-test-key',
|
|
302
|
+
fetch: mockFetch
|
|
303
|
+
});
|
|
304
|
+
const model = provider('kimi-for-coding');
|
|
305
|
+
|
|
306
|
+
const result = await model.doGenerate({
|
|
307
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Write a long story' }] }],
|
|
308
|
+
|
|
309
|
+
maxOutputTokens: 100
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
expect(result.finishReason.unified).toBe('length');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('doStream', () => {
|
|
317
|
+
it('should handle streaming response', async () => {
|
|
318
|
+
// Create a mock SSE stream
|
|
319
|
+
const events = [
|
|
320
|
+
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_123","type":"message","model":"kimi-for-coding","content":[],"usage":{"input_tokens":10}}}\n\n',
|
|
321
|
+
'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}\n\n',
|
|
322
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}\n\n',
|
|
323
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" World!"}}\n\n',
|
|
324
|
+
'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
|
|
325
|
+
'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}\n\n',
|
|
326
|
+
'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
const encoder = new TextEncoder();
|
|
330
|
+
const stream = new ReadableStream({
|
|
331
|
+
start(controller) {
|
|
332
|
+
for (const event of events) {
|
|
333
|
+
controller.enqueue(encoder.encode(event));
|
|
334
|
+
}
|
|
335
|
+
controller.close();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
mockFetch.mockResolvedValueOnce({
|
|
340
|
+
ok: true,
|
|
341
|
+
status: 200,
|
|
342
|
+
headers: new Headers({ 'content-type': 'text/event-stream' }),
|
|
343
|
+
body: stream
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const provider = createKimiCode({
|
|
347
|
+
apiKey: 'sk-test-key',
|
|
348
|
+
fetch: mockFetch
|
|
349
|
+
});
|
|
350
|
+
const model = provider('kimi-for-coding');
|
|
351
|
+
|
|
352
|
+
const result = await model.doStream({
|
|
353
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Say hello' }] }],
|
|
354
|
+
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const parts: any[] = [];
|
|
358
|
+
for await (const part of result.stream) {
|
|
359
|
+
parts.push(part);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Verify streaming worked
|
|
363
|
+
expect(parts.length).toBeGreaterThan(0);
|
|
364
|
+
|
|
365
|
+
// Check for expected part types
|
|
366
|
+
const partTypes = parts.map((p) => p.type);
|
|
367
|
+
expect(partTypes).toContain('stream-start');
|
|
368
|
+
expect(partTypes).toContain('response-metadata');
|
|
369
|
+
expect(partTypes).toContain('text-start');
|
|
370
|
+
expect(partTypes).toContain('text-delta');
|
|
371
|
+
expect(partTypes).toContain('text-end');
|
|
372
|
+
expect(partTypes).toContain('finish');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should handle thinking blocks in stream', async () => {
|
|
376
|
+
const events = [
|
|
377
|
+
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_123","type":"message","model":"kimi-k2-thinking","content":[],"usage":{"input_tokens":10}}}\n\n',
|
|
378
|
+
'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}\n\n',
|
|
379
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Thinking..."}}\n\n',
|
|
380
|
+
'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
|
|
381
|
+
'event: content_block_start\ndata: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}\n\n',
|
|
382
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Answer!"}}\n\n',
|
|
383
|
+
'event: content_block_stop\ndata: {"type":"content_block_stop","index":1}\n\n',
|
|
384
|
+
'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":10}}\n\n',
|
|
385
|
+
'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
const encoder = new TextEncoder();
|
|
389
|
+
const stream = new ReadableStream({
|
|
390
|
+
start(controller) {
|
|
391
|
+
for (const event of events) {
|
|
392
|
+
controller.enqueue(encoder.encode(event));
|
|
393
|
+
}
|
|
394
|
+
controller.close();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
mockFetch.mockResolvedValueOnce({
|
|
399
|
+
ok: true,
|
|
400
|
+
status: 200,
|
|
401
|
+
headers: new Headers({ 'content-type': 'text/event-stream' }),
|
|
402
|
+
body: stream
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
const provider = createKimiCode({
|
|
406
|
+
apiKey: 'sk-test-key',
|
|
407
|
+
fetch: mockFetch
|
|
408
|
+
});
|
|
409
|
+
const model = provider('kimi-k2-thinking');
|
|
410
|
+
|
|
411
|
+
const result = await model.doStream({
|
|
412
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Think about this' }] }],
|
|
413
|
+
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const parts: any[] = [];
|
|
417
|
+
for await (const part of result.stream) {
|
|
418
|
+
parts.push(part);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const partTypes = parts.map((p) => p.type);
|
|
422
|
+
expect(partTypes).toContain('reasoning-start');
|
|
423
|
+
expect(partTypes).toContain('reasoning-delta');
|
|
424
|
+
expect(partTypes).toContain('reasoning-end');
|
|
425
|
+
expect(partTypes).toContain('text-start');
|
|
426
|
+
expect(partTypes).toContain('text-delta');
|
|
427
|
+
expect(partTypes).toContain('text-end');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle tool calls in stream', async () => {
|
|
431
|
+
const events = [
|
|
432
|
+
'event: message_start\ndata: {"type":"message_start","message":{"id":"msg_123","type":"message","model":"kimi-for-coding","content":[],"usage":{"input_tokens":10}}}\n\n',
|
|
433
|
+
'event: content_block_start\ndata: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"tool_123","name":"read_file"}}\n\n',
|
|
434
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\\"path\\":"}}\n\n',
|
|
435
|
+
'event: content_block_delta\ndata: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"\\"/src/index.ts\\"}"}}\n\n',
|
|
436
|
+
'event: content_block_stop\ndata: {"type":"content_block_stop","index":0}\n\n',
|
|
437
|
+
'event: message_delta\ndata: {"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":15}}\n\n',
|
|
438
|
+
'event: message_stop\ndata: {"type":"message_stop"}\n\n'
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
const encoder = new TextEncoder();
|
|
442
|
+
const stream = new ReadableStream({
|
|
443
|
+
start(controller) {
|
|
444
|
+
for (const event of events) {
|
|
445
|
+
controller.enqueue(encoder.encode(event));
|
|
446
|
+
}
|
|
447
|
+
controller.close();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
mockFetch.mockResolvedValueOnce({
|
|
452
|
+
ok: true,
|
|
453
|
+
status: 200,
|
|
454
|
+
headers: new Headers({ 'content-type': 'text/event-stream' }),
|
|
455
|
+
body: stream
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const provider = createKimiCode({
|
|
459
|
+
apiKey: 'sk-test-key',
|
|
460
|
+
fetch: mockFetch
|
|
461
|
+
});
|
|
462
|
+
const model = provider('kimi-for-coding');
|
|
463
|
+
|
|
464
|
+
const result = await model.doStream({
|
|
465
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Read the file' }] }],
|
|
466
|
+
|
|
467
|
+
tools: [
|
|
468
|
+
{
|
|
469
|
+
type: 'function',
|
|
470
|
+
name: 'read_file',
|
|
471
|
+
description: 'Read a file',
|
|
472
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } } }
|
|
473
|
+
}
|
|
474
|
+
]
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const parts: any[] = [];
|
|
478
|
+
for await (const part of result.stream) {
|
|
479
|
+
parts.push(part);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const partTypes = parts.map((p) => p.type);
|
|
483
|
+
expect(partTypes).toContain('tool-input-start');
|
|
484
|
+
expect(partTypes).toContain('tool-input-delta');
|
|
485
|
+
expect(partTypes).toContain('tool-input-end');
|
|
486
|
+
expect(partTypes).toContain('tool-call');
|
|
487
|
+
|
|
488
|
+
const toolCallPart = parts.find((p) => p.type === 'tool-call');
|
|
489
|
+
expect(toolCallPart.toolName).toBe('read_file');
|
|
490
|
+
expect(toolCallPart.input).toContain('/src/index.ts');
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe('request headers', () => {
|
|
495
|
+
it('should include Anthropic version header', async () => {
|
|
496
|
+
const mockResponse = {
|
|
497
|
+
id: 'msg_123',
|
|
498
|
+
type: 'message',
|
|
499
|
+
model: 'kimi-for-coding',
|
|
500
|
+
content: [{ type: 'text', text: 'OK' }],
|
|
501
|
+
stop_reason: 'end_turn',
|
|
502
|
+
usage: { input_tokens: 5, output_tokens: 2 }
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
mockFetch.mockResolvedValueOnce({
|
|
506
|
+
ok: true,
|
|
507
|
+
status: 200,
|
|
508
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
509
|
+
json: async () => mockResponse,
|
|
510
|
+
text: async () => JSON.stringify(mockResponse)
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const provider = createKimiCode({
|
|
514
|
+
apiKey: 'sk-test-key',
|
|
515
|
+
fetch: mockFetch
|
|
516
|
+
});
|
|
517
|
+
const model = provider('kimi-for-coding');
|
|
518
|
+
|
|
519
|
+
await model.doGenerate({
|
|
520
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
|
|
521
|
+
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
525
|
+
expect(headers['anthropic-version']).toBe('2023-06-01');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('should include custom headers', async () => {
|
|
529
|
+
const mockResponse = {
|
|
530
|
+
id: 'msg_123',
|
|
531
|
+
type: 'message',
|
|
532
|
+
model: 'kimi-for-coding',
|
|
533
|
+
content: [{ type: 'text', text: 'OK' }],
|
|
534
|
+
stop_reason: 'end_turn',
|
|
535
|
+
usage: { input_tokens: 5, output_tokens: 2 }
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
mockFetch.mockResolvedValueOnce({
|
|
539
|
+
ok: true,
|
|
540
|
+
status: 200,
|
|
541
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
542
|
+
json: async () => mockResponse,
|
|
543
|
+
text: async () => JSON.stringify(mockResponse)
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const provider = createKimiCode({
|
|
547
|
+
apiKey: 'sk-test-key',
|
|
548
|
+
headers: { 'x-custom-header': 'custom-value' },
|
|
549
|
+
fetch: mockFetch
|
|
550
|
+
});
|
|
551
|
+
const model = provider('kimi-for-coding');
|
|
552
|
+
|
|
553
|
+
await model.doGenerate({
|
|
554
|
+
prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }]
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
558
|
+
// Custom headers are passed through provider headers option
|
|
559
|
+
expect(headers['x-custom-header']).toBe('custom-value');
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
});
|