@ai-sdk/moonshotai 2.0.44 → 2.0.46

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.
@@ -1,60 +1,543 @@
1
- import { OpenAICompatibleChatLanguageModel } from '@ai-sdk/openai-compatible';
2
- import type { OpenAICompatibleChatConfig } from '@ai-sdk/openai-compatible/internal';
3
- import type {
4
- LanguageModelV3CallOptions,
5
- LanguageModelV3GenerateResult,
6
- LanguageModelV3StreamPart,
7
- LanguageModelV3StreamResult,
1
+ import {
2
+ InvalidResponseDataError,
3
+ type APICallError,
4
+ type LanguageModelV3,
5
+ type LanguageModelV3CallOptions,
6
+ type LanguageModelV3Content,
7
+ type LanguageModelV3FinishReason,
8
+ type LanguageModelV3GenerateResult,
9
+ type LanguageModelV3StreamPart,
10
+ type LanguageModelV3StreamResult,
11
+ type SharedV3Warning,
8
12
  } from '@ai-sdk/provider';
13
+ import {
14
+ combineHeaders,
15
+ createEventSourceResponseHandler,
16
+ createJsonErrorResponseHandler,
17
+ createJsonResponseHandler,
18
+ generateId,
19
+ parseProviderOptions,
20
+ postJsonToApi,
21
+ type FetchFunction,
22
+ type InferSchema,
23
+ type ParseResult,
24
+ type ResponseHandler,
25
+ } from '@ai-sdk/provider-utils';
26
+ import { convertToMoonshotAIChatMessages } from './convert-to-moonshotai-chat-messages';
9
27
  import { convertMoonshotAIChatUsage } from './convert-moonshotai-chat-usage';
10
- import type { MoonshotAIChatModelId } from './moonshotai-chat-options';
11
-
12
- export class MoonshotAIChatLanguageModel extends OpenAICompatibleChatLanguageModel {
13
- constructor(
14
- modelId: MoonshotAIChatModelId,
15
- config: OpenAICompatibleChatConfig,
16
- ) {
17
- super(modelId, config);
28
+ import { getResponseMetadata } from './get-response-metadata';
29
+ import { mapMoonshotAIFinishReason } from './map-moonshotai-finish-reason';
30
+ import {
31
+ moonshotAIChatChunkSchema,
32
+ moonshotAIChatResponseSchema,
33
+ moonshotAIErrorSchema,
34
+ type MoonshotAIChatTokenUsage,
35
+ } from './moonshotai-chat-api-types';
36
+ import {
37
+ getModelThinkingKeepSupport,
38
+ moonshotaiLanguageModelOptions,
39
+ type MoonshotAIChatModelId,
40
+ } from './moonshotai-chat-options';
41
+ import { prepareTools } from './moonshotai-prepare-tools';
42
+
43
+ export type MoonshotAIChatConfig = {
44
+ provider: string;
45
+ headers?: () => Record<string, string | undefined>;
46
+ url: (options: { modelId: string; path: string }) => string;
47
+ fetch?: FetchFunction;
48
+ includeUsage?: boolean;
49
+ supportsStructuredOutputs?: boolean;
50
+ };
51
+
52
+ export class MoonshotAIChatLanguageModel implements LanguageModelV3 {
53
+ readonly specificationVersion = 'v3';
54
+
55
+ readonly modelId: MoonshotAIChatModelId;
56
+
57
+ // Moonshot AI does not fetch external URLs; the AI SDK downloads and
58
+ // inlines URL file parts instead. ms:// file references from the Moonshot
59
+ // Files API are passed through natively.
60
+ readonly supportedUrls = {
61
+ 'image/*': [/^ms:\/\//],
62
+ 'video/*': [/^ms:\/\//],
63
+ };
64
+
65
+ private readonly config: MoonshotAIChatConfig;
66
+ private readonly failedResponseHandler: ResponseHandler<APICallError>;
67
+
68
+ constructor(modelId: MoonshotAIChatModelId, config: MoonshotAIChatConfig) {
69
+ this.modelId = modelId;
70
+ this.config = config;
71
+
72
+ this.failedResponseHandler = createJsonErrorResponseHandler({
73
+ errorSchema: moonshotAIErrorSchema,
74
+ errorToMessage: error => error.error.message,
75
+ });
76
+ }
77
+
78
+ get provider(): string {
79
+ return this.config.provider;
80
+ }
81
+
82
+ private get providerOptionsName(): string {
83
+ return this.config.provider.split('.')[0].trim();
84
+ }
85
+
86
+ private async getArgs({
87
+ prompt,
88
+ maxOutputTokens,
89
+ temperature,
90
+ topP,
91
+ topK,
92
+ frequencyPenalty,
93
+ presencePenalty,
94
+ providerOptions,
95
+ stopSequences,
96
+ responseFormat,
97
+ seed,
98
+ toolChoice,
99
+ tools,
100
+ }: LanguageModelV3CallOptions) {
101
+ const moonshotOptions =
102
+ (await parseProviderOptions({
103
+ provider: this.providerOptionsName,
104
+ providerOptions,
105
+ schema: moonshotaiLanguageModelOptions,
106
+ })) ?? {};
107
+
108
+ const messages = convertToMoonshotAIChatMessages(prompt);
109
+
110
+ const allWarnings: SharedV3Warning[] = [];
111
+ if (topK != null) {
112
+ allWarnings.push({ type: 'unsupported', feature: 'topK' });
113
+ }
114
+ if (seed != null) {
115
+ allWarnings.push({ type: 'unsupported', feature: 'seed' });
116
+ }
117
+
118
+ const {
119
+ tools: moonshotTools,
120
+ toolChoice: moonshotToolChoice,
121
+ toolWarnings,
122
+ } = prepareTools({ tools, toolChoice });
123
+
124
+ // Thinking is configured through explicit provider options only.
125
+ const thinking = moonshotOptions.thinking;
126
+
127
+ // Moonshot has no reasoning_history field; the API silently ignores it
128
+ // (verified against the live API). Preserved Thinking maps to
129
+ // thinking.keep, which only accepts 'all' and only on some models
130
+ // (verified: k2.6, k2.7-code, k3 accept it; k2.5 rejects it). Other
131
+ // reasoningHistory values use the server default.
132
+ let keep: 'all' | undefined;
133
+ if (moonshotOptions.reasoningHistory === 'preserved') {
134
+ if (getModelThinkingKeepSupport(this.modelId)) {
135
+ keep = 'all';
136
+ } else {
137
+ allWarnings.push({
138
+ type: 'unsupported',
139
+ feature: `reasoningHistory 'preserved' is not supported by model "${this.modelId}"`,
140
+ });
141
+ }
142
+ }
143
+
144
+ let response_format: Record<string, unknown> | undefined;
145
+ if (responseFormat?.type === 'json') {
146
+ if (
147
+ this.config.supportsStructuredOutputs === true &&
148
+ responseFormat.schema != null
149
+ ) {
150
+ // kimi-k2.5 produces nonsensical output when the top-level `$schema`
151
+ // keyword injected by the AI SDK is present, even though it otherwise
152
+ // supports structured outputs. Strip it from the schema sent to
153
+ // Moonshot; the full original schema is still used for result
154
+ // validation.
155
+ const { $schema: _$schema, ...schemaWithoutDollarSchema } =
156
+ responseFormat.schema;
157
+ response_format = {
158
+ type: 'json_schema',
159
+ json_schema: {
160
+ name: responseFormat.name ?? 'response',
161
+ schema: schemaWithoutDollarSchema,
162
+ ...(responseFormat.description != null && {
163
+ description: responseFormat.description,
164
+ }),
165
+ },
166
+ };
167
+ } else {
168
+ response_format = { type: 'json_object' };
169
+ }
170
+ }
171
+
172
+ return {
173
+ args: {
174
+ model: this.modelId,
175
+ max_tokens: maxOutputTokens,
176
+ temperature,
177
+ top_p: topP,
178
+ frequency_penalty: frequencyPenalty,
179
+ presence_penalty: presencePenalty,
180
+ response_format,
181
+ stop: stopSequences,
182
+ messages,
183
+ tools: moonshotTools,
184
+ tool_choice: moonshotToolChoice,
185
+ ...(thinking != null || keep != null
186
+ ? {
187
+ thinking: {
188
+ ...(thinking?.type != null && { type: thinking.type }),
189
+ ...(thinking?.budgetTokens !== undefined && {
190
+ budget_tokens: thinking.budgetTokens,
191
+ }),
192
+ ...(keep != null && { keep }),
193
+ },
194
+ }
195
+ : {}),
196
+ ...(moonshotOptions.reasoningEffort != null && {
197
+ reasoning_effort: moonshotOptions.reasoningEffort,
198
+ }),
199
+ ...(moonshotOptions.promptCacheKey != null && {
200
+ prompt_cache_key: moonshotOptions.promptCacheKey,
201
+ }),
202
+ ...(moonshotOptions.safetyIdentifier != null && {
203
+ safety_identifier: moonshotOptions.safetyIdentifier,
204
+ }),
205
+ },
206
+ warnings: [...allWarnings, ...toolWarnings],
207
+ };
18
208
  }
19
209
 
20
210
  async doGenerate(
21
211
  options: LanguageModelV3CallOptions,
22
212
  ): Promise<LanguageModelV3GenerateResult> {
23
- const result = await super.doGenerate(options);
213
+ const { args, warnings } = await this.getArgs({ ...options });
214
+
215
+ const {
216
+ responseHeaders,
217
+ value: responseBody,
218
+ rawValue: rawResponse,
219
+ } = await postJsonToApi({
220
+ url: this.config.url({
221
+ path: '/chat/completions',
222
+ modelId: this.modelId,
223
+ }),
224
+ headers: combineHeaders(this.config.headers?.(), options.headers),
225
+ body: args,
226
+ failedResponseHandler: this.failedResponseHandler,
227
+ successfulResponseHandler: createJsonResponseHandler(
228
+ moonshotAIChatResponseSchema,
229
+ ),
230
+ abortSignal: options.abortSignal,
231
+ fetch: this.config.fetch,
232
+ });
233
+
234
+ const choice = responseBody.choices[0];
235
+ const content: Array<LanguageModelV3Content> = [];
24
236
 
25
- // @ts-expect-error accessing response body from parent result
26
- const usage = result.response?.body?.usage;
237
+ // reasoning content (before text):
238
+ const reasoning = choice.message.reasoning_content;
239
+ if (reasoning != null && reasoning.length > 0) {
240
+ content.push({ type: 'reasoning', text: reasoning });
241
+ }
242
+
243
+ // tool calls:
244
+ if (choice.message.tool_calls != null) {
245
+ for (const toolCall of choice.message.tool_calls) {
246
+ content.push({
247
+ type: 'tool-call',
248
+ toolCallId: toolCall.id ?? generateId(),
249
+ toolName: toolCall.function.name,
250
+ input: toolCall.function.arguments ?? '',
251
+ });
252
+ }
253
+ }
254
+
255
+ // text content:
256
+ const text = choice.message.content;
257
+ if (text != null && text.length > 0) {
258
+ content.push({ type: 'text', text });
259
+ }
27
260
 
28
261
  return {
29
- ...result,
30
- usage: convertMoonshotAIChatUsage(usage),
262
+ content,
263
+ finishReason: {
264
+ unified: mapMoonshotAIFinishReason(choice.finish_reason),
265
+ raw: choice.finish_reason ?? undefined,
266
+ },
267
+ usage: convertMoonshotAIChatUsage(responseBody.usage),
268
+ request: { body: args },
269
+ response: {
270
+ ...getResponseMetadata(responseBody),
271
+ headers: responseHeaders,
272
+ body: rawResponse,
273
+ },
274
+ warnings,
31
275
  };
32
276
  }
33
277
 
34
278
  async doStream(
35
279
  options: LanguageModelV3CallOptions,
36
280
  ): Promise<LanguageModelV3StreamResult> {
37
- const result = await super.doStream(options);
281
+ const { args, warnings } = await this.getArgs({ ...options });
282
+
283
+ const body = {
284
+ ...args,
285
+ stream: true,
286
+ ...(this.config.includeUsage && {
287
+ stream_options: { include_usage: true },
288
+ }),
289
+ };
290
+
291
+ const { responseHeaders, value: response } = await postJsonToApi({
292
+ url: this.config.url({
293
+ path: '/chat/completions',
294
+ modelId: this.modelId,
295
+ }),
296
+ headers: combineHeaders(this.config.headers?.(), options.headers),
297
+ body,
298
+ failedResponseHandler: this.failedResponseHandler,
299
+ successfulResponseHandler: createEventSourceResponseHandler(
300
+ moonshotAIChatChunkSchema,
301
+ ),
302
+ abortSignal: options.abortSignal,
303
+ fetch: this.config.fetch,
304
+ });
305
+
306
+ const toolCalls: Array<{
307
+ id: string;
308
+ type: 'function';
309
+ function: { name: string; arguments: string };
310
+ hasFinished: boolean;
311
+ }> = [];
312
+
313
+ let finishReason: LanguageModelV3FinishReason = {
314
+ unified: 'other',
315
+ raw: undefined,
316
+ };
317
+ let usage: MoonshotAIChatTokenUsage | undefined = undefined;
318
+ let isFirstChunk = true;
319
+ let isActiveReasoning = false;
320
+ let isActiveText = false;
38
321
 
39
322
  return {
40
- ...result,
41
- stream: result.stream.pipeThrough(
323
+ stream: response.pipeThrough(
42
324
  new TransformStream<
43
- LanguageModelV3StreamPart,
325
+ ParseResult<InferSchema<typeof moonshotAIChatChunkSchema>>,
44
326
  LanguageModelV3StreamPart
45
327
  >({
328
+ start(controller) {
329
+ controller.enqueue({ type: 'stream-start', warnings });
330
+ },
331
+
46
332
  transform(chunk, controller) {
47
- if (chunk.type === 'finish' && chunk.usage) {
333
+ // emit raw chunk if requested (before anything else):
334
+ if (options.includeRawChunks) {
335
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
336
+ }
337
+
338
+ // handle failed chunk parsing / validation:
339
+ if (!chunk.success) {
340
+ finishReason = { unified: 'error', raw: undefined };
341
+ controller.enqueue({ type: 'error', error: chunk.error });
342
+ return;
343
+ }
344
+ const value = chunk.value;
345
+
346
+ // handle error chunks:
347
+ if ('error' in value) {
348
+ finishReason = { unified: 'error', raw: undefined };
349
+ controller.enqueue({ type: 'error', error: value.error.message });
350
+ return;
351
+ }
352
+
353
+ if (isFirstChunk) {
354
+ isFirstChunk = false;
355
+
356
+ controller.enqueue({
357
+ type: 'response-metadata',
358
+ ...getResponseMetadata(value),
359
+ });
360
+ }
361
+
362
+ if (value.usage != null) {
363
+ usage = value.usage;
364
+ }
365
+
366
+ const choice = value.choices[0];
367
+
368
+ if (choice?.finish_reason != null) {
369
+ finishReason = {
370
+ unified: mapMoonshotAIFinishReason(choice.finish_reason),
371
+ raw: choice.finish_reason,
372
+ };
373
+ }
374
+
375
+ if (choice?.delta == null) {
376
+ return;
377
+ }
378
+
379
+ const delta = choice.delta;
380
+
381
+ // enqueue reasoning before text deltas:
382
+ const reasoningContent = delta.reasoning_content;
383
+ if (reasoningContent) {
384
+ if (!isActiveReasoning) {
385
+ controller.enqueue({
386
+ type: 'reasoning-start',
387
+ id: 'reasoning-0',
388
+ });
389
+ isActiveReasoning = true;
390
+ }
391
+
392
+ controller.enqueue({
393
+ type: 'reasoning-delta',
394
+ id: 'reasoning-0',
395
+ delta: reasoningContent,
396
+ });
397
+ }
398
+
399
+ if (delta.content) {
400
+ if (!isActiveText) {
401
+ controller.enqueue({ type: 'text-start', id: 'txt-0' });
402
+ isActiveText = true;
403
+ }
404
+
405
+ // end reasoning when text starts:
406
+ if (isActiveReasoning) {
407
+ controller.enqueue({
408
+ type: 'reasoning-end',
409
+ id: 'reasoning-0',
410
+ });
411
+ isActiveReasoning = false;
412
+ }
413
+
48
414
  controller.enqueue({
49
- ...chunk,
50
- usage: convertMoonshotAIChatUsage(chunk.usage.raw as any),
415
+ type: 'text-delta',
416
+ id: 'txt-0',
417
+ delta: delta.content,
51
418
  });
52
- } else {
53
- controller.enqueue(chunk);
54
419
  }
420
+
421
+ if (delta.tool_calls != null) {
422
+ // end reasoning when tool calls start:
423
+ if (isActiveReasoning) {
424
+ controller.enqueue({
425
+ type: 'reasoning-end',
426
+ id: 'reasoning-0',
427
+ });
428
+ isActiveReasoning = false;
429
+ }
430
+
431
+ for (const toolCallDelta of delta.tool_calls) {
432
+ const index = toolCallDelta.index;
433
+
434
+ if (toolCalls[index] == null) {
435
+ if (toolCallDelta.id == null) {
436
+ throw new InvalidResponseDataError({
437
+ data: toolCallDelta,
438
+ message: `Expected 'id' to be a string.`,
439
+ });
440
+ }
441
+
442
+ if (toolCallDelta.function?.name == null) {
443
+ throw new InvalidResponseDataError({
444
+ data: toolCallDelta,
445
+ message: `Expected 'function.name' to be a string.`,
446
+ });
447
+ }
448
+
449
+ controller.enqueue({
450
+ type: 'tool-input-start',
451
+ id: toolCallDelta.id,
452
+ toolName: toolCallDelta.function.name,
453
+ });
454
+
455
+ toolCalls[index] = {
456
+ id: toolCallDelta.id,
457
+ type: 'function',
458
+ function: {
459
+ name: toolCallDelta.function.name,
460
+ arguments: toolCallDelta.function.arguments ?? '',
461
+ },
462
+ hasFinished: false,
463
+ };
464
+
465
+ const toolCall = toolCalls[index];
466
+
467
+ if (
468
+ toolCall.function?.name != null &&
469
+ toolCall.function?.arguments != null &&
470
+ toolCall.function.arguments.length > 0
471
+ ) {
472
+ // send delta if the argument text has already started:
473
+ controller.enqueue({
474
+ type: 'tool-input-delta',
475
+ id: toolCall.id,
476
+ delta: toolCall.function.arguments,
477
+ });
478
+ }
479
+
480
+ continue;
481
+ }
482
+
483
+ // existing tool call, merge if not finished
484
+ const toolCall = toolCalls[index];
485
+
486
+ if (toolCall.hasFinished) {
487
+ continue;
488
+ }
489
+
490
+ if (toolCallDelta.function?.arguments != null) {
491
+ toolCall.function.arguments +=
492
+ toolCallDelta.function.arguments;
493
+ }
494
+
495
+ // send delta
496
+ controller.enqueue({
497
+ type: 'tool-input-delta',
498
+ id: toolCall.id,
499
+ delta: toolCallDelta.function.arguments ?? '',
500
+ });
501
+ }
502
+ }
503
+ },
504
+
505
+ flush(controller) {
506
+ if (isActiveReasoning) {
507
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
508
+ }
509
+
510
+ if (isActiveText) {
511
+ controller.enqueue({ type: 'text-end', id: 'txt-0' });
512
+ }
513
+
514
+ // go through all tool calls and send the ones that are not finished
515
+ for (const toolCall of toolCalls.filter(
516
+ toolCall => !toolCall.hasFinished,
517
+ )) {
518
+ controller.enqueue({
519
+ type: 'tool-input-end',
520
+ id: toolCall.id,
521
+ });
522
+
523
+ controller.enqueue({
524
+ type: 'tool-call',
525
+ toolCallId: toolCall.id,
526
+ toolName: toolCall.function.name,
527
+ input: toolCall.function.arguments,
528
+ });
529
+ }
530
+
531
+ controller.enqueue({
532
+ type: 'finish',
533
+ finishReason,
534
+ usage: convertMoonshotAIChatUsage(usage),
535
+ });
55
536
  },
56
537
  }),
57
538
  ),
539
+ request: { body },
540
+ response: { headers: responseHeaders },
58
541
  };
59
542
  }
60
543
  }
@@ -15,9 +15,9 @@ export type MoonshotAIChatModelId =
15
15
 
16
16
  export const moonshotaiLanguageModelOptions = z.object({
17
17
  /**
18
- * Reasoning effort for Kimi K3. Currently, only `max` is supported.
18
+ * Reasoning effort for Kimi K3.
19
19
  */
20
- reasoningEffort: z.literal('max').optional(),
20
+ reasoningEffort: z.enum(['low', 'high', 'max']).optional(),
21
21
 
22
22
  thinking: z
23
23
  .object({
@@ -27,8 +27,35 @@ export const moonshotaiLanguageModelOptions = z.object({
27
27
  .optional(),
28
28
 
29
29
  reasoningHistory: z.enum(['disabled', 'interleaved', 'preserved']).optional(),
30
+
31
+ /**
32
+ * Used to cache responses for similar requests to optimize cache hit rates.
33
+ * Typically a session or task id.
34
+ */
35
+ promptCacheKey: z.string().optional(),
36
+
37
+ /**
38
+ * A stable identifier used to help Moonshot detect users violating usage
39
+ * policies. Recommended to hash the username or email address.
40
+ */
41
+ safetyIdentifier: z.string().optional(),
30
42
  });
31
43
 
32
44
  export type MoonshotAILanguageModelOptions = z.infer<
33
45
  typeof moonshotaiLanguageModelOptions
34
46
  >;
47
+
48
+ /**
49
+ * Whether the model accepts `thinking.keep` (Preserved Thinking). Verified
50
+ * against the live API: kimi-k2.6, kimi-k2.7-code(+highspeed), and kimi-k3
51
+ * accept `keep: 'all'`; other models reject it with a 400.
52
+ */
53
+ export function getModelThinkingKeepSupport(
54
+ modelId: MoonshotAIChatModelId,
55
+ ): boolean {
56
+ return (
57
+ modelId === 'kimi-k2.6' ||
58
+ modelId === 'kimi-k3' ||
59
+ modelId.startsWith('kimi-k2.7-code')
60
+ );
61
+ }