@ai-sdk/deepseek 3.0.30 → 3.0.32

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,11 +1,14 @@
1
- import type {
2
- LanguageModelV4CallOptions,
3
- LanguageModelV4Prompt,
4
- SharedV4Warning,
1
+ import {
2
+ InvalidPromptError,
3
+ UnsupportedFunctionalityError,
4
+ type LanguageModelV4CallOptions,
5
+ type LanguageModelV4Prompt,
6
+ type SharedV4Warning,
5
7
  } from '@ai-sdk/provider';
6
8
  import {
7
9
  convertToBase64,
8
10
  getTopLevelMediaType,
11
+ parseProviderOptions,
9
12
  resolveFullMediaType,
10
13
  resolveProviderReference,
11
14
  } from '@ai-sdk/provider-utils';
@@ -13,21 +16,35 @@ import type {
13
16
  DeepSeekChatPrompt,
14
17
  DeepSeekContentPart,
15
18
  } from './deepseek-chat-api-types';
19
+ import { deepseekFilePartProviderOptions } from './deepseek-file-part-options';
20
+ import { deepseekAssistantMessageProviderOptions } from './deepseek-chat-language-model-options';
21
+
22
+ const supportedImageMediaTypes = new Set([
23
+ 'image/gif',
24
+ 'image/jpeg',
25
+ 'image/jpg',
26
+ 'image/png',
27
+ 'image/webp',
28
+ ]);
16
29
 
17
- export function convertToDeepSeekChatMessages({
30
+ export async function convertToDeepSeekChatMessages({
18
31
  prompt,
19
32
  responseFormat,
20
33
  modelId,
34
+ providerOptionsName = 'deepseek',
35
+ supportsAssistantPrefixCompletion = false,
21
36
  supportsStructuredOutputs = false,
22
37
  }: {
23
38
  prompt: LanguageModelV4Prompt;
24
39
  responseFormat: LanguageModelV4CallOptions['responseFormat'];
25
40
  modelId: string;
41
+ providerOptionsName?: string;
42
+ supportsAssistantPrefixCompletion?: boolean;
26
43
  supportsStructuredOutputs?: boolean;
27
- }): {
44
+ }): Promise<{
28
45
  messages: DeepSeekChatPrompt;
29
46
  warnings: Array<SharedV4Warning>;
30
- } {
47
+ }> {
31
48
  const isDeepSeekV4 = modelId.includes('deepseek-v4');
32
49
  const messages: DeepSeekChatPrompt = [];
33
50
  const warnings: Array<SharedV4Warning> = [];
@@ -64,12 +81,34 @@ export function convertToDeepSeekChatMessages({
64
81
  }
65
82
 
66
83
  let index = -1;
67
- for (const { role, content } of prompt) {
84
+ for (const { role, content, providerOptions } of prompt) {
68
85
  index++;
69
86
 
87
+ // The assistant schema extends the common message schema, so one parse
88
+ // validates names for every role and the assistant-only prefix option.
89
+ const deepseekMessageOptions = await parseProviderOptions({
90
+ provider: providerOptionsName,
91
+ providerOptions,
92
+ schema: deepseekAssistantMessageProviderOptions,
93
+ });
94
+
95
+ if (deepseekMessageOptions?.prefix === true && role !== 'assistant') {
96
+ throw new InvalidPromptError({
97
+ prompt,
98
+ message:
99
+ 'DeepSeek assistant prefix completion requires `prefix: true` on an assistant message.',
100
+ });
101
+ }
102
+
70
103
  switch (role) {
71
104
  case 'system': {
72
- messages.push({ role: 'system', content });
105
+ messages.push({
106
+ role: 'system',
107
+ content,
108
+ ...(deepseekMessageOptions?.name != null && {
109
+ name: deepseekMessageOptions.name,
110
+ }),
111
+ });
73
112
  break;
74
113
  }
75
114
 
@@ -96,7 +135,13 @@ export function convertToDeepSeekChatMessages({
96
135
  }
97
136
  }
98
137
 
99
- messages.push({ role: 'user', content: userContent });
138
+ messages.push({
139
+ role: 'user',
140
+ content: userContent,
141
+ ...(deepseekMessageOptions?.name != null && {
142
+ name: deepseekMessageOptions.name,
143
+ }),
144
+ });
100
145
  break;
101
146
  }
102
147
 
@@ -108,6 +153,12 @@ export function convertToDeepSeekChatMessages({
108
153
  part.type === 'file' &&
109
154
  getTopLevelMediaType(part.mediaType) === 'image'
110
155
  ) {
156
+ const filePartOptions = await parseProviderOptions({
157
+ provider: providerOptionsName,
158
+ providerOptions: part.providerOptions,
159
+ schema: deepseekFilePartProviderOptions,
160
+ });
161
+
111
162
  if (part.data.type === 'reference') {
112
163
  userContent.push({
113
164
  type: 'file',
@@ -117,15 +168,77 @@ export function convertToDeepSeekChatMessages({
117
168
  }),
118
169
  });
119
170
  } else if (part.data.type === 'url' || part.data.type === 'data') {
120
- userContent.push({
121
- type: 'image_url',
122
- image_url: {
123
- url:
124
- part.data.type === 'url'
125
- ? part.data.url.toString()
126
- : `data:${resolveFullMediaType({ part })};base64,${convertToBase64(part.data.data)}`,
127
- },
128
- });
171
+ const resolvedMediaType = resolveFullMediaType({ part });
172
+
173
+ if (!supportedImageMediaTypes.has(resolvedMediaType)) {
174
+ throw new UnsupportedFunctionalityError({
175
+ functionality: `DeepSeek image media type ${resolvedMediaType}`,
176
+ message:
177
+ 'DeepSeek supports JPEG, PNG, GIF, and WebP image inputs.',
178
+ });
179
+ }
180
+
181
+ if (part.data.type === 'url') {
182
+ const url = part.data.url.toString();
183
+
184
+ if (url.length > 8192) {
185
+ throw new InvalidPromptError({
186
+ prompt,
187
+ message:
188
+ 'DeepSeek image URLs must not exceed 8192 characters.',
189
+ });
190
+ }
191
+
192
+ if (filePartOptions?.fileData === true) {
193
+ throw new InvalidPromptError({
194
+ prompt,
195
+ message:
196
+ 'DeepSeek `fileData` image parts require inline data, not a URL.',
197
+ });
198
+ }
199
+
200
+ userContent.push({
201
+ type: 'image_url',
202
+ image_url: {
203
+ url,
204
+ ...(filePartOptions?.imageDetail != null && {
205
+ detail: filePartOptions.imageDetail,
206
+ }),
207
+ },
208
+ });
209
+ } else {
210
+ const dataUrl = `data:${
211
+ resolvedMediaType === 'image/jpg'
212
+ ? 'image/jpeg'
213
+ : resolvedMediaType
214
+ };base64,${convertToBase64(part.data.data)}`;
215
+
216
+ if (filePartOptions?.fileData === true) {
217
+ if (filePartOptions.imageDetail != null) {
218
+ throw new InvalidPromptError({
219
+ prompt,
220
+ message:
221
+ 'DeepSeek `imageDetail` cannot be combined with `fileData`.',
222
+ });
223
+ }
224
+
225
+ userContent.push({
226
+ type: 'file',
227
+ file_data: dataUrl,
228
+ ...(part.filename != null && { filename: part.filename }),
229
+ });
230
+ } else {
231
+ userContent.push({
232
+ type: 'image_url',
233
+ image_url: {
234
+ url: dataUrl,
235
+ ...(filePartOptions?.imageDetail != null && {
236
+ detail: filePartOptions.imageDetail,
237
+ }),
238
+ },
239
+ });
240
+ }
241
+ }
129
242
  } else {
130
243
  warnings.push({
131
244
  type: 'unsupported',
@@ -143,11 +256,32 @@ export function convertToDeepSeekChatMessages({
143
256
  messages.push({
144
257
  role: 'user',
145
258
  content: userContent,
259
+ ...(deepseekMessageOptions?.name != null && {
260
+ name: deepseekMessageOptions.name,
261
+ }),
146
262
  });
147
263
 
148
264
  break;
149
265
  }
150
266
  case 'assistant': {
267
+ if (deepseekMessageOptions?.prefix === true) {
268
+ if (index !== prompt.length - 1) {
269
+ throw new InvalidPromptError({
270
+ prompt,
271
+ message:
272
+ 'DeepSeek assistant prefix completion requires the prefixed assistant message to be the final message.',
273
+ });
274
+ }
275
+
276
+ if (!supportsAssistantPrefixCompletion) {
277
+ throw new UnsupportedFunctionalityError({
278
+ functionality: 'DeepSeek assistant prefix completion',
279
+ message:
280
+ 'DeepSeek assistant prefix completion requires a beta base URL ending in `/beta`.',
281
+ });
282
+ }
283
+ }
284
+
151
285
  let text = '';
152
286
  let reasoning: string | undefined;
153
287
 
@@ -195,6 +329,12 @@ export function convertToDeepSeekChatMessages({
195
329
  messages.push({
196
330
  role: 'assistant',
197
331
  content: text,
332
+ ...(deepseekMessageOptions?.name != null && {
333
+ name: deepseekMessageOptions.name,
334
+ }),
335
+ ...(deepseekMessageOptions?.prefix === true && {
336
+ prefix: true,
337
+ }),
198
338
  reasoning_content: reasoning ?? (isDeepSeekV4 ? '' : undefined),
199
339
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
200
340
  });
@@ -203,6 +343,13 @@ export function convertToDeepSeekChatMessages({
203
343
  }
204
344
 
205
345
  case 'tool': {
346
+ if (deepseekMessageOptions?.name != null) {
347
+ warnings.push({
348
+ type: 'unsupported',
349
+ feature: 'message name on tool messages',
350
+ });
351
+ }
352
+
206
353
  for (const toolResponse of content) {
207
354
  if (toolResponse.type === 'tool-approval-response') {
208
355
  continue;
@@ -36,7 +36,7 @@ export function convertDeepSeekUsage(
36
36
  },
37
37
  outputTokens: {
38
38
  total: completionTokens,
39
- text: completionTokens - reasoningTokens,
39
+ text: Math.max(0, completionTokens - reasoningTokens),
40
40
  reasoning: reasoningTokens,
41
41
  },
42
42
  raw: usage,
@@ -12,11 +12,13 @@ export type DeepSeekMessage =
12
12
  export interface DeepSeekSystemMessage {
13
13
  role: 'system';
14
14
  content: string;
15
+ name?: string;
15
16
  }
16
17
 
17
18
  export interface DeepSeekUserMessage {
18
19
  role: 'user';
19
20
  content: string | Array<DeepSeekContentPart>;
21
+ name?: string;
20
22
  }
21
23
 
22
24
  export type DeepSeekContentPart =
@@ -31,17 +33,28 @@ export interface DeepSeekContentPartText {
31
33
 
32
34
  export interface DeepSeekContentPartImage {
33
35
  type: 'image_url';
34
- image_url: { url: string };
36
+ image_url: {
37
+ url: string;
38
+ detail?: 'low' | 'high' | 'original' | 'auto';
39
+ };
35
40
  }
36
41
 
37
- export interface DeepSeekContentPartFile {
38
- type: 'file';
39
- file_id: string;
40
- }
42
+ export type DeepSeekContentPartFile =
43
+ | {
44
+ type: 'file';
45
+ file_id: string;
46
+ }
47
+ | {
48
+ type: 'file';
49
+ file_data: string;
50
+ filename?: string;
51
+ };
41
52
 
42
53
  export interface DeepSeekAssistantMessage {
43
54
  role: 'assistant';
44
55
  content?: string | null;
56
+ name?: string;
57
+ prefix?: true;
45
58
  reasoning_content?: string;
46
59
  tool_calls?: Array<DeepSeekMessageToolCall>;
47
60
  }
@@ -106,14 +119,39 @@ export const deepSeekErrorSchema = z.object({
106
119
 
107
120
  export type DeepSeekErrorData = z.infer<typeof deepSeekErrorSchema>;
108
121
 
122
+ const deepseekChatLogprobSchema = z.object({
123
+ token: z.string(),
124
+ logprob: z.number(),
125
+ bytes: z.array(z.number()).nullable(),
126
+ top_logprobs: z.array(
127
+ z.object({
128
+ token: z.string(),
129
+ logprob: z.number(),
130
+ bytes: z.array(z.number()).nullable(),
131
+ }),
132
+ ),
133
+ });
134
+
135
+ const deepseekChatLogprobsSchema = z
136
+ .object({
137
+ content: z.array(deepseekChatLogprobSchema).nullish(),
138
+ reasoning_content: z.array(deepseekChatLogprobSchema).nullish(),
139
+ })
140
+ .nullish();
141
+
142
+ export type DeepSeekChatLogprob = z.infer<typeof deepseekChatLogprobSchema>;
143
+
109
144
  // limited version of the schema, focussed on what is needed for the implementation
110
145
  // this approach limits breakages when the API changes and increases efficiency
111
146
  export const deepseekChatResponseSchema = z.object({
112
147
  id: z.string().nullish(),
113
148
  created: z.number().nullish(),
114
149
  model: z.string().nullish(),
150
+ object: z.literal('chat.completion').nullish(),
151
+ system_fingerprint: z.string().nullish(),
115
152
  choices: z.array(
116
153
  z.object({
154
+ index: z.number().nullish(),
117
155
  message: z.object({
118
156
  role: z.literal('assistant').nullish(),
119
157
  content: z.string().nullish(),
@@ -122,6 +160,7 @@ export const deepseekChatResponseSchema = z.object({
122
160
  .array(
123
161
  z.object({
124
162
  id: z.string().nullish(),
163
+ type: z.literal('function').nullish(),
125
164
  function: z.object({
126
165
  name: z.string(),
127
166
  arguments: z.string(),
@@ -130,6 +169,7 @@ export const deepseekChatResponseSchema = z.object({
130
169
  )
131
170
  .nullish(),
132
171
  }),
172
+ logprobs: deepseekChatLogprobsSchema,
133
173
  finish_reason: z.string().nullish(),
134
174
  }),
135
175
  ),
@@ -145,8 +185,11 @@ export const deepseekChatChunkSchema = lazySchema(() =>
145
185
  id: z.string().nullish(),
146
186
  created: z.number().nullish(),
147
187
  model: z.string().nullish(),
188
+ object: z.literal('chat.completion.chunk').nullish(),
189
+ system_fingerprint: z.string().nullish(),
148
190
  choices: z.array(
149
191
  z.object({
192
+ index: z.number().nullish(),
150
193
  delta: z
151
194
  .object({
152
195
  role: z.enum(['assistant']).nullish(),
@@ -157,6 +200,7 @@ export const deepseekChatChunkSchema = lazySchema(() =>
157
200
  z.object({
158
201
  index: z.number(),
159
202
  id: z.string().nullish(),
203
+ type: z.literal('function').nullish(),
160
204
  function: z.object({
161
205
  name: z.string().nullish(),
162
206
  arguments: z.string().nullish(),
@@ -166,6 +210,7 @@ export const deepseekChatChunkSchema = lazySchema(() =>
166
210
  .nullish(),
167
211
  })
168
212
  .nullish(),
213
+ logprobs: deepseekChatLogprobsSchema,
169
214
  finish_reason: z.string().nullish(),
170
215
  }),
171
216
  ),
@@ -2,31 +2,57 @@ import { z } from 'zod/v4';
2
2
 
3
3
  // https://api-docs.deepseek.com/quick_start/pricing
4
4
  export type DeepSeekChatModelId =
5
- | 'deepseek-chat'
6
- | 'deepseek-reasoner'
5
+ | 'deepseek-v4-flash'
6
+ | 'deepseek-v4-pro'
7
7
  | 'deepseek-v4-flash-vision-exp'
8
+ // Retired aliases remain assignable through the string escape hatch, but are
9
+ // intentionally omitted from first-class editor suggestions.
8
10
  | (string & {});
9
11
 
10
12
  export const deepseekLanguageModelChatOptions = z.object({
11
13
  /**
12
- * Type of thinking to use. Defaults to `enabled`.
14
+ * Whether to return log probabilities for generated tokens.
15
+ */
16
+ logprobs: z.boolean().optional(),
17
+
18
+ /**
19
+ * Number of most likely tokens to return at each token position.
20
+ *
21
+ * Setting this option automatically enables `logprobs`.
22
+ */
23
+ topLogprobs: z.number().int().min(0).max(20).optional(),
24
+
25
+ /**
26
+ * An opaque identifier for the end user. DeepSeek uses this identifier for
27
+ * content-safety tracing and request isolation.
13
28
  *
14
- * See https://api-docs.deepseek.com/guides/thinking_mode for the
15
- * `adaptive` option, which lets the model decide when to think.
29
+ * Must contain only ASCII letters, numbers, underscores, and hyphens, and
30
+ * must be at most 512 characters long.
31
+ */
32
+ userId: z
33
+ .string()
34
+ .regex(/^[a-zA-Z0-9_-]+$/, 'userId must match /^[a-zA-Z0-9_-]+$/')
35
+ .max(512, 'userId must be at most 512 characters long')
36
+ .optional(),
37
+
38
+ /**
39
+ * Type of thinking to use. Defaults to `enabled`.
16
40
  */
17
41
  thinking: z
18
42
  .object({
43
+ // `adaptive` is accepted at runtime for backwards compatibility and
44
+ // mapped to `enabled`, but is intentionally excluded from the exported
45
+ // provider options type.
19
46
  type: z.enum(['adaptive', 'enabled', 'disabled']).optional(),
20
47
  })
21
48
  .optional(),
22
49
 
23
50
  /**
24
51
  * Controls the thinking strength for DeepSeek V4 reasoning models.
25
- *
26
- * DeepSeek's API accepts `low`, `medium`, `high`, `xhigh`, and `max`.
27
- * Per their docs, `low` and `medium` are mapped to `high`, and `xhigh`
28
- * is mapped to `max` server-side for compatibility with other providers.
29
52
  */
53
+ // `medium` and `xhigh` are accepted at runtime for backwards compatibility
54
+ // and mapped to canonical DeepSeek values, but are intentionally excluded
55
+ // from the exported provider options type.
30
56
  reasoningEffort: z.enum(['low', 'medium', 'high', 'xhigh', 'max']).optional(),
31
57
 
32
58
  /**
@@ -37,6 +63,71 @@ export const deepseekLanguageModelChatOptions = z.object({
37
63
  strictJsonSchema: z.boolean().optional(),
38
64
  });
39
65
 
40
- export type DeepSeekLanguageModelChatOptions = z.infer<
41
- typeof deepseekLanguageModelChatOptions
66
+ export type DeepSeekLanguageModelChatOptions = {
67
+ /**
68
+ * Whether to return log probabilities for generated tokens.
69
+ */
70
+ logprobs?: boolean;
71
+
72
+ /**
73
+ * Number of most likely tokens to return at each token position.
74
+ *
75
+ * Setting this option automatically enables `logprobs`.
76
+ */
77
+ topLogprobs?: number;
78
+
79
+ /**
80
+ * An opaque identifier for the end user. DeepSeek uses this identifier for
81
+ * content-safety tracing and request isolation.
82
+ *
83
+ * Must contain only ASCII letters, numbers, underscores, and hyphens, and
84
+ * must be at most 512 characters long.
85
+ */
86
+ userId?: string;
87
+
88
+ /**
89
+ * Controls whether thinking mode is enabled. Defaults to `enabled`.
90
+ */
91
+ thinking?: {
92
+ type?: 'enabled' | 'disabled';
93
+ };
94
+
95
+ /**
96
+ * Controls the thinking strength for DeepSeek V4 reasoning models.
97
+ */
98
+ reasoningEffort?: 'low' | 'high' | 'max';
99
+
100
+ /**
101
+ * Whether to use strict JSON schema validation for structured outputs.
102
+ * Only applies when the serving endpoint supports JSON schema response
103
+ * formats (e.g. Azure). Defaults to `true`.
104
+ */
105
+ strictJsonSchema?: boolean;
106
+ };
107
+
108
+ export const deepseekMessageProviderOptions = z.object({
109
+ /**
110
+ * The name of the participant represented by the message.
111
+ *
112
+ * Supported on system, user, and assistant messages.
113
+ */
114
+ name: z.string().optional(),
115
+ });
116
+
117
+ export type DeepSeekMessageProviderOptions = z.infer<
118
+ typeof deepseekMessageProviderOptions
119
+ >;
120
+
121
+ export const deepseekAssistantMessageProviderOptions =
122
+ deepseekMessageProviderOptions.extend({
123
+ /**
124
+ * Whether the assistant message content is a prefix that DeepSeek should
125
+ * continue. This beta feature is only supported on the final assistant
126
+ * message when using a beta base URL.
127
+ */
128
+ prefix: z.literal(true).optional(),
129
+ });
130
+
131
+ export type DeepSeekAssistantMessageProviderOptions = z.infer<
132
+ typeof deepseekAssistantMessageProviderOptions
42
133
  >;