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,829 @@
1
+ /**
2
+ * Kimi chat language model implementation.
3
+ * @module
4
+ */
5
+
6
+ import {
7
+ InvalidResponseDataError,
8
+ type JSONValue,
9
+ type LanguageModelV3,
10
+ type LanguageModelV3CallOptions,
11
+ type LanguageModelV3Content,
12
+ type LanguageModelV3FinishReason,
13
+ type LanguageModelV3GenerateResult,
14
+ type LanguageModelV3StreamPart,
15
+ type LanguageModelV3StreamResult,
16
+ type SharedV3ProviderMetadata,
17
+ type SharedV3Warning
18
+ } from '@ai-sdk/provider';
19
+ import {
20
+ type ParseResult,
21
+ combineHeaders,
22
+ createEventSourceResponseHandler,
23
+ createJsonResponseHandler,
24
+ generateId,
25
+ isParsableJson,
26
+ parseProviderOptions,
27
+ postJsonToApi,
28
+ removeUndefinedEntries
29
+ } from '@ai-sdk/provider-utils';
30
+ import { z } from 'zod/v4';
31
+ import { kimiErrorSchema, kimiFailedResponseHandler } from '../core';
32
+ import { type KimiCodeInterpreterToolOptions, type KimiWebSearchToolOptions, prepareKimiTools } from '../tools';
33
+ import { convertToKimiChatMessages } from './kimi-chat-messages';
34
+ import {
35
+ convertKimiUsage,
36
+ extractCodeInterpreterTokens,
37
+ extractMessageContent,
38
+ extractWebSearchTokens,
39
+ getKimiRequestId,
40
+ getResponseMetadata,
41
+ mapKimiFinishReason
42
+ } from './kimi-chat-response';
43
+ import {
44
+ type KimiCachingConfig,
45
+ type KimiChatConfig,
46
+ type KimiChatModelId,
47
+ type KimiChatSettings,
48
+ type KimiProviderOptions,
49
+ inferModelCapabilities,
50
+ kimiProviderOptionsSchema
51
+ } from './kimi-chat-settings';
52
+
53
+ // ============================================================================
54
+ // Language Model Implementation
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Kimi chat language model implementing LanguageModelV3.
59
+ */
60
+ export class KimiChatLanguageModel implements LanguageModelV3 {
61
+ readonly specificationVersion = 'v3';
62
+ readonly modelId: KimiChatModelId;
63
+
64
+ private readonly config: KimiChatConfig;
65
+ private readonly settings: KimiChatSettings;
66
+ private readonly generateIdFn: () => string;
67
+ private readonly supportsStructuredOutputs: boolean;
68
+
69
+ constructor(modelId: KimiChatModelId, settings: KimiChatSettings, config: KimiChatConfig) {
70
+ this.modelId = modelId;
71
+ this.settings = settings;
72
+ this.config = config;
73
+ this.generateIdFn = config.generateId ?? generateId;
74
+ this.supportsStructuredOutputs = config.supportsStructuredOutputs ?? false;
75
+ }
76
+
77
+ get provider(): string {
78
+ return this.config.provider;
79
+ }
80
+
81
+ private get providerOptionsName(): string {
82
+ return this.config.provider.split('.')[0].trim();
83
+ }
84
+
85
+ /**
86
+ * Get the inferred or configured capabilities for this model.
87
+ */
88
+ get capabilities() {
89
+ const inferred = inferModelCapabilities(this.modelId);
90
+ return {
91
+ ...inferred,
92
+ ...this.settings.capabilities
93
+ };
94
+ }
95
+
96
+ get supportedUrls() {
97
+ const caps = this.capabilities;
98
+ const patterns: Record<string, RegExp[]> = {
99
+ 'image/*': [/^https?:\/\/.*$/i]
100
+ };
101
+
102
+ // Add video support for models that support it
103
+ if (caps.videoInput) {
104
+ patterns['video/*'] = [/^https?:\/\/.*$/i];
105
+ }
106
+
107
+ return this.settings.supportedUrls ?? this.config.supportedUrls ?? patterns;
108
+ }
109
+
110
+ private async getArgs({
111
+ prompt,
112
+ maxOutputTokens,
113
+ temperature,
114
+ topP,
115
+ topK,
116
+ frequencyPenalty,
117
+ presencePenalty,
118
+ stopSequences,
119
+ responseFormat,
120
+ seed,
121
+ providerOptions,
122
+ tools,
123
+ toolChoice
124
+ }: LanguageModelV3CallOptions) {
125
+ const warnings: SharedV3Warning[] = [];
126
+
127
+ const deprecatedOptions = await parseProviderOptions({
128
+ provider: 'moonshot',
129
+ providerOptions,
130
+ schema: kimiProviderOptionsSchema
131
+ });
132
+
133
+ if (deprecatedOptions != null) {
134
+ warnings.push({
135
+ type: 'other',
136
+ message: "The 'moonshot' key in providerOptions is deprecated. Use 'kimi' instead."
137
+ });
138
+ }
139
+
140
+ const providerOptionsName = this.providerOptionsName;
141
+ const kimiOptions = await parseProviderOptions({
142
+ provider: providerOptionsName,
143
+ providerOptions,
144
+ schema: kimiProviderOptionsSchema
145
+ });
146
+
147
+ const options: KimiProviderOptions = {
148
+ ...(deprecatedOptions ?? {}),
149
+ ...(kimiOptions ?? {})
150
+ };
151
+
152
+ if (topK != null) {
153
+ warnings.push({ type: 'unsupported', feature: 'topK' });
154
+ }
155
+
156
+ const strictJsonSchema = options.strictJsonSchema ?? true;
157
+
158
+ if (responseFormat?.type === 'json' && responseFormat.schema != null && !this.supportsStructuredOutputs) {
159
+ warnings.push({
160
+ type: 'unsupported',
161
+ feature: 'responseFormat',
162
+ details: 'JSON schema response format requires structured outputs support.'
163
+ });
164
+ }
165
+
166
+ // Resolve web search configuration from settings and provider options
167
+ const webSearch = resolveBuiltinToolConfig(this.settings.webSearch, options.webSearch);
168
+
169
+ // Resolve code interpreter configuration from settings and provider options
170
+ const codeInterpreter = resolveBuiltinToolConfig(this.settings.codeInterpreter, options.codeInterpreter);
171
+
172
+ // Resolve tool choice polyfill setting
173
+ const toolChoicePolyfill = options.toolChoicePolyfill ?? this.settings.toolChoicePolyfill ?? true;
174
+
175
+ const {
176
+ tools: kimiTools,
177
+ toolChoice: kimiToolChoice,
178
+ toolWarnings,
179
+ toolChoiceSystemMessage
180
+ } = prepareKimiTools({
181
+ tools,
182
+ toolChoice,
183
+ webSearch,
184
+ codeInterpreter,
185
+ toolChoicePolyfill
186
+ });
187
+
188
+ // Resolve caching configuration
189
+ const caching = resolveCachingConfig(this.settings.caching, options.caching);
190
+
191
+ // Build caching headers
192
+ const cachingHeaders = buildCachingHeaders(caching);
193
+
194
+ const passthroughOptions = getPassthroughOptions({
195
+ providerOptions,
196
+ providerOptionsName,
197
+ deprecatedProviderOptionsName: 'moonshot',
198
+ knownKeys: Object.keys(kimiProviderOptionsSchema.shape)
199
+ });
200
+
201
+ // Convert messages and optionally inject tool choice system message
202
+ const messages = convertToKimiChatMessages(prompt);
203
+ if (toolChoiceSystemMessage) {
204
+ // Prepend the tool choice instruction as a system message
205
+ messages.unshift({ role: 'system', content: toolChoiceSystemMessage });
206
+ }
207
+
208
+ const body = removeUndefinedEntries({
209
+ model: this.modelId,
210
+ messages,
211
+ max_tokens: maxOutputTokens,
212
+ temperature,
213
+ top_p: topP,
214
+ frequency_penalty: frequencyPenalty,
215
+ presence_penalty: presencePenalty,
216
+ stop: stopSequences,
217
+ seed,
218
+ response_format:
219
+ responseFormat?.type === 'json'
220
+ ? this.supportsStructuredOutputs && responseFormat.schema != null
221
+ ? {
222
+ type: 'json_schema',
223
+ json_schema: {
224
+ schema: responseFormat.schema,
225
+ strict: strictJsonSchema,
226
+ name: responseFormat.name ?? 'response',
227
+ description: responseFormat.description
228
+ }
229
+ }
230
+ : { type: 'json_object' }
231
+ : undefined,
232
+ tools: kimiTools,
233
+ tool_choice: kimiToolChoice,
234
+ user: options.user,
235
+ ...(kimiTools != null && options.parallelToolCalls != null
236
+ ? { parallel_tool_calls: options.parallelToolCalls }
237
+ : {}),
238
+ ...passthroughOptions
239
+ });
240
+
241
+ const requestHeaders: Record<string, string | undefined> = {
242
+ ...(options.requestId ? { 'X-Request-ID': options.requestId } : {}),
243
+ ...(options.extraHeaders ?? {}),
244
+ ...cachingHeaders
245
+ };
246
+
247
+ return {
248
+ body,
249
+ warnings: [...warnings, ...toolWarnings],
250
+ requestHeaders
251
+ };
252
+ }
253
+
254
+ async doGenerate(options: LanguageModelV3CallOptions): Promise<LanguageModelV3GenerateResult> {
255
+ const { body, warnings, requestHeaders } = await this.getArgs(options);
256
+
257
+ const {
258
+ responseHeaders,
259
+ value: response,
260
+ rawValue
261
+ } = await postJsonToApi({
262
+ url: `${this.config.baseURL}/chat/completions`,
263
+ headers: combineHeaders(this.config.headers(), requestHeaders, options.headers),
264
+ body,
265
+ failedResponseHandler: kimiFailedResponseHandler,
266
+ successfulResponseHandler: createJsonResponseHandler(kimiChatResponseSchema),
267
+ abortSignal: options.abortSignal,
268
+ fetch: this.config.fetch
269
+ });
270
+
271
+ const choice = response.choices[0];
272
+ const content: Array<LanguageModelV3Content> = [];
273
+
274
+ const { text, reasoning } = extractMessageContent(choice.message);
275
+
276
+ if (reasoning.length > 0) {
277
+ content.push({ type: 'reasoning', text: reasoning });
278
+ }
279
+
280
+ if (text.length > 0) {
281
+ content.push({ type: 'text', text });
282
+ }
283
+
284
+ if (choice.message.tool_calls != null) {
285
+ for (const toolCall of choice.message.tool_calls) {
286
+ content.push({
287
+ type: 'tool-call',
288
+ toolCallId: toolCall.id ?? this.generateIdFn(),
289
+ toolName: toolCall.function.name,
290
+ input: toolCall.function.arguments ?? ''
291
+ });
292
+ }
293
+ }
294
+
295
+ // Extract built-in tool token usage from tool calls
296
+ const webSearchTokens = extractWebSearchTokens(choice.message.tool_calls);
297
+ const codeInterpreterTokens = extractCodeInterpreterTokens(choice.message.tool_calls);
298
+
299
+ const providerMetadata = buildProviderMetadata({
300
+ providerOptionsName: this.providerOptionsName,
301
+ responseHeaders,
302
+ webSearchTokens,
303
+ codeInterpreterTokens
304
+ });
305
+
306
+ return {
307
+ content,
308
+ finishReason: {
309
+ unified: mapKimiFinishReason(choice.finish_reason),
310
+ raw: choice.finish_reason ?? undefined
311
+ },
312
+ usage: convertKimiUsage(response.usage, webSearchTokens, codeInterpreterTokens),
313
+ ...(providerMetadata ? { providerMetadata } : {}),
314
+ request: { body },
315
+ response: {
316
+ ...getResponseMetadata(response),
317
+ headers: responseHeaders,
318
+ body: rawValue
319
+ },
320
+ warnings
321
+ };
322
+ }
323
+
324
+ async doStream(options: LanguageModelV3CallOptions): Promise<LanguageModelV3StreamResult> {
325
+ const { body, warnings, requestHeaders } = await this.getArgs(options);
326
+ const streamBody = {
327
+ ...body,
328
+ stream: true,
329
+ stream_options: this.config.includeUsageInStream ? { include_usage: true } : undefined
330
+ };
331
+
332
+ const { responseHeaders, value: response } = await postJsonToApi({
333
+ url: `${this.config.baseURL}/chat/completions`,
334
+ headers: combineHeaders(this.config.headers(), requestHeaders, options.headers),
335
+ body: streamBody,
336
+ failedResponseHandler: kimiFailedResponseHandler,
337
+ successfulResponseHandler: createEventSourceResponseHandler(kimiChatChunkSchema),
338
+ abortSignal: options.abortSignal,
339
+ fetch: this.config.fetch
340
+ });
341
+
342
+ const requestId = getKimiRequestId(responseHeaders);
343
+
344
+ let finishReason: LanguageModelV3FinishReason = {
345
+ unified: 'other',
346
+ raw: undefined
347
+ };
348
+ let usage: z.infer<typeof kimiTokenUsageSchema> | undefined;
349
+
350
+ let isFirstChunk = true;
351
+ let isActiveText = false;
352
+ let isActiveReasoning = false;
353
+
354
+ const toolCalls: Array<{
355
+ id: string;
356
+ type: 'function';
357
+ function: { name: string; arguments: string };
358
+ hasFinished: boolean;
359
+ }> = [];
360
+
361
+ const providerOptionsName = this.providerOptionsName;
362
+ const _generateIdFn = this.generateIdFn;
363
+
364
+ return {
365
+ stream: response.pipeThrough(
366
+ new TransformStream<ParseResult<z.infer<typeof kimiChatChunkSchema>>, LanguageModelV3StreamPart>({
367
+ start(controller) {
368
+ controller.enqueue({ type: 'stream-start', warnings });
369
+ },
370
+
371
+ transform(chunk, controller) {
372
+ if (options.includeRawChunks) {
373
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
374
+ }
375
+
376
+ if (!chunk.success) {
377
+ finishReason = { unified: 'error', raw: undefined };
378
+ controller.enqueue({ type: 'error', error: chunk.error });
379
+ return;
380
+ }
381
+
382
+ if ('error' in chunk.value) {
383
+ finishReason = { unified: 'error', raw: undefined };
384
+ const error = (chunk.value as { error?: { message?: string } }).error;
385
+ controller.enqueue({
386
+ type: 'error',
387
+ error: error?.message ?? chunk.value
388
+ });
389
+ return;
390
+ }
391
+
392
+ const value = chunk.value as z.infer<typeof kimiChatChunkBaseSchema>;
393
+
394
+ if (isFirstChunk) {
395
+ isFirstChunk = false;
396
+ controller.enqueue({
397
+ type: 'response-metadata',
398
+ ...getResponseMetadata(value)
399
+ });
400
+ }
401
+
402
+ if (value.usage != null) {
403
+ usage = value.usage;
404
+ }
405
+
406
+ const choice = value.choices[0];
407
+ if (!choice) {
408
+ return;
409
+ }
410
+
411
+ if (choice.finish_reason != null) {
412
+ finishReason = {
413
+ unified: mapKimiFinishReason(choice.finish_reason),
414
+ raw: choice.finish_reason ?? undefined
415
+ };
416
+ }
417
+
418
+ const delta = choice.delta;
419
+ if (!delta) {
420
+ return;
421
+ }
422
+
423
+ const reasoningDelta = delta.reasoning_content ?? delta.reasoning;
424
+ if (reasoningDelta) {
425
+ if (!isActiveReasoning) {
426
+ if (isActiveText) {
427
+ controller.enqueue({ type: 'text-end', id: 'text-0' });
428
+ isActiveText = false;
429
+ }
430
+ controller.enqueue({
431
+ type: 'reasoning-start',
432
+ id: 'reasoning-0'
433
+ });
434
+ isActiveReasoning = true;
435
+ }
436
+
437
+ controller.enqueue({
438
+ type: 'reasoning-delta',
439
+ id: 'reasoning-0',
440
+ delta: reasoningDelta
441
+ });
442
+ }
443
+
444
+ if (delta.content) {
445
+ if (isActiveReasoning) {
446
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
447
+ isActiveReasoning = false;
448
+ }
449
+
450
+ if (!isActiveText) {
451
+ controller.enqueue({ type: 'text-start', id: 'text-0' });
452
+ isActiveText = true;
453
+ }
454
+
455
+ controller.enqueue({
456
+ type: 'text-delta',
457
+ id: 'text-0',
458
+ delta: delta.content
459
+ });
460
+ }
461
+
462
+ if (delta.tool_calls != null) {
463
+ if (isActiveReasoning) {
464
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
465
+ isActiveReasoning = false;
466
+ }
467
+
468
+ for (const toolCallDelta of delta.tool_calls) {
469
+ const index = toolCallDelta.index ?? toolCalls.length;
470
+
471
+ if (toolCalls[index] == null) {
472
+ if (toolCallDelta.id == null) {
473
+ throw new InvalidResponseDataError({
474
+ data: toolCallDelta,
475
+ message: "Expected 'id' to be a string."
476
+ });
477
+ }
478
+
479
+ if (toolCallDelta.function?.name == null) {
480
+ throw new InvalidResponseDataError({
481
+ data: toolCallDelta,
482
+ message: "Expected 'function.name' to be a string."
483
+ });
484
+ }
485
+
486
+ controller.enqueue({
487
+ type: 'tool-input-start',
488
+ id: toolCallDelta.id,
489
+ toolName: toolCallDelta.function.name
490
+ });
491
+
492
+ toolCalls[index] = {
493
+ id: toolCallDelta.id,
494
+ type: 'function',
495
+ function: {
496
+ name: toolCallDelta.function.name,
497
+ arguments: toolCallDelta.function.arguments ?? ''
498
+ },
499
+ hasFinished: false
500
+ };
501
+
502
+ const toolCall = toolCalls[index];
503
+
504
+ if (toolCall.function.arguments.length > 0) {
505
+ controller.enqueue({
506
+ type: 'tool-input-delta',
507
+ id: toolCall.id,
508
+ delta: toolCall.function.arguments
509
+ });
510
+ }
511
+
512
+ if (isParsableJson(toolCall.function.arguments)) {
513
+ controller.enqueue({ type: 'tool-input-end', id: toolCall.id });
514
+
515
+ controller.enqueue({
516
+ type: 'tool-call',
517
+ toolCallId: toolCall.id,
518
+ toolName: toolCall.function.name,
519
+ input: toolCall.function.arguments
520
+ });
521
+
522
+ toolCall.hasFinished = true;
523
+ }
524
+
525
+ continue;
526
+ }
527
+
528
+ const toolCall = toolCalls[index];
529
+ if (toolCall.hasFinished) {
530
+ continue;
531
+ }
532
+
533
+ if (toolCallDelta.function?.arguments != null) {
534
+ toolCall.function.arguments += toolCallDelta.function.arguments;
535
+ }
536
+
537
+ controller.enqueue({
538
+ type: 'tool-input-delta',
539
+ id: toolCall.id,
540
+ delta: toolCallDelta.function?.arguments ?? ''
541
+ });
542
+
543
+ if (isParsableJson(toolCall.function.arguments)) {
544
+ controller.enqueue({ type: 'tool-input-end', id: toolCall.id });
545
+
546
+ controller.enqueue({
547
+ type: 'tool-call',
548
+ toolCallId: toolCall.id,
549
+ toolName: toolCall.function.name,
550
+ input: toolCall.function.arguments
551
+ });
552
+
553
+ toolCall.hasFinished = true;
554
+ }
555
+ }
556
+ }
557
+ },
558
+
559
+ flush(controller) {
560
+ if (isActiveReasoning) {
561
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
562
+ isActiveReasoning = false;
563
+ }
564
+
565
+ if (isActiveText) {
566
+ controller.enqueue({ type: 'text-end', id: 'text-0' });
567
+ isActiveText = false;
568
+ }
569
+
570
+ for (const toolCall of toolCalls.filter((call) => !call.hasFinished)) {
571
+ controller.enqueue({ type: 'tool-input-end', id: toolCall.id });
572
+ controller.enqueue({
573
+ type: 'tool-call',
574
+ toolCallId: toolCall.id,
575
+ toolName: toolCall.function.name,
576
+ input: toolCall.function.arguments
577
+ });
578
+ }
579
+
580
+ // Extract built-in tool tokens from accumulated tool calls
581
+ const webSearchTokens = extractWebSearchTokens(toolCalls);
582
+ const codeInterpreterTokens = extractCodeInterpreterTokens(toolCalls);
583
+
584
+ const providerMetadata: SharedV3ProviderMetadata | undefined =
585
+ requestId || webSearchTokens != null || codeInterpreterTokens != null
586
+ ? {
587
+ [providerOptionsName]: {
588
+ ...(requestId ? { requestId } : {}),
589
+ ...(webSearchTokens != null ? { webSearchTokens } : {}),
590
+ ...(codeInterpreterTokens != null ? { codeInterpreterTokens } : {})
591
+ }
592
+ }
593
+ : undefined;
594
+
595
+ controller.enqueue({
596
+ type: 'finish',
597
+ finishReason,
598
+ usage: convertKimiUsage(usage, webSearchTokens, codeInterpreterTokens),
599
+ ...(providerMetadata ? { providerMetadata } : {})
600
+ });
601
+ }
602
+ })
603
+ ),
604
+ request: { body: streamBody },
605
+ response: { headers: responseHeaders }
606
+ };
607
+ }
608
+ }
609
+
610
+ // ============================================================================
611
+ // Helper Functions
612
+ // ============================================================================
613
+
614
+ function buildProviderMetadata({
615
+ providerOptionsName,
616
+ responseHeaders,
617
+ webSearchTokens,
618
+ codeInterpreterTokens
619
+ }: {
620
+ providerOptionsName: string;
621
+ responseHeaders?: Record<string, string>;
622
+ webSearchTokens?: number;
623
+ codeInterpreterTokens?: number;
624
+ }): SharedV3ProviderMetadata | undefined {
625
+ const requestId = getKimiRequestId(responseHeaders);
626
+
627
+ if (!requestId && webSearchTokens == null && codeInterpreterTokens == null) {
628
+ return undefined;
629
+ }
630
+
631
+ return {
632
+ [providerOptionsName]: {
633
+ ...(requestId ? { requestId } : {}),
634
+ ...(webSearchTokens != null ? { webSearchTokens } : {}),
635
+ ...(codeInterpreterTokens != null ? { codeInterpreterTokens } : {})
636
+ }
637
+ };
638
+ }
639
+
640
+ function getPassthroughOptions({
641
+ providerOptions,
642
+ providerOptionsName,
643
+ deprecatedProviderOptionsName,
644
+ knownKeys
645
+ }: {
646
+ providerOptions: LanguageModelV3CallOptions['providerOptions'];
647
+ providerOptionsName: string;
648
+ deprecatedProviderOptionsName: string;
649
+ knownKeys: string[];
650
+ }) {
651
+ const rawOptions = [providerOptions?.[deprecatedProviderOptionsName], providerOptions?.[providerOptionsName]].filter(
652
+ (entry): entry is Record<string, JSONValue | undefined> => entry != null && typeof entry === 'object'
653
+ );
654
+
655
+ const passthrough: Record<string, JSONValue | undefined> = {};
656
+
657
+ for (const options of rawOptions) {
658
+ for (const [key, value] of Object.entries(options ?? {})) {
659
+ if (!knownKeys.includes(key)) {
660
+ passthrough[key] = value;
661
+ }
662
+ }
663
+ }
664
+
665
+ return passthrough;
666
+ }
667
+
668
+ type BuiltinToolOptions = boolean | KimiWebSearchToolOptions | KimiCodeInterpreterToolOptions | undefined;
669
+
670
+ function resolveBuiltinToolConfig<T extends BuiltinToolOptions>(
671
+ settingsConfig: T | undefined,
672
+ optionsConfig: T | undefined
673
+ ): T | undefined {
674
+ // Provider options take precedence
675
+ if (optionsConfig != null) {
676
+ if (typeof optionsConfig === 'boolean') {
677
+ return optionsConfig ? ({ enabled: true } as T) : undefined;
678
+ }
679
+ const config = optionsConfig as { enabled: boolean };
680
+ return config.enabled ? optionsConfig : undefined;
681
+ }
682
+
683
+ // Fall back to settings
684
+ if (settingsConfig != null) {
685
+ if (typeof settingsConfig === 'boolean') {
686
+ return settingsConfig ? ({ enabled: true } as T) : undefined;
687
+ }
688
+ const config = settingsConfig as { enabled: boolean };
689
+ return config.enabled ? settingsConfig : undefined;
690
+ }
691
+
692
+ return undefined;
693
+ }
694
+
695
+ type CachingOptions = boolean | KimiCachingConfig | undefined;
696
+
697
+ /**
698
+ * Resolve caching configuration from settings and provider options.
699
+ */
700
+ function resolveCachingConfig(
701
+ settingsConfig: CachingOptions,
702
+ optionsConfig: CachingOptions
703
+ ): KimiCachingConfig | undefined {
704
+ // Provider options take precedence
705
+ const config = optionsConfig ?? settingsConfig;
706
+
707
+ if (config == null) {
708
+ return undefined;
709
+ }
710
+
711
+ if (typeof config === 'boolean') {
712
+ return config ? { enabled: true } : undefined;
713
+ }
714
+
715
+ return config.enabled ? config : undefined;
716
+ }
717
+
718
+ /**
719
+ * Build HTTP headers for context caching.
720
+ * Kimi uses specific headers to control caching behavior.
721
+ */
722
+ function buildCachingHeaders(caching: KimiCachingConfig | undefined): Record<string, string> {
723
+ if (!caching?.enabled) {
724
+ return {};
725
+ }
726
+
727
+ const headers: Record<string, string> = {
728
+ 'X-Kimi-Cache': 'enabled'
729
+ };
730
+
731
+ if (caching.cacheKey) {
732
+ headers['X-Kimi-Cache-Key'] = caching.cacheKey;
733
+ }
734
+
735
+ if (caching.ttlSeconds) {
736
+ headers['X-Kimi-Cache-TTL'] = String(caching.ttlSeconds);
737
+ }
738
+
739
+ if (caching.resetCache) {
740
+ headers['X-Kimi-Cache-Reset'] = 'true';
741
+ }
742
+
743
+ return headers;
744
+ }
745
+
746
+ // ============================================================================
747
+ // Zod Schemas
748
+ // ============================================================================
749
+
750
+ const kimiTokenUsageSchema = z
751
+ .object({
752
+ prompt_tokens: z.number().nullish(),
753
+ completion_tokens: z.number().nullish(),
754
+ total_tokens: z.number().nullish(),
755
+ prompt_tokens_details: z
756
+ .object({
757
+ cached_tokens: z.number().nullish()
758
+ })
759
+ .nullish(),
760
+ completion_tokens_details: z
761
+ .object({
762
+ reasoning_tokens: z.number().nullish()
763
+ })
764
+ .nullish()
765
+ })
766
+ .nullish();
767
+
768
+ const kimiChatResponseSchema = z.looseObject({
769
+ id: z.string().nullish(),
770
+ created: z.number().nullish(),
771
+ model: z.string().nullish(),
772
+ choices: z.array(
773
+ z.object({
774
+ message: z.object({
775
+ role: z.string().nullish(),
776
+ content: z.union([z.string(), z.array(z.any())]).nullish(),
777
+ reasoning_content: z.string().nullish(),
778
+ reasoning: z.string().nullish(),
779
+ tool_calls: z
780
+ .array(
781
+ z.object({
782
+ id: z.string().nullish(),
783
+ function: z.object({
784
+ name: z.string(),
785
+ arguments: z.string().nullish()
786
+ })
787
+ })
788
+ )
789
+ .nullish()
790
+ }),
791
+ finish_reason: z.string().nullish()
792
+ })
793
+ ),
794
+ usage: kimiTokenUsageSchema
795
+ });
796
+
797
+ const kimiChatChunkBaseSchema = z.looseObject({
798
+ id: z.string().nullish(),
799
+ created: z.number().nullish(),
800
+ model: z.string().nullish(),
801
+ choices: z.array(
802
+ z.object({
803
+ delta: z
804
+ .object({
805
+ role: z.enum(['assistant']).nullish(),
806
+ content: z.string().nullish(),
807
+ reasoning_content: z.string().nullish(),
808
+ reasoning: z.string().nullish(),
809
+ tool_calls: z
810
+ .array(
811
+ z.object({
812
+ index: z.number().nullish(),
813
+ id: z.string().nullish(),
814
+ function: z.object({
815
+ name: z.string().nullish(),
816
+ arguments: z.string().nullish()
817
+ })
818
+ })
819
+ )
820
+ .nullish()
821
+ })
822
+ .nullish(),
823
+ finish_reason: z.string().nullish()
824
+ })
825
+ ),
826
+ usage: kimiTokenUsageSchema
827
+ });
828
+
829
+ const kimiChatChunkSchema = z.union([kimiChatChunkBaseSchema, kimiErrorSchema]);