kimi-vercel-ai-sdk-provider 0.2.0 → 0.4.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.
@@ -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 include strict when provided', () => {
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
- expect(result.tools?.[0]).toMatchObject({
242
- type: 'function',
243
- function: {
244
- name: 'test',
245
- strict: true
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
 
@@ -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: maxOutputTokens,
212
- temperature,
230
+ max_tokens: resolvedMaxTokens,
231
+ temperature: resolvedTemperature,
213
232
  top_p: topP,
214
233
  frequency_penalty: frequencyPenalty,
215
234
  presence_penalty: presencePenalty,
@@ -765,6 +784,26 @@ const kimiTokenUsageSchema = z
765
784
  })
766
785
  .nullish();
767
786
 
787
+ /**
788
+ * Schema for content parts in response messages.
789
+ * Can be text, image, or other content types.
790
+ */
791
+ const kimiContentPartSchema = z.union([
792
+ z.object({
793
+ type: z.literal('text'),
794
+ text: z.string()
795
+ }),
796
+ z.object({
797
+ type: z.literal('image_url'),
798
+ image_url: z.object({
799
+ url: z.string()
800
+ })
801
+ }),
802
+ z.looseObject({
803
+ type: z.string()
804
+ })
805
+ ]);
806
+
768
807
  const kimiChatResponseSchema = z.looseObject({
769
808
  id: z.string().nullish(),
770
809
  created: z.number().nullish(),
@@ -773,7 +812,7 @@ const kimiChatResponseSchema = z.looseObject({
773
812
  z.object({
774
813
  message: z.object({
775
814
  role: z.string().nullish(),
776
- content: z.union([z.string(), z.array(z.any())]).nullish(),
815
+ content: z.union([z.string(), z.array(kimiContentPartSchema)]).nullish(),
777
816
  reasoning_content: z.string().nullish(),
778
817
  reasoning: z.string().nullish(),
779
818
  tool_calls: z
@@ -19,7 +19,7 @@ export const kimiErrorSchema = z.union([
19
19
  error: z.object({
20
20
  message: z.string(),
21
21
  type: z.string().nullish(),
22
- param: z.any().nullish(),
22
+ param: z.string().nullish(),
23
23
  code: z.union([z.string(), z.number()]).nullish(),
24
24
  request_id: z.string().nullish()
25
25
  })
package/src/core/index.ts CHANGED
@@ -13,7 +13,7 @@ export type {
13
13
  KimiTokenUsage
14
14
  } from './types';
15
15
  // Utilities
16
- export type { KimiExtendedUsage } from './utils';
16
+ export type { KimiExtendedUsage, ReasoningAnalysis } from './utils';
17
17
  // Errors
18
18
  export {
19
19
  KimiAuthenticationError,
@@ -26,11 +26,18 @@ export {
26
26
  kimiErrorSchema,
27
27
  kimiFailedResponseHandler
28
28
  } from './errors';
29
- export { inferModelCapabilities } from './types';
30
29
  export {
30
+ STANDARD_MODEL_DEFAULT_MAX_TOKENS,
31
+ THINKING_MODEL_DEFAULT_MAX_TOKENS,
32
+ THINKING_MODEL_TEMPERATURE,
33
+ inferModelCapabilities
34
+ } from './types';
35
+ export {
36
+ analyzeReasoningPreservation,
31
37
  convertKimiUsage,
32
38
  extractMessageContent,
33
39
  getKimiRequestId,
34
40
  getResponseMetadata,
35
- mapKimiFinishReason
41
+ mapKimiFinishReason,
42
+ recommendThinkingModel
36
43
  } from './utils';
package/src/core/types.ts CHANGED
@@ -70,18 +70,68 @@ export interface KimiModelCapabilities {
70
70
  * Whether the model supports structured outputs.
71
71
  */
72
72
  structuredOutputs?: boolean;
73
+
74
+ /**
75
+ * Default temperature for the model.
76
+ * Thinking models require temperature=1.0 for optimal reasoning.
77
+ */
78
+ defaultTemperature?: number;
79
+
80
+ /**
81
+ * Whether temperature is locked (cannot be changed).
82
+ * Thinking models have this set to true.
83
+ */
84
+ temperatureLocked?: boolean;
85
+
86
+ /**
87
+ * Default max output tokens for the model.
88
+ * Thinking models need higher limits to avoid truncated reasoning.
89
+ */
90
+ defaultMaxOutputTokens?: number;
73
91
  }
74
92
 
93
+ /**
94
+ * Default temperature for thinking models.
95
+ * Kimi thinking models require temperature=1.0 for optimal reasoning quality.
96
+ */
97
+ export const THINKING_MODEL_TEMPERATURE = 1.0;
98
+
99
+ /**
100
+ * Default max output tokens for thinking models.
101
+ * Higher limit ensures reasoning traces aren't truncated.
102
+ */
103
+ export const THINKING_MODEL_DEFAULT_MAX_TOKENS = 32768;
104
+
105
+ /**
106
+ * Default max output tokens for standard models.
107
+ */
108
+ export const STANDARD_MODEL_DEFAULT_MAX_TOKENS = 4096;
109
+
75
110
  /**
76
111
  * Infer model capabilities from the model ID.
77
112
  *
78
113
  * @param modelId - The model identifier
79
114
  * @returns Inferred capabilities based on model name patterns
80
115
  *
116
+ * @remarks
117
+ * This function automatically detects model capabilities and sets
118
+ * appropriate defaults:
119
+ * - Thinking models (`-thinking` suffix) get temperature=1.0 locked
120
+ * - Thinking models get 32k default max_tokens to avoid truncation
121
+ * - K2.5 models get video input support
122
+ *
81
123
  * @example
82
124
  * ```ts
83
125
  * const caps = inferModelCapabilities('kimi-k2.5-thinking');
84
- * // { thinking: true, alwaysThinking: true, videoInput: true, ... }
126
+ * // {
127
+ * // thinking: true,
128
+ * // alwaysThinking: true,
129
+ * // videoInput: true,
130
+ * // temperatureLocked: true,
131
+ * // defaultTemperature: 1.0,
132
+ * // defaultMaxOutputTokens: 32768,
133
+ * // ...
134
+ * // }
85
135
  * ```
86
136
  */
87
137
  export function inferModelCapabilities(modelId: string): KimiModelCapabilities {
@@ -96,7 +146,12 @@ export function inferModelCapabilities(modelId: string): KimiModelCapabilities {
96
146
  maxContextSize: 256_000, // 256k context window
97
147
  toolCalling: true,
98
148
  jsonMode: true,
99
- structuredOutputs: true
149
+ structuredOutputs: true,
150
+ // Thinking models require temperature=1.0 for optimal reasoning
151
+ defaultTemperature: isThinkingModel ? THINKING_MODEL_TEMPERATURE : undefined,
152
+ temperatureLocked: isThinkingModel,
153
+ // Thinking models need higher token limits to avoid truncated reasoning
154
+ defaultMaxOutputTokens: isThinkingModel ? THINKING_MODEL_DEFAULT_MAX_TOKENS : STANDARD_MODEL_DEFAULT_MAX_TOKENS
100
155
  };
101
156
  }
102
157
 
package/src/core/utils.ts CHANGED
@@ -208,3 +208,141 @@ export function extractMessageContent(message: {
208
208
 
209
209
  return { text, reasoning };
210
210
  }
211
+
212
+ // ============================================================================
213
+ // Multi-turn Reasoning Utilities
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Information about reasoning content in a conversation.
218
+ */
219
+ export interface ReasoningAnalysis {
220
+ /** Total number of messages with reasoning content */
221
+ messagesWithReasoning: number;
222
+ /** Total reasoning tokens (estimated by character count / 4) */
223
+ estimatedReasoningTokens: number;
224
+ /** Whether reasoning is properly preserved in the conversation */
225
+ isPreserved: boolean;
226
+ /** Messages that are missing expected reasoning content */
227
+ missingReasoningIndices: number[];
228
+ }
229
+
230
+ /**
231
+ * Analyze reasoning content preservation in a conversation.
232
+ *
233
+ * This utility helps verify that reasoning content is being properly
234
+ * preserved across multi-turn conversations with thinking models.
235
+ * Kimi requires reasoning content to be maintained in the message
236
+ * history for logical continuity in agentic/tool-calling scenarios.
237
+ *
238
+ * @param messages - Array of messages to analyze
239
+ * @returns Analysis of reasoning preservation
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * const analysis = analyzeReasoningPreservation(messages);
244
+ * if (!analysis.isPreserved) {
245
+ * console.warn('Reasoning content missing from messages:', analysis.missingReasoningIndices);
246
+ * }
247
+ * ```
248
+ */
249
+ export function analyzeReasoningPreservation(
250
+ messages: Array<{
251
+ role: string;
252
+ content?: unknown;
253
+ reasoning_content?: string | null;
254
+ reasoning?: string | null;
255
+ }>
256
+ ): ReasoningAnalysis {
257
+ let messagesWithReasoning = 0;
258
+ let totalReasoningChars = 0;
259
+ const missingReasoningIndices: number[] = [];
260
+
261
+ // Track whether we've seen a tool call that should have reasoning preserved
262
+ let expectReasoningAfterToolCall = false;
263
+
264
+ for (let i = 0; i < messages.length; i++) {
265
+ const message = messages[i];
266
+
267
+ if (message.role === 'assistant') {
268
+ const { reasoning } = extractMessageContent(message);
269
+
270
+ if (reasoning.length > 0) {
271
+ messagesWithReasoning++;
272
+ totalReasoningChars += reasoning.length;
273
+ expectReasoningAfterToolCall = false;
274
+ } else if (expectReasoningAfterToolCall) {
275
+ // This assistant message should have reasoning from the previous turn
276
+ missingReasoningIndices.push(i);
277
+ }
278
+
279
+ // Check if this message has tool calls
280
+ if ('tool_calls' in message && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
281
+ expectReasoningAfterToolCall = true;
282
+ }
283
+ } else if (message.role === 'tool') {
284
+ // After a tool response, we expect the next assistant message to potentially have reasoning
285
+ expectReasoningAfterToolCall = true;
286
+ }
287
+ }
288
+
289
+ return {
290
+ messagesWithReasoning,
291
+ estimatedReasoningTokens: Math.ceil(totalReasoningChars / 4),
292
+ isPreserved: missingReasoningIndices.length === 0,
293
+ missingReasoningIndices
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Check if a conversation is suitable for thinking models.
299
+ *
300
+ * Thinking models work best with:
301
+ * - Complex reasoning tasks
302
+ * - Multi-step problem solving
303
+ * - Tasks requiring chain-of-thought
304
+ *
305
+ * This helper provides guidance on whether a thinking model would benefit
306
+ * the conversation.
307
+ *
308
+ * @param messageCount - Number of messages in the conversation
309
+ * @param hasToolCalls - Whether the conversation includes tool calls
310
+ * @param estimatedComplexity - Estimated task complexity (0-1)
311
+ * @returns Recommendation on using thinking models
312
+ */
313
+ export function recommendThinkingModel(
314
+ messageCount: number,
315
+ hasToolCalls: boolean,
316
+ estimatedComplexity: number
317
+ ): { recommended: boolean; reason: string } {
318
+ // Thinking models are recommended for:
319
+ // 1. Complex tasks (complexity > 0.5)
320
+ // 2. Agentic scenarios with tool calls
321
+ // 3. Multi-turn conversations where reasoning continuity matters
322
+
323
+ if (estimatedComplexity > 0.7) {
324
+ return {
325
+ recommended: true,
326
+ reason: 'High complexity task benefits from extended reasoning'
327
+ };
328
+ }
329
+
330
+ if (hasToolCalls && messageCount > 2) {
331
+ return {
332
+ recommended: true,
333
+ reason: 'Multi-turn tool usage benefits from reasoning preservation'
334
+ };
335
+ }
336
+
337
+ if (estimatedComplexity > 0.5) {
338
+ return {
339
+ recommended: true,
340
+ reason: 'Moderate complexity may benefit from reasoning'
341
+ };
342
+ }
343
+
344
+ return {
345
+ recommended: false,
346
+ reason: 'Standard model sufficient for this task'
347
+ };
348
+ }