@namzu/deepseek 0.1.0
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.
- package/CHANGELOG.md +21 -0
- package/LICENSE.md +110 -0
- package/README.md +89 -0
- package/dist/client.d.ts +68 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +380 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
- package/src/client.ts +475 -0
- package/src/index.ts +35 -0
- package/src/types.ts +52 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChatCompletionParams,
|
|
3
|
+
ChatCompletionResponse,
|
|
4
|
+
LLMProvider,
|
|
5
|
+
ModelInfo,
|
|
6
|
+
ProviderCapabilities,
|
|
7
|
+
ReasoningBlock,
|
|
8
|
+
StreamChunk,
|
|
9
|
+
ThinkingConfig,
|
|
10
|
+
TokenUsage,
|
|
11
|
+
ToolChoice,
|
|
12
|
+
} from '@namzu/sdk'
|
|
13
|
+
import {
|
|
14
|
+
ProviderRequestError,
|
|
15
|
+
attributionHeaders,
|
|
16
|
+
isCallerAbortError,
|
|
17
|
+
isProviderRequestError,
|
|
18
|
+
providerVendorError,
|
|
19
|
+
toolResultToText,
|
|
20
|
+
} from '@namzu/sdk'
|
|
21
|
+
import OpenAI from 'openai'
|
|
22
|
+
import type {
|
|
23
|
+
ChatCompletionMessageParam,
|
|
24
|
+
ChatCompletionTool,
|
|
25
|
+
ChatCompletionToolChoiceOption,
|
|
26
|
+
} from 'openai/resources/chat/completions'
|
|
27
|
+
import type { DeepSeekConfig } from './types.js'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* No vision, no documents.
|
|
31
|
+
*
|
|
32
|
+
* The wire is OpenAI's Chat Completions, which has `image_url` content parts —
|
|
33
|
+
* but the models behind this endpoint are text-only, and a capability set is a
|
|
34
|
+
* claim about what a call will actually do, not about what the request format
|
|
35
|
+
* can express. Claiming vision here would route an image-bearing run to a
|
|
36
|
+
* driver that sends the image and gets prose about nothing.
|
|
37
|
+
*/
|
|
38
|
+
export const DEEPSEEK_CAPABILITIES: ProviderCapabilities = {
|
|
39
|
+
supportsTools: true,
|
|
40
|
+
supportsStreaming: true,
|
|
41
|
+
supportsFunctionCalling: true,
|
|
42
|
+
supportsVision: false,
|
|
43
|
+
supportsDocuments: false,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEFAULT_BASE_URL = 'https://api.deepseek.com'
|
|
47
|
+
|
|
48
|
+
type DeepSeekFinishReason =
|
|
49
|
+
| 'stop'
|
|
50
|
+
| 'length'
|
|
51
|
+
| 'tool_calls'
|
|
52
|
+
| 'content_filter'
|
|
53
|
+
| 'insufficient_system_resource'
|
|
54
|
+
|
|
55
|
+
function mapFinishReason(
|
|
56
|
+
reason: DeepSeekFinishReason | string | null | undefined,
|
|
57
|
+
): ChatCompletionResponse['finishReason'] {
|
|
58
|
+
switch (reason) {
|
|
59
|
+
case 'length':
|
|
60
|
+
return 'length'
|
|
61
|
+
case 'tool_calls':
|
|
62
|
+
return 'tool_calls'
|
|
63
|
+
case 'content_filter':
|
|
64
|
+
return 'content_filter'
|
|
65
|
+
default:
|
|
66
|
+
return 'stop'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Usage, including the two counts this vendor reports that OpenAI's shape does
|
|
72
|
+
* not.
|
|
73
|
+
*
|
|
74
|
+
* `completion_tokens_details.reasoning_tokens` is billed as output and is not
|
|
75
|
+
* separable from it after the fact, so it is surfaced rather than discarded —
|
|
76
|
+
* a thinking-mode run whose reasoning dwarfs its answer looks like an
|
|
77
|
+
* inexplicably expensive short reply otherwise.
|
|
78
|
+
*
|
|
79
|
+
* `prompt_cache_hit_tokens` is the vendor's own name for what
|
|
80
|
+
* `prompt_tokens_details.cached_tokens` also reports. Both are read, the
|
|
81
|
+
* details field first, because the flat one is the older spelling and a
|
|
82
|
+
* gateway may forward only one.
|
|
83
|
+
*/
|
|
84
|
+
interface RawDeepSeekUsage {
|
|
85
|
+
prompt_tokens?: number
|
|
86
|
+
completion_tokens?: number
|
|
87
|
+
total_tokens?: number
|
|
88
|
+
prompt_tokens_details?: { cached_tokens?: number }
|
|
89
|
+
prompt_cache_hit_tokens?: number
|
|
90
|
+
completion_tokens_details?: { reasoning_tokens?: number }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseUsage(raw?: RawDeepSeekUsage | null): TokenUsage {
|
|
94
|
+
if (!raw) {
|
|
95
|
+
return {
|
|
96
|
+
promptTokens: 0,
|
|
97
|
+
completionTokens: 0,
|
|
98
|
+
totalTokens: 0,
|
|
99
|
+
cachedTokens: 0,
|
|
100
|
+
cacheWriteTokens: 0,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const promptTokens = raw.prompt_tokens ?? 0
|
|
104
|
+
const completionTokens = raw.completion_tokens ?? 0
|
|
105
|
+
const reasoningTokens = raw.completion_tokens_details?.reasoning_tokens
|
|
106
|
+
return {
|
|
107
|
+
promptTokens,
|
|
108
|
+
completionTokens,
|
|
109
|
+
totalTokens: raw.total_tokens ?? promptTokens + completionTokens,
|
|
110
|
+
cachedTokens: raw.prompt_tokens_details?.cached_tokens ?? raw.prompt_cache_hit_tokens ?? 0,
|
|
111
|
+
// The vendor has no write-side cache charge: a cache entry is created as
|
|
112
|
+
// a side effect of a miss, at the ordinary input rate. Reporting a
|
|
113
|
+
// fabricated number here would be worse than the zero.
|
|
114
|
+
cacheWriteTokens: 0,
|
|
115
|
+
...(reasoningTokens !== undefined ? { reasoningTokens } : {}),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatToolChoice(tc: ToolChoice | undefined): ChatCompletionToolChoiceOption | undefined {
|
|
120
|
+
if (tc === undefined) return undefined
|
|
121
|
+
if (tc === 'auto' || tc === 'none' || tc === 'required') return tc
|
|
122
|
+
if (typeof tc === 'object' && tc.type === 'function') {
|
|
123
|
+
return { type: 'function', function: { name: tc.function.name } }
|
|
124
|
+
}
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The four the vendor accepts in thinking mode and applies none of. */
|
|
129
|
+
const SAMPLING_FIELDS = ['temperature', 'topP', 'frequencyPenalty', 'presencePenalty'] as const
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Is thinking on for this call?
|
|
133
|
+
*
|
|
134
|
+
* **Absent means ON.** That is the vendor's default, not a guess: a request
|
|
135
|
+
* with no `thinking` key comes back carrying `reasoning_content`. Every rule
|
|
136
|
+
* below that turns on "thinking is enabled" has to read it this way, or it
|
|
137
|
+
* applies to explicit callers only and misses the common case entirely.
|
|
138
|
+
*/
|
|
139
|
+
export function thinkingEnabled(thinking: ThinkingConfig | undefined): boolean {
|
|
140
|
+
return thinking?.type !== 'disabled'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Refuse a sampling parameter that thinking mode will discard.
|
|
145
|
+
*
|
|
146
|
+
* Measured against the live API on 2026-08-17 rather than read off the
|
|
147
|
+
* documentation, which says only that these are "not supported": all four are
|
|
148
|
+
* accepted with HTTP 200 and applied to nothing. So a caller who pinned
|
|
149
|
+
* `temperature: 0` for reproducibility gets sampling, no error, and no way to
|
|
150
|
+
* find out — `refuse-do-not-degrade`, exactly.
|
|
151
|
+
*
|
|
152
|
+
* `samplingInThinkingMode: 'ignore'` opts out, for a host that keeps one
|
|
153
|
+
* parameter set across several providers and would rather this one drop them
|
|
154
|
+
* quietly than make it special.
|
|
155
|
+
*/
|
|
156
|
+
export function assertSamplingUsable(
|
|
157
|
+
params: ChatCompletionParams,
|
|
158
|
+
mode: 'refuse' | 'ignore',
|
|
159
|
+
): void {
|
|
160
|
+
if (mode === 'ignore') return
|
|
161
|
+
if (!thinkingEnabled(params.thinking)) return
|
|
162
|
+
const set = SAMPLING_FIELDS.filter((f) => params[f] !== undefined)
|
|
163
|
+
if (set.length === 0) return
|
|
164
|
+
throw new Error(
|
|
165
|
+
`DeepSeekProvider: ${set.join(', ')} ${set.length === 1 ? 'is' : 'are'} ignored in thinking mode, which is on for this call — the vendor accepts them and applies none of them, so the call would silently sample when you asked it not to. Disable thinking for this call (\`thinking: { type: 'disabled' }\`) to have them honoured, drop them, or set \`samplingInThinkingMode: 'ignore'\` on the provider to send them anyway.`,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Refuse a reasoning effort this wire cannot carry.
|
|
171
|
+
*
|
|
172
|
+
* `thinking.effort` is accepted here and validated against nothing — measured:
|
|
173
|
+
* `effort: 'bogus'` returns 200, and `effort: 'none'` still produces reasoning
|
|
174
|
+
* tokens. Only `thinking.type` is validated (the vendor's own error names its
|
|
175
|
+
* variants: `adaptive`, `enabled`, `disabled`). So an effort sent on this wire
|
|
176
|
+
* is a parameter accepted and discarded, which is the one thing a driver must
|
|
177
|
+
* not do quietly.
|
|
178
|
+
*
|
|
179
|
+
* The vendor's Anthropic-format endpoint does take `reasoning.effort`. A
|
|
180
|
+
* driver built on that wire could honour this; this one is built on Chat
|
|
181
|
+
* Completions and says so.
|
|
182
|
+
*/
|
|
183
|
+
export function assertEffortUnsupported(params: ChatCompletionParams): void {
|
|
184
|
+
if (params.effort === undefined) return
|
|
185
|
+
throw new Error(
|
|
186
|
+
`DeepSeekProvider: \`effort\` is not carried by this endpoint. The vendor's Chat Completions wire validates \`thinking.type\` and ignores any effort sent beside it, so passing one through would change nothing and report success. Use \`thinking: { type: 'adaptive' | 'enabled' | 'disabled' }\` to steer this driver.`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The reasoning blocks an assistant message carries, in wire form.
|
|
192
|
+
*
|
|
193
|
+
* The vendor's rule: with tool calls in play, an assistant turn's
|
|
194
|
+
* `reasoning_content` **must** be replayed in every later turn of the same
|
|
195
|
+
* flow; without tool calls it is ignored if sent. Replayed unconditionally
|
|
196
|
+
* here, which satisfies both halves and matches what `AssistantMessage.reasoning`
|
|
197
|
+
* already promises — "replayed verbatim and ahead of the text/tool blocks".
|
|
198
|
+
*
|
|
199
|
+
* Measured note, because it matters to anyone debugging this: as of
|
|
200
|
+
* 2026-08-17 omitting the replay does NOT produce the documented 400 on either
|
|
201
|
+
* `deepseek-v4-flash` or `deepseek-v4-pro`. The rule is still followed, because
|
|
202
|
+
* a contract the vendor states and does not currently enforce is a contract
|
|
203
|
+
* that can start being enforced in any release.
|
|
204
|
+
*/
|
|
205
|
+
function replayReasoning(reasoning: readonly ReasoningBlock[] | undefined): string | undefined {
|
|
206
|
+
if (!reasoning || reasoning.length === 0) return undefined
|
|
207
|
+
const text = reasoning
|
|
208
|
+
.filter((b) => b.type === 'thinking' && typeof b.text === 'string')
|
|
209
|
+
.map((b) => b.text as string)
|
|
210
|
+
.join('')
|
|
211
|
+
return text.length > 0 ? text : undefined
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function toDeepSeekMessages(
|
|
215
|
+
messages: ChatCompletionParams['messages'],
|
|
216
|
+
): ChatCompletionMessageParam[] {
|
|
217
|
+
return messages.map((msg): ChatCompletionMessageParam => {
|
|
218
|
+
if (msg.role === 'system') return { role: 'system', content: msg.content }
|
|
219
|
+
if (msg.role === 'user') {
|
|
220
|
+
// No attachment branch, deliberately. `DEEPSEEK_CAPABILITIES` says
|
|
221
|
+
// this driver has neither vision nor documents, and a driver that
|
|
222
|
+
// quietly encoded an image the model cannot read would answer about
|
|
223
|
+
// a picture it never saw.
|
|
224
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
'DeepSeekProvider: this driver has no vision or document support, and a message carried attachments. Route multimodal turns to a provider whose capabilities include them.',
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return { role: 'user', content: msg.content }
|
|
230
|
+
}
|
|
231
|
+
if (msg.role === 'tool') {
|
|
232
|
+
return {
|
|
233
|
+
role: 'tool',
|
|
234
|
+
content: toolResultToText(msg.content),
|
|
235
|
+
tool_call_id: msg.toolCallId,
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const assistant: ChatCompletionMessageParam = { role: 'assistant', content: msg.content }
|
|
239
|
+
if ('toolCalls' in msg && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
240
|
+
;(assistant as { tool_calls?: unknown }).tool_calls = msg.toolCalls.map((tc) => ({
|
|
241
|
+
id: tc.id,
|
|
242
|
+
type: 'function' as const,
|
|
243
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
const replayed = replayReasoning((msg as { reasoning?: readonly ReasoningBlock[] }).reasoning)
|
|
247
|
+
if (replayed !== undefined) {
|
|
248
|
+
;(assistant as { reasoning_content?: string }).reasoning_content = replayed
|
|
249
|
+
}
|
|
250
|
+
return assistant
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function toDeepSeekTools(params: ChatCompletionParams): ChatCompletionTool[] | undefined {
|
|
255
|
+
if (!params.tools || params.tools.length === 0) return undefined
|
|
256
|
+
// No `strict` branch: `enforceToolInputSchema` names tools whose schema
|
|
257
|
+
// should be enforced rather than suggested, and this wire has no field for
|
|
258
|
+
// it. Setting `strict: true` here would be accepted and ignored — the same
|
|
259
|
+
// defect `assertEffortUnsupported` refuses above — so the flag is left to
|
|
260
|
+
// the kernel's own post-call Zod validation, which runs either way.
|
|
261
|
+
return params.tools.map((t) => ({
|
|
262
|
+
type: 'function' as const,
|
|
263
|
+
function: {
|
|
264
|
+
name: t.function.name,
|
|
265
|
+
description: t.function.description ?? '',
|
|
266
|
+
parameters: (t.function.parameters ?? {}) as Record<string, unknown>,
|
|
267
|
+
},
|
|
268
|
+
}))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export class DeepSeekProvider implements LLMProvider {
|
|
272
|
+
readonly id = 'deepseek'
|
|
273
|
+
readonly name = 'DeepSeek'
|
|
274
|
+
readonly capabilities = DEEPSEEK_CAPABILITIES
|
|
275
|
+
|
|
276
|
+
private client: OpenAI
|
|
277
|
+
private defaultModel?: string
|
|
278
|
+
private samplingMode: 'refuse' | 'ignore'
|
|
279
|
+
|
|
280
|
+
constructor(config: DeepSeekConfig) {
|
|
281
|
+
if (!config.apiKey) {
|
|
282
|
+
throw new Error('DeepSeek API key is required. Set DEEPSEEK_API_KEY env variable.')
|
|
283
|
+
}
|
|
284
|
+
const clientOptions: ConstructorParameters<typeof OpenAI>[0] = {
|
|
285
|
+
apiKey: config.apiKey,
|
|
286
|
+
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
|
|
287
|
+
}
|
|
288
|
+
if (config.timeout !== undefined) clientOptions.timeout = config.timeout
|
|
289
|
+
clientOptions.defaultHeaders = { ...attributionHeaders(), ...(config.defaultHeaders ?? {}) }
|
|
290
|
+
|
|
291
|
+
this.client = new OpenAI(clientOptions)
|
|
292
|
+
this.defaultModel = config.model
|
|
293
|
+
this.samplingMode = config.samplingInThinkingMode ?? 'refuse'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private resolveModel(params: ChatCompletionParams): string {
|
|
297
|
+
const model = params.model || this.defaultModel
|
|
298
|
+
if (!model) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
'DeepSeekProvider: model is required. Pass `model` in config or on the chat call.',
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
return model
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
307
|
+
const model = this.resolveModel(params)
|
|
308
|
+
assertEffortUnsupported(params)
|
|
309
|
+
assertSamplingUsable(params, this.samplingMode)
|
|
310
|
+
|
|
311
|
+
const sampling = thinkingEnabled(params.thinking)
|
|
312
|
+
? {}
|
|
313
|
+
: {
|
|
314
|
+
temperature: params.temperature,
|
|
315
|
+
top_p: params.topP,
|
|
316
|
+
frequency_penalty: params.frequencyPenalty,
|
|
317
|
+
presence_penalty: params.presencePenalty,
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Built as its own object so `stream: true` keeps its literal type and the
|
|
321
|
+
// SDK's streaming overload is the one selected. Casting the whole
|
|
322
|
+
// argument — the obvious way to smuggle `thinking` past a type that does
|
|
323
|
+
// not know about it — widens `stream` to `boolean` and the call resolves
|
|
324
|
+
// to the non-streaming overload, whose result has no async iterator.
|
|
325
|
+
const body = {
|
|
326
|
+
model,
|
|
327
|
+
messages: toDeepSeekMessages(params.messages),
|
|
328
|
+
stream: true as const,
|
|
329
|
+
stream_options: { include_usage: true },
|
|
330
|
+
tools: toDeepSeekTools(params),
|
|
331
|
+
tool_choice: formatToolChoice(params.toolChoice),
|
|
332
|
+
max_tokens: params.maxTokens,
|
|
333
|
+
stop: params.stop,
|
|
334
|
+
response_format: params.responseFormat,
|
|
335
|
+
...sampling,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Sent only when the caller said something. Absent means the vendor's own
|
|
339
|
+
// default, which is thinking ON — see `thinkingEnabled`. The three
|
|
340
|
+
// variants are the vendor's, and they are the same three the SDK's
|
|
341
|
+
// `ThinkingConfig` declares, so this is a rename and not a mapping.
|
|
342
|
+
//
|
|
343
|
+
// Cast back to `typeof body` rather than to the SDK's parameter type: the
|
|
344
|
+
// extra key rides along at runtime, and the literal `stream: true`
|
|
345
|
+
// survives, which is what keeps the streaming overload selected.
|
|
346
|
+
const withThinking = (
|
|
347
|
+
params.thinking ? { ...body, thinking: { type: params.thinking.type } } : body
|
|
348
|
+
) as typeof body
|
|
349
|
+
|
|
350
|
+
let stream: Awaited<ReturnType<typeof this.client.chat.completions.create>>
|
|
351
|
+
try {
|
|
352
|
+
stream = await this.client.chat.completions.create(withThinking, {
|
|
353
|
+
signal: params.signal,
|
|
354
|
+
})
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
|
|
357
|
+
if (isProviderRequestError(err)) throw err
|
|
358
|
+
throw providerVendorError({ providerId: 'deepseek', error: err })
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// One reasoning block per turn on this wire: the vendor streams
|
|
362
|
+
// `reasoning_content` as a flat run of deltas with no index and no
|
|
363
|
+
// block boundaries, so index 0 is the whole of it rather than a guess.
|
|
364
|
+
let reasoningOpen = false
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
for await (const chunk of stream) {
|
|
368
|
+
params.signal?.throwIfAborted()
|
|
369
|
+
try {
|
|
370
|
+
const choice = chunk.choices[0]
|
|
371
|
+
const delta = choice?.delta as
|
|
372
|
+
| { content?: string | null; reasoning_content?: string | null; tool_calls?: unknown[] }
|
|
373
|
+
| undefined
|
|
374
|
+
|
|
375
|
+
const toolCalls = (
|
|
376
|
+
delta?.tool_calls as
|
|
377
|
+
| Array<{
|
|
378
|
+
index: number
|
|
379
|
+
id?: string
|
|
380
|
+
type?: 'function'
|
|
381
|
+
function?: { name?: string; arguments?: string }
|
|
382
|
+
}>
|
|
383
|
+
| undefined
|
|
384
|
+
)?.map((tc) => ({
|
|
385
|
+
index: tc.index,
|
|
386
|
+
id: tc.id,
|
|
387
|
+
type: tc.type,
|
|
388
|
+
function: tc.function
|
|
389
|
+
? { name: tc.function.name, arguments: tc.function.arguments }
|
|
390
|
+
: undefined,
|
|
391
|
+
}))
|
|
392
|
+
|
|
393
|
+
const reasoningText = delta?.reasoning_content
|
|
394
|
+
const finishReason = choice?.finish_reason
|
|
395
|
+
? mapFinishReason(choice.finish_reason)
|
|
396
|
+
: undefined
|
|
397
|
+
const usage = chunk.usage ? parseUsage(chunk.usage as RawDeepSeekUsage) : undefined
|
|
398
|
+
|
|
399
|
+
if (reasoningText) {
|
|
400
|
+
reasoningOpen = true
|
|
401
|
+
yield {
|
|
402
|
+
id: chunk.id,
|
|
403
|
+
delta: { reasoning: { index: 0, type: 'thinking', text: reasoningText } },
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// The block closes when content or a tool call starts, which is
|
|
408
|
+
// the only boundary this wire gives. Without it a consumer's
|
|
409
|
+
// reasoning pane never receives `done` and stays open for the
|
|
410
|
+
// life of the run.
|
|
411
|
+
const contentStarting =
|
|
412
|
+
(delta?.content !== undefined && delta.content !== null) ||
|
|
413
|
+
(toolCalls !== undefined && toolCalls.length > 0)
|
|
414
|
+
if (reasoningOpen && contentStarting) {
|
|
415
|
+
reasoningOpen = false
|
|
416
|
+
yield { id: chunk.id, delta: { reasoning: { index: 0, done: true } } }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const hasDelta =
|
|
420
|
+
(delta?.content !== undefined && delta.content !== null) ||
|
|
421
|
+
(toolCalls !== undefined && toolCalls.length > 0)
|
|
422
|
+
if (!hasDelta && !finishReason && !usage) continue
|
|
423
|
+
|
|
424
|
+
yield {
|
|
425
|
+
id: chunk.id,
|
|
426
|
+
delta: { content: delta?.content ?? undefined, toolCalls },
|
|
427
|
+
finishReason,
|
|
428
|
+
usage,
|
|
429
|
+
}
|
|
430
|
+
} catch (parseErr) {
|
|
431
|
+
if (isProviderRequestError(parseErr)) throw parseErr
|
|
432
|
+
throw new ProviderRequestError({
|
|
433
|
+
kind: 'server',
|
|
434
|
+
providerId: 'deepseek',
|
|
435
|
+
detail: 'the provider stream returned malformed data',
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// A turn that was all reasoning and no content — the model thought
|
|
440
|
+
// and then stopped — still has to close its block.
|
|
441
|
+
if (reasoningOpen) {
|
|
442
|
+
yield { id: 'deepseek-reasoning-close', delta: { reasoning: { index: 0, done: true } } }
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
|
|
446
|
+
if (isProviderRequestError(err)) throw err
|
|
447
|
+
throw providerVendorError({ providerId: 'deepseek', error: err })
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async probeCredential(): Promise<void> {
|
|
452
|
+
await this.client.models.list()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async listModels(): Promise<ModelInfo[]> {
|
|
456
|
+
const page = await this.client.models.list()
|
|
457
|
+
return page.data.map((m) => ({
|
|
458
|
+
id: m.id,
|
|
459
|
+
name: m.id,
|
|
460
|
+
inputPrice: 0,
|
|
461
|
+
outputPrice: 0,
|
|
462
|
+
supportsToolUse: true,
|
|
463
|
+
supportsStreaming: true,
|
|
464
|
+
}))
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async healthCheck(): Promise<boolean> {
|
|
468
|
+
try {
|
|
469
|
+
await this.client.models.list()
|
|
470
|
+
return true
|
|
471
|
+
} catch {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ProviderRegistry, type RegisterOptions } from '@namzu/sdk'
|
|
2
|
+
import { DEEPSEEK_CAPABILITIES, DeepSeekProvider } from './client.js'
|
|
3
|
+
import type { DeepSeekProviderConfig } from './types.js'
|
|
4
|
+
|
|
5
|
+
// Module augmentation: register deepseek's config type in the sdk's registry
|
|
6
|
+
// interface. This must live inside index.ts (not a .d.ts) so it executes when
|
|
7
|
+
// index.ts is imported.
|
|
8
|
+
declare module '@namzu/sdk' {
|
|
9
|
+
interface ProviderConfigRegistry {
|
|
10
|
+
deepseek: DeepSeekProviderConfig
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register `DeepSeekProvider` under the `'deepseek'` type in `@namzu/sdk`'s
|
|
16
|
+
* `ProviderRegistry`. Call once at app startup before
|
|
17
|
+
* `ProviderRegistry.create({ type: 'deepseek', ... })`.
|
|
18
|
+
*
|
|
19
|
+
* Throws `DuplicateProviderError` if `'deepseek'` is already registered.
|
|
20
|
+
* Pass `{ replace: true }` to override.
|
|
21
|
+
*/
|
|
22
|
+
export function registerDeepSeek(options?: RegisterOptions): void {
|
|
23
|
+
ProviderRegistry.register('deepseek', DeepSeekProvider, DEEPSEEK_CAPABILITIES, options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
DEEPSEEK_CAPABILITIES,
|
|
28
|
+
DeepSeekProvider,
|
|
29
|
+
assertEffortUnsupported,
|
|
30
|
+
assertSamplingUsable,
|
|
31
|
+
thinkingEnabled,
|
|
32
|
+
toDeepSeekMessages,
|
|
33
|
+
toDeepSeekTools,
|
|
34
|
+
} from './client.js'
|
|
35
|
+
export type { DeepSeekConfig, DeepSeekProviderConfig } from './types.js'
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek-specific provider config shapes.
|
|
3
|
+
*
|
|
4
|
+
* `DeepSeekConfig` is the constructor input for `DeepSeekProvider` (no
|
|
5
|
+
* discriminator). `DeepSeekProviderConfig` is the shape the consumer passes to
|
|
6
|
+
* `ProviderRegistry.create({ type: 'deepseek', ... })` — it extends
|
|
7
|
+
* `DeepSeekConfig` with the `type: 'deepseek'` discriminator for the registry's
|
|
8
|
+
* generic narrowing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface DeepSeekConfig {
|
|
12
|
+
apiKey: string
|
|
13
|
+
/** Default model. Can be overridden per-call via ChatCompletionParams.model. */
|
|
14
|
+
model?: string
|
|
15
|
+
/**
|
|
16
|
+
* Override the base URL.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to the vendor's own endpoint. Set this for a gateway that
|
|
19
|
+
* fronts DeepSeek — but note that a gateway which strips unknown response
|
|
20
|
+
* fields will drop `reasoning_content`, and this driver's whole reason to
|
|
21
|
+
* exist is that field.
|
|
22
|
+
*/
|
|
23
|
+
baseURL?: string
|
|
24
|
+
/** Request timeout in ms. */
|
|
25
|
+
timeout?: number
|
|
26
|
+
/** Custom headers appended to every request. */
|
|
27
|
+
defaultHeaders?: Record<string, string>
|
|
28
|
+
/**
|
|
29
|
+
* What to do when a caller sets a sampling parameter that thinking mode
|
|
30
|
+
* ignores — `temperature`, `topP`, `frequencyPenalty`, `presencePenalty`.
|
|
31
|
+
*
|
|
32
|
+
* The vendor accepts all four in thinking mode and applies none of them:
|
|
33
|
+
* no error, no warning, no effect. Measured against the live API on
|
|
34
|
+
* 2026-08-17, not read off the documentation, which says only that they
|
|
35
|
+
* are "not supported".
|
|
36
|
+
*
|
|
37
|
+
* `'refuse'` (the default) throws, because a caller who set `temperature:
|
|
38
|
+
* 0` and got sampling anyway believes they pinned something they did not
|
|
39
|
+
* — the exact shape of `refuse-do-not-degrade`. `'ignore'` sends them and
|
|
40
|
+
* lets the vendor discard them, for a host that would rather have one
|
|
41
|
+
* knob it can leave set across providers.
|
|
42
|
+
*
|
|
43
|
+
* Thinking is ON by default at the vendor, so this fires more often than
|
|
44
|
+
* you might expect. Disable thinking per call (`thinking: { type:
|
|
45
|
+
* 'disabled' }`) and the parameters are honoured normally.
|
|
46
|
+
*/
|
|
47
|
+
samplingInThinkingMode?: 'refuse' | 'ignore'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DeepSeekProviderConfig extends DeepSeekConfig {
|
|
51
|
+
type: 'deepseek'
|
|
52
|
+
}
|