@ai-sdk/deepseek 2.0.8 → 2.0.9

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,534 @@
1
+ import {
2
+ APICallError,
3
+ InvalidResponseDataError,
4
+ LanguageModelV3,
5
+ LanguageModelV3CallOptions,
6
+ LanguageModelV3Content,
7
+ LanguageModelV3FinishReason,
8
+ LanguageModelV3GenerateResult,
9
+ LanguageModelV3StreamPart,
10
+ LanguageModelV3StreamResult,
11
+ } from '@ai-sdk/provider';
12
+ import {
13
+ combineHeaders,
14
+ createEventSourceResponseHandler,
15
+ createJsonErrorResponseHandler,
16
+ createJsonResponseHandler,
17
+ FetchFunction,
18
+ generateId,
19
+ InferSchema,
20
+ isParsableJson,
21
+ parseProviderOptions,
22
+ ParseResult,
23
+ postJsonToApi,
24
+ ResponseHandler,
25
+ } from '@ai-sdk/provider-utils';
26
+ import { convertToDeepSeekChatMessages } from './convert-to-deepseek-chat-messages';
27
+ import { convertDeepSeekUsage } from './convert-to-deepseek-usage';
28
+ import {
29
+ deepseekChatChunkSchema,
30
+ deepseekChatResponseSchema,
31
+ DeepSeekChatTokenUsage,
32
+ deepSeekErrorSchema,
33
+ } from './deepseek-chat-api-types';
34
+ import {
35
+ DeepSeekChatModelId,
36
+ deepseekChatOptions,
37
+ } from './deepseek-chat-options';
38
+ import { prepareTools } from './deepseek-prepare-tools';
39
+ import { getResponseMetadata } from './get-response-metadata';
40
+ import { mapDeepSeekFinishReason } from './map-deepseek-finish-reason';
41
+
42
+ export type DeepSeekChatConfig = {
43
+ provider: string;
44
+ headers: () => Record<string, string | undefined>;
45
+ url: (options: { modelId: string; path: string }) => string;
46
+ fetch?: FetchFunction;
47
+ };
48
+
49
+ export class DeepSeekChatLanguageModel implements LanguageModelV3 {
50
+ readonly specificationVersion = 'v3';
51
+
52
+ readonly modelId: DeepSeekChatModelId;
53
+ readonly supportedUrls = {};
54
+
55
+ private readonly config: DeepSeekChatConfig;
56
+ private readonly failedResponseHandler: ResponseHandler<APICallError>;
57
+
58
+ constructor(modelId: DeepSeekChatModelId, config: DeepSeekChatConfig) {
59
+ this.modelId = modelId;
60
+ this.config = config;
61
+
62
+ this.failedResponseHandler = createJsonErrorResponseHandler({
63
+ errorSchema: deepSeekErrorSchema,
64
+ errorToMessage: (error: InferSchema<typeof deepSeekErrorSchema>) =>
65
+ error.error.message,
66
+ });
67
+ }
68
+
69
+ get provider(): string {
70
+ return this.config.provider;
71
+ }
72
+
73
+ private get providerOptionsName(): string {
74
+ return this.config.provider.split('.')[0].trim();
75
+ }
76
+
77
+ private async getArgs({
78
+ prompt,
79
+ maxOutputTokens,
80
+ temperature,
81
+ topP,
82
+ topK,
83
+ frequencyPenalty,
84
+ presencePenalty,
85
+ providerOptions,
86
+ stopSequences,
87
+ responseFormat,
88
+ seed,
89
+ toolChoice,
90
+ tools,
91
+ }: LanguageModelV3CallOptions) {
92
+ const deepseekOptions =
93
+ (await parseProviderOptions({
94
+ provider: this.providerOptionsName,
95
+ providerOptions,
96
+ schema: deepseekChatOptions,
97
+ })) ?? {};
98
+
99
+ const { messages, warnings } = convertToDeepSeekChatMessages({
100
+ prompt,
101
+ responseFormat,
102
+ });
103
+
104
+ if (topK != null) {
105
+ warnings.push({ type: 'unsupported', feature: 'topK' });
106
+ }
107
+
108
+ if (seed != null) {
109
+ warnings.push({ type: 'unsupported', feature: 'seed' });
110
+ }
111
+
112
+ const {
113
+ tools: deepseekTools,
114
+ toolChoice: deepseekToolChoices,
115
+ toolWarnings,
116
+ } = prepareTools({
117
+ tools,
118
+ toolChoice,
119
+ });
120
+
121
+ return {
122
+ args: {
123
+ model: this.modelId,
124
+ max_tokens: maxOutputTokens,
125
+ temperature,
126
+ top_p: topP,
127
+ frequency_penalty: frequencyPenalty,
128
+ presence_penalty: presencePenalty,
129
+ response_format:
130
+ responseFormat?.type === 'json' ? { type: 'json_object' } : undefined,
131
+ stop: stopSequences,
132
+ messages,
133
+ tools: deepseekTools,
134
+ tool_choice: deepseekToolChoices,
135
+ thinking:
136
+ deepseekOptions.thinking?.type != null
137
+ ? { type: deepseekOptions.thinking.type }
138
+ : undefined,
139
+ },
140
+ warnings: [...warnings, ...toolWarnings],
141
+ };
142
+ }
143
+
144
+ async doGenerate(
145
+ options: LanguageModelV3CallOptions,
146
+ ): Promise<LanguageModelV3GenerateResult> {
147
+ const { args, warnings } = await this.getArgs({ ...options });
148
+
149
+ const {
150
+ responseHeaders,
151
+ value: responseBody,
152
+ rawValue: rawResponse,
153
+ } = await postJsonToApi({
154
+ url: this.config.url({
155
+ path: '/chat/completions',
156
+ modelId: this.modelId,
157
+ }),
158
+ headers: combineHeaders(this.config.headers(), options.headers),
159
+ body: args,
160
+ failedResponseHandler: this.failedResponseHandler,
161
+ successfulResponseHandler: createJsonResponseHandler(
162
+ deepseekChatResponseSchema,
163
+ ),
164
+ abortSignal: options.abortSignal,
165
+ fetch: this.config.fetch,
166
+ });
167
+
168
+ const choice = responseBody.choices[0];
169
+ const content: Array<LanguageModelV3Content> = [];
170
+
171
+ // reasoning content (before text):
172
+ const reasoning = choice.message.reasoning_content;
173
+ if (reasoning != null && reasoning.length > 0) {
174
+ content.push({
175
+ type: 'reasoning',
176
+ text: reasoning,
177
+ });
178
+ }
179
+
180
+ // tool calls:
181
+ if (choice.message.tool_calls != null) {
182
+ for (const toolCall of choice.message.tool_calls) {
183
+ content.push({
184
+ type: 'tool-call',
185
+ toolCallId: toolCall.id ?? generateId(),
186
+ toolName: toolCall.function.name,
187
+ input: toolCall.function.arguments!,
188
+ });
189
+ }
190
+ }
191
+
192
+ // text content:
193
+ const text = choice.message.content;
194
+ if (text != null && text.length > 0) {
195
+ content.push({ type: 'text', text });
196
+ }
197
+
198
+ return {
199
+ content,
200
+ finishReason: {
201
+ unified: mapDeepSeekFinishReason(choice.finish_reason),
202
+ raw: choice.finish_reason ?? undefined,
203
+ },
204
+ usage: convertDeepSeekUsage(responseBody.usage),
205
+ providerMetadata: {
206
+ [this.providerOptionsName]: {
207
+ promptCacheHitTokens: responseBody.usage?.prompt_cache_hit_tokens,
208
+ promptCacheMissTokens: responseBody.usage?.prompt_cache_miss_tokens,
209
+ },
210
+ },
211
+ request: { body: args },
212
+ response: {
213
+ ...getResponseMetadata(responseBody),
214
+ headers: responseHeaders,
215
+ body: rawResponse,
216
+ },
217
+ warnings,
218
+ };
219
+ }
220
+
221
+ async doStream(
222
+ options: LanguageModelV3CallOptions,
223
+ ): Promise<LanguageModelV3StreamResult> {
224
+ const { args, warnings } = await this.getArgs({ ...options });
225
+
226
+ const body = {
227
+ ...args,
228
+ stream: true,
229
+ stream_options: { include_usage: true },
230
+ };
231
+
232
+ const { responseHeaders, value: response } = await postJsonToApi({
233
+ url: this.config.url({
234
+ path: '/chat/completions',
235
+ modelId: this.modelId,
236
+ }),
237
+ headers: combineHeaders(this.config.headers(), options.headers),
238
+ body,
239
+ failedResponseHandler: this.failedResponseHandler,
240
+ successfulResponseHandler: createEventSourceResponseHandler(
241
+ deepseekChatChunkSchema,
242
+ ),
243
+ abortSignal: options.abortSignal,
244
+ fetch: this.config.fetch,
245
+ });
246
+
247
+ const toolCalls: Array<{
248
+ id: string;
249
+ type: 'function';
250
+ function: {
251
+ name: string;
252
+ arguments: string;
253
+ };
254
+ hasFinished: boolean;
255
+ }> = [];
256
+
257
+ let finishReason: LanguageModelV3FinishReason = {
258
+ unified: 'other',
259
+ raw: undefined,
260
+ };
261
+ let usage: DeepSeekChatTokenUsage | undefined = undefined;
262
+ let isFirstChunk = true;
263
+ const providerOptionsName = this.providerOptionsName;
264
+ let isActiveReasoning = false;
265
+ let isActiveText = false;
266
+
267
+ return {
268
+ stream: response.pipeThrough(
269
+ new TransformStream<
270
+ ParseResult<InferSchema<typeof deepseekChatChunkSchema>>,
271
+ LanguageModelV3StreamPart
272
+ >({
273
+ start(controller) {
274
+ controller.enqueue({ type: 'stream-start', warnings });
275
+ },
276
+
277
+ transform(chunk, controller) {
278
+ // Emit raw chunk if requested (before anything else)
279
+ if (options.includeRawChunks) {
280
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
281
+ }
282
+
283
+ // handle failed chunk parsing / validation:
284
+ if (!chunk.success) {
285
+ finishReason = { unified: 'error', raw: undefined };
286
+ controller.enqueue({ type: 'error', error: chunk.error });
287
+ return;
288
+ }
289
+ const value = chunk.value;
290
+
291
+ // handle error chunks:
292
+ if ('error' in value) {
293
+ finishReason = { unified: 'error', raw: undefined };
294
+ controller.enqueue({ type: 'error', error: value.error.message });
295
+ return;
296
+ }
297
+
298
+ if (isFirstChunk) {
299
+ isFirstChunk = false;
300
+
301
+ controller.enqueue({
302
+ type: 'response-metadata',
303
+ ...getResponseMetadata(value),
304
+ });
305
+ }
306
+
307
+ if (value.usage != null) {
308
+ usage = value.usage;
309
+ }
310
+
311
+ const choice = value.choices[0];
312
+
313
+ if (choice?.finish_reason != null) {
314
+ finishReason = {
315
+ unified: mapDeepSeekFinishReason(choice.finish_reason),
316
+ raw: choice.finish_reason,
317
+ };
318
+ }
319
+
320
+ if (choice?.delta == null) {
321
+ return;
322
+ }
323
+
324
+ const delta = choice.delta;
325
+
326
+ // enqueue reasoning before text deltas:
327
+ const reasoningContent = delta.reasoning_content;
328
+ if (reasoningContent) {
329
+ if (!isActiveReasoning) {
330
+ controller.enqueue({
331
+ type: 'reasoning-start',
332
+ id: 'reasoning-0',
333
+ });
334
+ isActiveReasoning = true;
335
+ }
336
+
337
+ controller.enqueue({
338
+ type: 'reasoning-delta',
339
+ id: 'reasoning-0',
340
+ delta: reasoningContent,
341
+ });
342
+ }
343
+
344
+ if (delta.content) {
345
+ if (!isActiveText) {
346
+ controller.enqueue({ type: 'text-start', id: 'txt-0' });
347
+ isActiveText = true;
348
+ }
349
+
350
+ // end reasoning when text starts:
351
+ if (isActiveReasoning) {
352
+ controller.enqueue({
353
+ type: 'reasoning-end',
354
+ id: 'reasoning-0',
355
+ });
356
+ isActiveReasoning = false;
357
+ }
358
+
359
+ controller.enqueue({
360
+ type: 'text-delta',
361
+ id: 'txt-0',
362
+ delta: delta.content,
363
+ });
364
+ }
365
+
366
+ if (delta.tool_calls != null) {
367
+ // end reasoning when tool calls start:
368
+ if (isActiveReasoning) {
369
+ controller.enqueue({
370
+ type: 'reasoning-end',
371
+ id: 'reasoning-0',
372
+ });
373
+ isActiveReasoning = false;
374
+ }
375
+
376
+ for (const toolCallDelta of delta.tool_calls) {
377
+ const index = toolCallDelta.index;
378
+
379
+ if (toolCalls[index] == null) {
380
+ if (toolCallDelta.id == null) {
381
+ throw new InvalidResponseDataError({
382
+ data: toolCallDelta,
383
+ message: `Expected 'id' to be a string.`,
384
+ });
385
+ }
386
+
387
+ if (toolCallDelta.function?.name == null) {
388
+ throw new InvalidResponseDataError({
389
+ data: toolCallDelta,
390
+ message: `Expected 'function.name' to be a string.`,
391
+ });
392
+ }
393
+
394
+ controller.enqueue({
395
+ type: 'tool-input-start',
396
+ id: toolCallDelta.id,
397
+ toolName: toolCallDelta.function.name,
398
+ });
399
+
400
+ toolCalls[index] = {
401
+ id: toolCallDelta.id,
402
+ type: 'function',
403
+ function: {
404
+ name: toolCallDelta.function.name,
405
+ arguments: toolCallDelta.function.arguments ?? '',
406
+ },
407
+ hasFinished: false,
408
+ };
409
+
410
+ const toolCall = toolCalls[index];
411
+
412
+ if (
413
+ toolCall.function?.name != null &&
414
+ toolCall.function?.arguments != null
415
+ ) {
416
+ // send delta if the argument text has already started:
417
+ if (toolCall.function.arguments.length > 0) {
418
+ controller.enqueue({
419
+ type: 'tool-input-delta',
420
+ id: toolCall.id,
421
+ delta: toolCall.function.arguments,
422
+ });
423
+ }
424
+
425
+ // check if tool call is complete
426
+ // (some providers send the full tool call in one chunk):
427
+ if (isParsableJson(toolCall.function.arguments)) {
428
+ controller.enqueue({
429
+ type: 'tool-input-end',
430
+ id: toolCall.id,
431
+ });
432
+
433
+ controller.enqueue({
434
+ type: 'tool-call',
435
+ toolCallId: toolCall.id ?? generateId(),
436
+ toolName: toolCall.function.name,
437
+ input: toolCall.function.arguments,
438
+ });
439
+ toolCall.hasFinished = true;
440
+ }
441
+ }
442
+
443
+ continue;
444
+ }
445
+
446
+ // existing tool call, merge if not finished
447
+ const toolCall = toolCalls[index];
448
+
449
+ if (toolCall.hasFinished) {
450
+ continue;
451
+ }
452
+
453
+ if (toolCallDelta.function?.arguments != null) {
454
+ toolCall.function!.arguments +=
455
+ toolCallDelta.function?.arguments ?? '';
456
+ }
457
+
458
+ // send delta
459
+ controller.enqueue({
460
+ type: 'tool-input-delta',
461
+ id: toolCall.id,
462
+ delta: toolCallDelta.function.arguments ?? '',
463
+ });
464
+
465
+ // check if tool call is complete
466
+ if (
467
+ toolCall.function?.name != null &&
468
+ toolCall.function?.arguments != null &&
469
+ isParsableJson(toolCall.function.arguments)
470
+ ) {
471
+ controller.enqueue({
472
+ type: 'tool-input-end',
473
+ id: toolCall.id,
474
+ });
475
+
476
+ controller.enqueue({
477
+ type: 'tool-call',
478
+ toolCallId: toolCall.id ?? generateId(),
479
+ toolName: toolCall.function.name,
480
+ input: toolCall.function.arguments,
481
+ });
482
+ toolCall.hasFinished = true;
483
+ }
484
+ }
485
+ }
486
+ },
487
+
488
+ flush(controller) {
489
+ if (isActiveReasoning) {
490
+ controller.enqueue({ type: 'reasoning-end', id: 'reasoning-0' });
491
+ }
492
+
493
+ if (isActiveText) {
494
+ controller.enqueue({ type: 'text-end', id: 'txt-0' });
495
+ }
496
+
497
+ // go through all tool calls and send the ones that are not finished
498
+ for (const toolCall of toolCalls.filter(
499
+ toolCall => !toolCall.hasFinished,
500
+ )) {
501
+ controller.enqueue({
502
+ type: 'tool-input-end',
503
+ id: toolCall.id,
504
+ });
505
+
506
+ controller.enqueue({
507
+ type: 'tool-call',
508
+ toolCallId: toolCall.id ?? generateId(),
509
+ toolName: toolCall.function.name,
510
+ input: toolCall.function.arguments,
511
+ });
512
+ }
513
+
514
+ controller.enqueue({
515
+ type: 'finish',
516
+ finishReason,
517
+ usage: convertDeepSeekUsage(usage),
518
+ providerMetadata: {
519
+ [providerOptionsName]: {
520
+ promptCacheHitTokens:
521
+ usage?.prompt_cache_hit_tokens ?? undefined,
522
+ promptCacheMissTokens:
523
+ usage?.prompt_cache_miss_tokens ?? undefined,
524
+ },
525
+ },
526
+ });
527
+ },
528
+ }),
529
+ ),
530
+ request: { body },
531
+ response: { headers: responseHeaders },
532
+ };
533
+ }
534
+ }
@@ -0,0 +1,20 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ // https://api-docs.deepseek.com/quick_start/pricing
4
+ export type DeepSeekChatModelId =
5
+ | 'deepseek-chat'
6
+ | 'deepseek-reasoner'
7
+ | (string & {});
8
+
9
+ export const deepseekChatOptions = z.object({
10
+ /**
11
+ * Type of thinking to use. Defaults to `enabled`.
12
+ */
13
+ thinking: z
14
+ .object({
15
+ type: z.enum(['enabled', 'disabled']).optional(),
16
+ })
17
+ .optional(),
18
+ });
19
+
20
+ export type DeepSeekChatOptions = z.infer<typeof deepseekChatOptions>;