@ai-sdk/deepseek 3.0.31 → 3.0.34

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.
@@ -14,6 +14,7 @@ import {
14
14
  createEventSourceResponseHandler,
15
15
  createJsonErrorResponseHandler,
16
16
  createJsonResponseHandler,
17
+ createProviderStreamError,
17
18
  generateId,
18
19
  isCustomReasoning,
19
20
  mapReasoningToProviderEffort,
@@ -34,6 +35,7 @@ import {
34
35
  deepseekChatChunkSchema,
35
36
  deepseekChatResponseSchema,
36
37
  deepSeekErrorSchema,
38
+ type DeepSeekChatLogprob,
37
39
  type DeepSeekChatTokenUsage,
38
40
  } from './deepseek-chat-api-types';
39
41
  import {
@@ -49,10 +51,133 @@ export type DeepSeekChatConfig = {
49
51
  headers?: () => Record<string, string | undefined>;
50
52
  url: (options: { modelId: string; path: string }) => string;
51
53
  fetch?: FetchFunction;
54
+ supportsAssistantPrefixCompletion?: boolean;
55
+ supportsStrictToolCalls?: boolean;
56
+ supportsPenaltySampling?: boolean;
52
57
  supportsThinking?: boolean;
53
58
  supportsStructuredOutputs?: boolean;
54
59
  };
55
60
 
61
+ function createDeepSeekStreamError(
62
+ error: {
63
+ message: string;
64
+ type?: string | null;
65
+ code?: string | number | null;
66
+ },
67
+ data: unknown,
68
+ ) {
69
+ const metadata = getDeepSeekStreamErrorMetadata(error);
70
+
71
+ return createProviderStreamError({
72
+ message: error.message,
73
+ type: error.type ?? undefined,
74
+ code: error.code ?? undefined,
75
+ ...metadata,
76
+ data,
77
+ });
78
+ }
79
+
80
+ function getDeepSeekStreamErrorMetadata(error: {
81
+ type?: string | null;
82
+ code?: string | number | null;
83
+ }): {
84
+ statusCode?: number;
85
+ isRetryable?: boolean;
86
+ } {
87
+ if (
88
+ error.code === 'insufficient_quota' ||
89
+ error.type === 'insufficient_quota'
90
+ ) {
91
+ return { statusCode: 429, isRetryable: false };
92
+ }
93
+
94
+ const explicitStatusCode = getHttpStatusCode(error.code);
95
+ if (explicitStatusCode != null) {
96
+ return {
97
+ statusCode: explicitStatusCode,
98
+ isRetryable: isRetryableStatusCode(explicitStatusCode),
99
+ };
100
+ }
101
+
102
+ for (const discriminator of [error.code, error.type]) {
103
+ switch (discriminator) {
104
+ case 'rate_limit_exceeded':
105
+ case 'rate_limit_error':
106
+ return { statusCode: 429, isRetryable: true };
107
+ case 'server_error':
108
+ case 'api_error':
109
+ case 'internal_server_error':
110
+ return { statusCode: 500, isRetryable: true };
111
+ case 'overloaded_error':
112
+ case 'service_unavailable':
113
+ return { statusCode: 503, isRetryable: true };
114
+ case 'timeout':
115
+ case 'timeout_error':
116
+ return { statusCode: 504, isRetryable: true };
117
+ case 'authentication_error':
118
+ case 'invalid_api_key':
119
+ return { statusCode: 401, isRetryable: false };
120
+ case 'permission_error':
121
+ return { statusCode: 403, isRetryable: false };
122
+ case 'not_found_error':
123
+ case 'model_not_found':
124
+ return { statusCode: 404, isRetryable: false };
125
+ case 'bad_request':
126
+ case 'context_length_exceeded':
127
+ case 'invalid_request_error':
128
+ return { statusCode: 400, isRetryable: false };
129
+ }
130
+ }
131
+
132
+ return {};
133
+ }
134
+
135
+ function getHttpStatusCode(value: unknown): number | undefined {
136
+ const statusCode =
137
+ typeof value === 'string' && /^\d{3}$/.test(value) ? Number(value) : value;
138
+
139
+ return typeof statusCode === 'number' &&
140
+ Number.isInteger(statusCode) &&
141
+ statusCode >= 400 &&
142
+ statusCode <= 599
143
+ ? statusCode
144
+ : undefined;
145
+ }
146
+
147
+ function isRetryableStatusCode(statusCode: number): boolean {
148
+ return (
149
+ statusCode === 408 ||
150
+ statusCode === 409 ||
151
+ statusCode === 429 ||
152
+ statusCode >= 500
153
+ );
154
+ }
155
+
156
+ function mapDeepSeekProviderReasoningEffort({
157
+ reasoningEffort,
158
+ warnings,
159
+ }: {
160
+ reasoningEffort: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
161
+ warnings: SharedV4Warning[];
162
+ }): 'low' | 'high' | 'max' {
163
+ const mapped =
164
+ reasoningEffort === 'medium'
165
+ ? 'high'
166
+ : reasoningEffort === 'xhigh'
167
+ ? 'max'
168
+ : reasoningEffort;
169
+
170
+ if (mapped !== reasoningEffort) {
171
+ warnings.push({
172
+ type: 'compatibility',
173
+ feature: 'reasoningEffort',
174
+ details: `reasoningEffort "${reasoningEffort}" is not a canonical DeepSeek value. mapped to "${mapped}".`,
175
+ });
176
+ }
177
+
178
+ return mapped;
179
+ }
180
+
56
181
  export class DeepSeekChatLanguageModel implements LanguageModelV4 {
57
182
  readonly specificationVersion = 'v4';
58
183
 
@@ -123,11 +248,16 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
123
248
 
124
249
  const supportsStructuredOutputs =
125
250
  this.config.supportsStructuredOutputs === true;
251
+ const supportsPenaltySampling =
252
+ this.config.supportsPenaltySampling === true;
126
253
 
127
- const { messages, warnings } = convertToDeepSeekChatMessages({
254
+ const { messages, warnings } = await convertToDeepSeekChatMessages({
128
255
  prompt,
129
256
  responseFormat,
130
257
  modelId: this.modelId,
258
+ providerOptionsName: this.providerOptionsName,
259
+ supportsAssistantPrefixCompletion:
260
+ this.config.supportsAssistantPrefixCompletion,
131
261
  supportsStructuredOutputs,
132
262
  });
133
263
  const allWarnings: SharedV4Warning[] = [...warnings];
@@ -140,6 +270,24 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
140
270
  allWarnings.push({ type: 'unsupported', feature: 'seed' });
141
271
  }
142
272
 
273
+ if (!supportsPenaltySampling && frequencyPenalty != null) {
274
+ allWarnings.push({
275
+ type: 'deprecated',
276
+ setting: 'frequencyPenalty',
277
+ message:
278
+ 'frequencyPenalty is deprecated by DeepSeek and has been omitted. Remove frequencyPenalty from the request.',
279
+ });
280
+ }
281
+
282
+ if (!supportsPenaltySampling && presencePenalty != null) {
283
+ allWarnings.push({
284
+ type: 'deprecated',
285
+ setting: 'presencePenalty',
286
+ message:
287
+ 'presencePenalty is deprecated by DeepSeek and has been omitted. Remove presencePenalty from the request.',
288
+ });
289
+ }
290
+
143
291
  const {
144
292
  tools: deepseekTools,
145
293
  toolChoice: deepseekToolChoices,
@@ -147,41 +295,88 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
147
295
  } = prepareTools({
148
296
  tools,
149
297
  toolChoice,
298
+ supportsStrictToolCalls: this.config.supportsStrictToolCalls,
150
299
  });
151
300
 
301
+ const thinkingType = deepseekOptions.thinking?.type;
302
+ if (thinkingType === 'adaptive') {
303
+ allWarnings.push({
304
+ type: 'compatibility',
305
+ feature: 'thinking.type',
306
+ details:
307
+ 'thinking.type "adaptive" is not a canonical DeepSeek value. mapped to "enabled".',
308
+ });
309
+ }
310
+
152
311
  const thinking =
153
312
  this.config.supportsThinking === false
154
313
  ? undefined
155
- : deepseekOptions.thinking?.type != null
156
- ? { type: deepseekOptions.thinking.type }
314
+ : thinkingType != null
315
+ ? { type: thinkingType === 'adaptive' ? 'enabled' : thinkingType }
157
316
  : isCustomReasoning(reasoning)
158
317
  ? { type: reasoning === 'none' ? 'disabled' : 'enabled' }
159
318
  : undefined;
160
319
 
320
+ const isThinkingEnabled =
321
+ this.config.supportsThinking !== false &&
322
+ thinking?.type !== 'disabled' &&
323
+ (thinking != null ||
324
+ this.modelId === 'deepseek-reasoner' ||
325
+ this.modelId.includes('deepseek-v4'));
326
+
327
+ if (isThinkingEnabled && temperature != null) {
328
+ allWarnings.push({
329
+ type: 'unsupported',
330
+ feature: 'temperature',
331
+ details:
332
+ "temperature has no effect when DeepSeek thinking is enabled. Set providerOptions.deepseek.thinking.type to 'disabled' to use temperature.",
333
+ });
334
+ }
335
+
336
+ if (isThinkingEnabled && topP != null) {
337
+ allWarnings.push({
338
+ type: 'unsupported',
339
+ feature: 'topP',
340
+ details:
341
+ "topP has no effect when DeepSeek thinking is enabled. Set providerOptions.deepseek.thinking.type to 'disabled' to use topP.",
342
+ });
343
+ }
344
+
161
345
  const reasoningEffort =
162
- deepseekOptions.reasoningEffort ??
163
- (isCustomReasoning(reasoning) && reasoning !== 'none'
164
- ? mapReasoningToProviderEffort({
165
- reasoning,
166
- effortMap: {
167
- minimal: 'low',
168
- low: 'low',
169
- medium: 'medium',
170
- high: 'high',
171
- xhigh: 'max',
172
- },
346
+ deepseekOptions.reasoningEffort != null
347
+ ? mapDeepSeekProviderReasoningEffort({
348
+ reasoningEffort: deepseekOptions.reasoningEffort,
173
349
  warnings: allWarnings,
174
350
  })
175
- : undefined);
351
+ : isCustomReasoning(reasoning) && reasoning !== 'none'
352
+ ? mapReasoningToProviderEffort({
353
+ reasoning,
354
+ effortMap: {
355
+ minimal: 'low',
356
+ low: 'low',
357
+ medium: 'high',
358
+ high: 'high',
359
+ xhigh: 'max',
360
+ },
361
+ warnings: allWarnings,
362
+ })
363
+ : undefined;
176
364
 
177
365
  return {
178
366
  args: {
179
367
  model: this.modelId,
368
+ ...((deepseekOptions.logprobs === true ||
369
+ deepseekOptions.topLogprobs != null) && { logprobs: true }),
370
+ ...(deepseekOptions.topLogprobs != null && {
371
+ top_logprobs: deepseekOptions.topLogprobs,
372
+ }),
180
373
  max_tokens: maxOutputTokens,
181
- temperature,
182
- top_p: topP,
183
- frequency_penalty: frequencyPenalty,
184
- presence_penalty: presencePenalty,
374
+ temperature: isThinkingEnabled ? undefined : temperature,
375
+ top_p: isThinkingEnabled ? undefined : topP,
376
+ frequency_penalty: supportsPenaltySampling
377
+ ? frequencyPenalty
378
+ : undefined,
379
+ presence_penalty: supportsPenaltySampling ? presencePenalty : undefined,
185
380
  response_format:
186
381
  responseFormat?.type === 'json'
187
382
  ? supportsStructuredOutputs && responseFormat.schema != null
@@ -201,6 +396,9 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
201
396
  tools: deepseekTools,
202
397
  tool_choice: deepseekToolChoices,
203
398
  thinking,
399
+ ...(deepseekOptions.userId != null && {
400
+ user_id: deepseekOptions.userId,
401
+ }),
204
402
  ...(thinking?.type !== 'disabled' &&
205
403
  reasoningEffort != null && {
206
404
  reasoning_effort: reasoningEffort,
@@ -275,6 +473,22 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
275
473
  [this.providerOptionsName]: {
276
474
  promptCacheHitTokens: responseBody.usage?.prompt_cache_hit_tokens,
277
475
  promptCacheMissTokens: responseBody.usage?.prompt_cache_miss_tokens,
476
+ ...(responseBody.object != null && {
477
+ responseObject: responseBody.object,
478
+ }),
479
+ ...(choice.index != null && { choiceIndex: choice.index }),
480
+ ...(choice.message.role != null && {
481
+ messageRole: choice.message.role,
482
+ }),
483
+ ...(choice.message.tool_calls != null && {
484
+ toolCallTypes: choice.message.tool_calls
485
+ .map(toolCall => toolCall.type)
486
+ .filter(type => type != null),
487
+ }),
488
+ ...(choice.logprobs != null && { logprobs: choice.logprobs }),
489
+ ...(responseBody.system_fingerprint != null && {
490
+ systemFingerprint: responseBody.system_fingerprint,
491
+ }),
278
492
  },
279
493
  },
280
494
  request: { body: args },
@@ -320,10 +534,17 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
320
534
  raw: undefined,
321
535
  };
322
536
  let usage: DeepSeekChatTokenUsage | undefined = undefined;
537
+ let systemFingerprint: string | undefined = undefined;
323
538
  let isFirstChunk = true;
324
539
  const providerOptionsName = this.providerOptionsName;
325
540
  let isActiveReasoning = false;
326
541
  let isActiveText = false;
542
+ let responseObject: 'chat.completion.chunk' | undefined;
543
+ let choiceIndex: number | undefined;
544
+ let messageRole: 'assistant' | undefined;
545
+ const toolCallTypes = new Map<number, 'function'>();
546
+ const contentLogprobs: DeepSeekChatLogprob[] = [];
547
+ const reasoningLogprobs: DeepSeekChatLogprob[] = [];
327
548
 
328
549
  return {
329
550
  stream: response.pipeThrough(
@@ -355,7 +576,10 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
355
576
  // handle error chunks:
356
577
  if ('error' in value) {
357
578
  finishReason = { unified: 'error', raw: undefined };
358
- controller.enqueue({ type: 'error', error: value.error.message });
579
+ controller.enqueue({
580
+ type: 'error',
581
+ error: createDeepSeekStreamError(value.error, value),
582
+ });
359
583
  return;
360
584
  }
361
585
 
@@ -372,8 +596,22 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
372
596
  usage = value.usage;
373
597
  }
374
598
 
599
+ if (value.object != null) {
600
+ responseObject = value.object;
601
+ }
602
+
603
+ // The fingerprint is repeated on stream chunks; keep the latest
604
+ // non-null value in case it changes during the response.
605
+ if (value.system_fingerprint != null) {
606
+ systemFingerprint = value.system_fingerprint;
607
+ }
608
+
375
609
  const choice = value.choices[0];
376
610
 
611
+ if (choice?.index != null) {
612
+ choiceIndex = choice.index;
613
+ }
614
+
377
615
  if (choice?.finish_reason != null) {
378
616
  finishReason = {
379
617
  unified: mapDeepSeekFinishReason(choice.finish_reason),
@@ -381,12 +619,24 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
381
619
  };
382
620
  }
383
621
 
622
+ if (choice?.logprobs?.content != null) {
623
+ contentLogprobs.push(...choice.logprobs.content);
624
+ }
625
+
626
+ if (choice?.logprobs?.reasoning_content != null) {
627
+ reasoningLogprobs.push(...choice.logprobs.reasoning_content);
628
+ }
629
+
384
630
  if (choice?.delta == null) {
385
631
  return;
386
632
  }
387
633
 
388
634
  const delta = choice.delta;
389
635
 
636
+ if (delta.role != null) {
637
+ messageRole = delta.role;
638
+ }
639
+
390
640
  // enqueue reasoning before text deltas:
391
641
  const reasoningContent = delta.reasoning_content;
392
642
  if (reasoningContent) {
@@ -438,6 +688,9 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
438
688
  }
439
689
 
440
690
  for (const toolCallDelta of delta.tool_calls) {
691
+ if (toolCallDelta.type != null) {
692
+ toolCallTypes.set(toolCallDelta.index, toolCallDelta.type);
693
+ }
441
694
  toolCallTracker.processDelta(toolCallDelta);
442
695
  }
443
696
  }
@@ -464,6 +717,26 @@ export class DeepSeekChatLanguageModel implements LanguageModelV4 {
464
717
  usage?.prompt_cache_hit_tokens ?? undefined,
465
718
  promptCacheMissTokens:
466
719
  usage?.prompt_cache_miss_tokens ?? undefined,
720
+ ...(responseObject != null && { responseObject }),
721
+ ...(choiceIndex != null && { choiceIndex }),
722
+ ...(messageRole != null && { messageRole }),
723
+ ...(toolCallTypes.size > 0 && {
724
+ toolCallTypes: [...toolCallTypes.entries()]
725
+ .sort(([left], [right]) => left - right)
726
+ .map(([, type]) => type),
727
+ }),
728
+ ...((contentLogprobs.length > 0 ||
729
+ reasoningLogprobs.length > 0) && {
730
+ logprobs: {
731
+ ...(contentLogprobs.length > 0 && {
732
+ content: contentLogprobs,
733
+ }),
734
+ ...(reasoningLogprobs.length > 0 && {
735
+ reasoning_content: reasoningLogprobs,
736
+ }),
737
+ },
738
+ }),
739
+ ...(systemFingerprint != null && { systemFingerprint }),
467
740
  },
468
741
  },
469
742
  });
@@ -0,0 +1,24 @@
1
+ import { z } from 'zod/v4';
2
+
3
+ export const deepseekFilePartProviderOptions = z.object({
4
+ /**
5
+ * Controls how DeepSeek processes an image sent as an `image_url` part.
6
+ *
7
+ * @see https://api-docs.deepseek.com/api/create-chat-completion/
8
+ */
9
+ imageDetail: z.enum(['low', 'high', 'original', 'auto']).optional(),
10
+
11
+ /**
12
+ * Sends inline image data as a DeepSeek `file` part using `file_data`
13
+ * instead of an `image_url` data URL. When set, the file part's filename
14
+ * is preserved.
15
+ *
16
+ * This option only applies to inline image data. It cannot be combined
17
+ * with `imageDetail`.
18
+ */
19
+ fileData: z.literal(true).optional(),
20
+ });
21
+
22
+ export type DeepSeekFilePartProviderOptions = z.infer<
23
+ typeof deepseekFilePartProviderOptions
24
+ >;
@@ -1,6 +1,7 @@
1
- import type {
2
- LanguageModelV4CallOptions,
3
- SharedV4Warning,
1
+ import {
2
+ UnsupportedFunctionalityError,
3
+ type LanguageModelV4CallOptions,
4
+ type SharedV4Warning,
4
5
  } from '@ai-sdk/provider';
5
6
  import type {
6
7
  DeepSeekFunctionTool,
@@ -10,9 +11,11 @@ import type {
10
11
  export function prepareTools({
11
12
  tools,
12
13
  toolChoice,
14
+ supportsStrictToolCalls,
13
15
  }: {
14
16
  tools: LanguageModelV4CallOptions['tools'];
15
17
  toolChoice?: LanguageModelV4CallOptions['toolChoice'];
18
+ supportsStrictToolCalls?: boolean;
16
19
  }): {
17
20
  tools: undefined | Array<DeepSeekFunctionTool>;
18
21
  toolChoice: DeepSeekToolChoice;
@@ -27,6 +30,29 @@ export function prepareTools({
27
30
  return { tools: undefined, toolChoice: undefined, toolWarnings };
28
31
  }
29
32
 
33
+ const functionTools = tools.filter(tool => tool.type === 'function');
34
+ const hasStrictTool = functionTools.some(tool => tool.strict === true);
35
+
36
+ if (hasStrictTool && supportsStrictToolCalls === false) {
37
+ throw new UnsupportedFunctionalityError({
38
+ functionality: 'DeepSeek strict tool calls',
39
+ message:
40
+ 'DeepSeek strict tool calls require a beta base URL ending in `/beta`.',
41
+ });
42
+ }
43
+
44
+ if (
45
+ hasStrictTool &&
46
+ supportsStrictToolCalls === true &&
47
+ functionTools.some(tool => tool.strict !== true)
48
+ ) {
49
+ throw new UnsupportedFunctionalityError({
50
+ functionality: 'mixed DeepSeek strict and non-strict tool calls',
51
+ message:
52
+ 'DeepSeek strict mode requires every function tool in the request to set `strict: true`.',
53
+ });
54
+ }
55
+
30
56
  const deepseekTools: Array<DeepSeekFunctionTool> = [];
31
57
 
32
58
  for (const tool of tools) {
@@ -91,6 +91,8 @@ export function createDeepSeek(
91
91
  url: ({ path }) => `${baseURL}${path}`,
92
92
  headers: getHeaders,
93
93
  fetch: options.fetch,
94
+ supportsAssistantPrefixCompletion: baseURL.endsWith('/beta'),
95
+ supportsStrictToolCalls: baseURL.endsWith('/beta'),
94
96
  });
95
97
  };
96
98
 
@@ -5,12 +5,16 @@ export const deepSeekFilesResponseSchema = lazySchema(() =>
5
5
  zodSchema(
6
6
  z.object({
7
7
  id: z.string(),
8
- object: z.string().nullish(),
9
- bytes: z.number().nullish(),
10
- created_at: z.number().nullish(),
8
+ // These fields are required by DeepSeek's OpenAPI schema, but they are
9
+ // not needed to construct the provider reference. Keep them nullish so
10
+ // uploads remain resilient to incomplete responses while validating any
11
+ // returned values precisely enough to avoid misleading metadata.
12
+ object: z.literal('file').nullish(),
13
+ bytes: z.number().int().nonnegative().nullish(),
14
+ created_at: z.number().int().nonnegative().nullish(),
11
15
  filename: z.string().nullish(),
12
- purpose: z.string().nullish(),
13
- expires_at: z.number().nullish(),
16
+ purpose: z.literal('user_data').nullish(),
17
+ expires_at: z.number().int().nonnegative().nullish(),
14
18
  }),
15
19
  ),
16
20
  );