@ai-sdk/moonshotai 3.0.31 → 3.0.33

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