@namzu/deepseek 0.1.0 → 1.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 +183 -0
- package/README.md +26 -9
- package/dist/client.d.ts +7 -13
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +255 -60
- package/dist/client.js.map +1 -1
- package/package.json +13 -14
- package/src/client.ts +313 -61
package/src/client.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChatCompletionParams,
|
|
3
3
|
ChatCompletionResponse,
|
|
4
|
+
ImageAttachment,
|
|
4
5
|
LLMProvider,
|
|
5
6
|
ModelInfo,
|
|
7
|
+
ModelInputModality,
|
|
6
8
|
ProviderCapabilities,
|
|
9
|
+
ProviderRoute,
|
|
7
10
|
ReasoningBlock,
|
|
11
|
+
ReasoningEffort,
|
|
8
12
|
StreamChunk,
|
|
9
13
|
ThinkingConfig,
|
|
10
14
|
TokenUsage,
|
|
11
15
|
ToolChoice,
|
|
16
|
+
ToolResultContent,
|
|
12
17
|
} from '@namzu/sdk'
|
|
13
18
|
import {
|
|
14
19
|
ProviderRequestError,
|
|
@@ -16,7 +21,7 @@ import {
|
|
|
16
21
|
isCallerAbortError,
|
|
17
22
|
isProviderRequestError,
|
|
18
23
|
providerVendorError,
|
|
19
|
-
|
|
24
|
+
toToolResultBlocks,
|
|
20
25
|
} from '@namzu/sdk'
|
|
21
26
|
import OpenAI from 'openai'
|
|
22
27
|
import type {
|
|
@@ -26,24 +31,29 @@ import type {
|
|
|
26
31
|
} from 'openai/resources/chat/completions'
|
|
27
32
|
import type { DeepSeekConfig } from './types.js'
|
|
28
33
|
|
|
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
|
-
*/
|
|
34
|
+
/** Driver-level mapping capabilities; model input support is listed separately. */
|
|
38
35
|
export const DEEPSEEK_CAPABILITIES: ProviderCapabilities = {
|
|
39
36
|
supportsTools: true,
|
|
40
37
|
supportsStreaming: true,
|
|
41
38
|
supportsFunctionCalling: true,
|
|
42
|
-
supportsVision:
|
|
39
|
+
supportsVision: true,
|
|
43
40
|
supportsDocuments: false,
|
|
44
41
|
}
|
|
45
42
|
|
|
46
43
|
const DEFAULT_BASE_URL = 'https://api.deepseek.com'
|
|
44
|
+
const VISION_MODEL = 'deepseek-v4-flash-vision-exp'
|
|
45
|
+
const TEXT_MODALITIES = Object.freeze(['text'] as const)
|
|
46
|
+
const VISION_MODALITIES = Object.freeze(['text', 'image'] as const)
|
|
47
|
+
const IMAGE_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
|
|
48
|
+
const KNOWN_MODELS = Object.freeze([
|
|
49
|
+
{ id: 'deepseek-v4-flash', name: 'DeepSeek-V4-Flash', inputModalities: TEXT_MODALITIES },
|
|
50
|
+
{ id: 'deepseek-v4-pro', name: 'DeepSeek-V4-Pro', inputModalities: TEXT_MODALITIES },
|
|
51
|
+
{
|
|
52
|
+
id: VISION_MODEL,
|
|
53
|
+
name: 'DeepSeek-V4-Flash-Vision-Exp',
|
|
54
|
+
inputModalities: VISION_MODALITIES,
|
|
55
|
+
},
|
|
56
|
+
] as const)
|
|
47
57
|
|
|
48
58
|
type DeepSeekFinishReason =
|
|
49
59
|
| 'stop'
|
|
@@ -188,13 +198,87 @@ export function assertEffortUnsupported(params: ChatCompletionParams): void {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
|
-
*
|
|
201
|
+
* Adapter-private state for one completed reasoning response.
|
|
202
|
+
*
|
|
203
|
+
* The route is repeated inside the opaque envelope rather than trusted from
|
|
204
|
+
* `AssistantMessage.source`: durable content is authoritative, and native
|
|
205
|
+
* metadata is used only after the two independent records agree.
|
|
206
|
+
*/
|
|
207
|
+
interface DeepSeekReplayState {
|
|
208
|
+
readonly kind: 'namzu-deepseek-reasoning'
|
|
209
|
+
readonly version: 1
|
|
210
|
+
readonly route: ProviderRoute
|
|
211
|
+
readonly reasoningContent: string
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sameRoute(left: ProviderRoute, right: ProviderRoute): boolean {
|
|
215
|
+
return (
|
|
216
|
+
left.providerId === right.providerId &&
|
|
217
|
+
left.model === right.model &&
|
|
218
|
+
left.chainIndex === right.chainIndex
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveDeepSeekRoute(model: string, candidate: ProviderRoute | undefined): ProviderRoute {
|
|
223
|
+
if (candidate === undefined) return { providerId: 'deepseek', model, chainIndex: 0 }
|
|
224
|
+
if (
|
|
225
|
+
candidate.providerId !== 'deepseek' ||
|
|
226
|
+
candidate.model !== model ||
|
|
227
|
+
!Number.isSafeInteger(candidate.chainIndex) ||
|
|
228
|
+
candidate.chainIndex < 0
|
|
229
|
+
) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
'DeepSeekProvider: providerRoute must name this provider, the requested model, and a non-negative integer chainIndex.',
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
return candidate
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isDeepSeekReplayState(value: unknown): value is DeepSeekReplayState {
|
|
238
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
239
|
+
const state = value as Record<string, unknown>
|
|
240
|
+
const route = state.route
|
|
241
|
+
return (
|
|
242
|
+
state.kind === 'namzu-deepseek-reasoning' &&
|
|
243
|
+
state.version === 1 &&
|
|
244
|
+
typeof state.reasoningContent === 'string' &&
|
|
245
|
+
state.reasoningContent.length > 0 &&
|
|
246
|
+
typeof route === 'object' &&
|
|
247
|
+
route !== null &&
|
|
248
|
+
!Array.isArray(route) &&
|
|
249
|
+
typeof (route as Record<string, unknown>).providerId === 'string' &&
|
|
250
|
+
typeof (route as Record<string, unknown>).model === 'string' &&
|
|
251
|
+
Number.isSafeInteger((route as Record<string, unknown>).chainIndex) &&
|
|
252
|
+
((route as Record<string, unknown>).chainIndex as number) >= 0
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function durableDeepSeekReasoning(
|
|
257
|
+
reasoning: readonly ReasoningBlock[] | undefined,
|
|
258
|
+
): string | undefined {
|
|
259
|
+
if (!reasoning || reasoning.length === 0) return undefined
|
|
260
|
+
if (
|
|
261
|
+
reasoning.some(
|
|
262
|
+
(block) =>
|
|
263
|
+
block.type !== 'thinking' ||
|
|
264
|
+
typeof block.text !== 'string' ||
|
|
265
|
+
block.signature !== undefined ||
|
|
266
|
+
block.encrypted !== undefined,
|
|
267
|
+
)
|
|
268
|
+
) {
|
|
269
|
+
return undefined
|
|
270
|
+
}
|
|
271
|
+
const text = reasoning.map((block) => block.text as string).join('')
|
|
272
|
+
return text.length > 0 ? text : undefined
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Restore `reasoning_content` only from a validated same-route envelope.
|
|
192
277
|
*
|
|
193
|
-
* The vendor's rule: with tool calls in play, an assistant turn's
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* already promises — "replayed verbatim and ahead of the text/tool blocks".
|
|
278
|
+
* The vendor's rule: with tool calls in play, an assistant turn's native
|
|
279
|
+
* reasoning must be echoed on later turns. Matching provider/model names are
|
|
280
|
+
* not enough: state can come from another chain member or another adapter
|
|
281
|
+
* format, and legacy messages carry no proof at all.
|
|
198
282
|
*
|
|
199
283
|
* Measured note, because it matters to anyone debugging this: as of
|
|
200
284
|
* 2026-08-17 omitting the replay does NOT produce the documented 400 on either
|
|
@@ -202,53 +286,170 @@ export function assertEffortUnsupported(params: ChatCompletionParams): void {
|
|
|
202
286
|
* a contract the vendor states and does not currently enforce is a contract
|
|
203
287
|
* that can start being enforced in any release.
|
|
204
288
|
*/
|
|
205
|
-
function replayReasoning(
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
289
|
+
function replayReasoning(
|
|
290
|
+
message: Extract<ChatCompletionParams['messages'][number], { role: 'assistant' }>,
|
|
291
|
+
targetRoute: ProviderRoute,
|
|
292
|
+
): string | undefined {
|
|
293
|
+
const source = message.source
|
|
294
|
+
if (!source || source.type !== 'model' || !sameRoute(source, targetRoute)) return undefined
|
|
295
|
+
const state = source.replayState
|
|
296
|
+
if (!isDeepSeekReplayState(state) || !sameRoute(state.route, source)) return undefined
|
|
297
|
+
const durable = durableDeepSeekReasoning(message.reasoning)
|
|
298
|
+
return durable !== undefined && durable === state.reasoningContent ? durable : undefined
|
|
212
299
|
}
|
|
213
300
|
|
|
214
301
|
export function toDeepSeekMessages(
|
|
215
302
|
messages: ChatCompletionParams['messages'],
|
|
303
|
+
targetRoute: ProviderRoute,
|
|
216
304
|
): ChatCompletionMessageParam[] {
|
|
217
|
-
|
|
218
|
-
|
|
305
|
+
assertDeepSeekRichContent(messages, targetRoute.model)
|
|
306
|
+
|
|
307
|
+
type ImagePart = { type: 'image_url'; image_url: { url: string } }
|
|
308
|
+
type UserPart = { type: 'text'; text: string } | ImagePart
|
|
309
|
+
const imagePart = (image: ImageAttachment): ImagePart => ({
|
|
310
|
+
type: 'image_url',
|
|
311
|
+
image_url: { url: `data:${image.mediaType};base64,${image.data}` },
|
|
312
|
+
})
|
|
313
|
+
const wire: ChatCompletionMessageParam[] = []
|
|
314
|
+
let pendingToolImages: ImagePart[] = []
|
|
315
|
+
const flushToolImages = (): void => {
|
|
316
|
+
if (pendingToolImages.length === 0) return
|
|
317
|
+
wire.push({
|
|
318
|
+
role: 'user',
|
|
319
|
+
content: [
|
|
320
|
+
{ type: 'text', text: 'Attached image(s) from tool result:' },
|
|
321
|
+
...pendingToolImages,
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
pendingToolImages = []
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const msg of messages) {
|
|
328
|
+
if (msg.role !== 'tool') flushToolImages()
|
|
329
|
+
if (msg.role === 'system') {
|
|
330
|
+
wire.push({ role: 'system', content: msg.content })
|
|
331
|
+
continue
|
|
332
|
+
}
|
|
219
333
|
if (msg.role === 'user') {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
334
|
+
if (!msg.attachments || msg.attachments.length === 0) {
|
|
335
|
+
wire.push({ role: 'user', content: msg.content })
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
const content: UserPart[] = []
|
|
339
|
+
if (msg.content.length > 0) content.push({ type: 'text', text: msg.content })
|
|
340
|
+
for (const attachment of msg.attachments) {
|
|
341
|
+
// The preflight above proves these are inline images. This local check
|
|
342
|
+
// keeps the narrowing explicit instead of casting a public union.
|
|
343
|
+
if (attachment.type === 'stored' || attachment.type === 'document') continue
|
|
344
|
+
content.push(imagePart(attachment))
|
|
228
345
|
}
|
|
229
|
-
|
|
346
|
+
wire.push({ role: 'user', content })
|
|
347
|
+
continue
|
|
230
348
|
}
|
|
231
349
|
if (msg.role === 'tool') {
|
|
232
|
-
|
|
350
|
+
const { text, images } = splitToolResult(msg.content)
|
|
351
|
+
wire.push({
|
|
233
352
|
role: 'tool',
|
|
234
|
-
content:
|
|
353
|
+
content: text || '(no output)',
|
|
235
354
|
tool_call_id: msg.toolCallId,
|
|
236
|
-
}
|
|
355
|
+
})
|
|
356
|
+
pendingToolImages.push(...images.map(imagePart))
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
const assistant: ChatCompletionMessageParam = {
|
|
360
|
+
role: 'assistant',
|
|
361
|
+
content: msg.content,
|
|
237
362
|
}
|
|
238
|
-
const assistant: ChatCompletionMessageParam = { role: 'assistant', content: msg.content }
|
|
239
363
|
if ('toolCalls' in msg && msg.toolCalls && msg.toolCalls.length > 0) {
|
|
240
364
|
;(assistant as { tool_calls?: unknown }).tool_calls = msg.toolCalls.map((tc) => ({
|
|
241
365
|
id: tc.id,
|
|
242
366
|
type: 'function' as const,
|
|
243
|
-
function: {
|
|
367
|
+
function: {
|
|
368
|
+
name: tc.function.name,
|
|
369
|
+
arguments: tc.function.arguments,
|
|
370
|
+
},
|
|
244
371
|
}))
|
|
245
372
|
}
|
|
246
|
-
const replayed = replayReasoning(
|
|
373
|
+
const replayed = replayReasoning(msg, targetRoute)
|
|
247
374
|
if (replayed !== undefined) {
|
|
248
375
|
;(assistant as { reasoning_content?: string }).reasoning_content = replayed
|
|
249
376
|
}
|
|
250
|
-
|
|
251
|
-
}
|
|
377
|
+
wire.push(assistant)
|
|
378
|
+
}
|
|
379
|
+
flushToolImages()
|
|
380
|
+
return wire
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function splitToolResult(content: ToolResultContent): {
|
|
384
|
+
readonly text: string
|
|
385
|
+
readonly images: readonly ImageAttachment[]
|
|
386
|
+
} {
|
|
387
|
+
const text: string[] = []
|
|
388
|
+
const images: ImageAttachment[] = []
|
|
389
|
+
for (const block of toToolResultBlocks(content)) {
|
|
390
|
+
if (block.type === 'text') text.push(block.text)
|
|
391
|
+
else if (block.type === 'image') images.push(block)
|
|
392
|
+
}
|
|
393
|
+
return { text: text.join('\n'), images }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Refuse every rich-content shape this exact model/wire cannot represent.
|
|
398
|
+
*
|
|
399
|
+
* This runs before the request object is built, so a text model never sees an
|
|
400
|
+
* image data URL and a document never reaches a wire that would silently drop
|
|
401
|
+
* it. Stored user refs should have been resolved by the SDK attachment store;
|
|
402
|
+
* accepting one here would serialize a reference rather than its bytes.
|
|
403
|
+
*/
|
|
404
|
+
function assertDeepSeekRichContent(
|
|
405
|
+
messages: ChatCompletionParams['messages'],
|
|
406
|
+
model: string,
|
|
407
|
+
): void {
|
|
408
|
+
let hasImage = false
|
|
409
|
+
const acceptImage = (mediaType: string): void => {
|
|
410
|
+
if (!IMAGE_MEDIA_TYPES.has(mediaType)) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`DeepSeekProvider: image type '${mediaType}' is not supported. Use image/png, image/jpeg, image/webp, or image/gif.`,
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
hasImage = true
|
|
416
|
+
}
|
|
417
|
+
for (const message of messages) {
|
|
418
|
+
if (message.role === 'user') {
|
|
419
|
+
for (const attachment of message.attachments ?? []) {
|
|
420
|
+
if (attachment.type === 'stored') {
|
|
421
|
+
throw new Error(
|
|
422
|
+
'DeepSeekProvider: an unresolved stored attachment reached the driver. Configure an AttachmentStore so Namzu can resolve its bytes before the model request.',
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
if (attachment.type === 'document') {
|
|
426
|
+
throw new Error(
|
|
427
|
+
'DeepSeekProvider: this Chat Completions driver does not support document input. Route the turn to a document-capable provider.',
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
acceptImage(attachment.mediaType)
|
|
431
|
+
}
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
if (message.role !== 'tool') continue
|
|
435
|
+
for (const block of toToolResultBlocks(message.content)) {
|
|
436
|
+
if (block.type === 'document') {
|
|
437
|
+
throw new Error(
|
|
438
|
+
'DeepSeekProvider: this Chat Completions driver does not support document tool results. Route the turn to a document-capable provider.',
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
if (block.type === 'image') acceptImage(block.mediaType)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (hasImage && model !== VISION_MODEL) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`DeepSeekProvider: model '${model}' does not accept image input. Select '${VISION_MODEL}' or remove the images.`,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function inputModalitiesFor(model: string): readonly ModelInputModality[] | undefined {
|
|
452
|
+
return KNOWN_MODELS.find((candidate) => candidate.id === model)?.inputModalities
|
|
252
453
|
}
|
|
253
454
|
|
|
254
455
|
export function toDeepSeekTools(params: ChatCompletionParams): ChatCompletionTool[] | undefined {
|
|
@@ -286,7 +487,10 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
286
487
|
baseURL: config.baseURL ?? DEFAULT_BASE_URL,
|
|
287
488
|
}
|
|
288
489
|
if (config.timeout !== undefined) clientOptions.timeout = config.timeout
|
|
289
|
-
clientOptions.defaultHeaders = {
|
|
490
|
+
clientOptions.defaultHeaders = {
|
|
491
|
+
...attributionHeaders(),
|
|
492
|
+
...(config.defaultHeaders ?? {}),
|
|
493
|
+
}
|
|
290
494
|
|
|
291
495
|
this.client = new OpenAI(clientOptions)
|
|
292
496
|
this.defaultModel = config.model
|
|
@@ -303,8 +507,14 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
303
507
|
return model
|
|
304
508
|
}
|
|
305
509
|
|
|
510
|
+
/** This Chat Completions endpoint explicitly carries no effort control. */
|
|
511
|
+
reasoningEffortLevelsFor(_model: string): readonly ReasoningEffort[] {
|
|
512
|
+
return []
|
|
513
|
+
}
|
|
514
|
+
|
|
306
515
|
async *chatStream(params: ChatCompletionParams): AsyncIterable<StreamChunk> {
|
|
307
516
|
const model = this.resolveModel(params)
|
|
517
|
+
const providerRoute = resolveDeepSeekRoute(model, params.providerRoute)
|
|
308
518
|
assertEffortUnsupported(params)
|
|
309
519
|
assertSamplingUsable(params, this.samplingMode)
|
|
310
520
|
|
|
@@ -324,7 +534,7 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
324
534
|
// to the non-streaming overload, whose result has no async iterator.
|
|
325
535
|
const body = {
|
|
326
536
|
model,
|
|
327
|
-
messages: toDeepSeekMessages(params.messages),
|
|
537
|
+
messages: toDeepSeekMessages(params.messages, providerRoute),
|
|
328
538
|
stream: true as const,
|
|
329
539
|
stream_options: { include_usage: true },
|
|
330
540
|
tools: toDeepSeekTools(params),
|
|
@@ -362,6 +572,8 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
362
572
|
// `reasoning_content` as a flat run of deltas with no index and no
|
|
363
573
|
// block boundaries, so index 0 is the whole of it rather than a guess.
|
|
364
574
|
let reasoningOpen = false
|
|
575
|
+
let reasoningContent = ''
|
|
576
|
+
let replayStateEmitted = false
|
|
365
577
|
|
|
366
578
|
try {
|
|
367
579
|
for await (const chunk of stream) {
|
|
@@ -369,7 +581,11 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
369
581
|
try {
|
|
370
582
|
const choice = chunk.choices[0]
|
|
371
583
|
const delta = choice?.delta as
|
|
372
|
-
| {
|
|
584
|
+
| {
|
|
585
|
+
content?: string | null
|
|
586
|
+
reasoning_content?: string | null
|
|
587
|
+
tool_calls?: unknown[]
|
|
588
|
+
}
|
|
373
589
|
| undefined
|
|
374
590
|
|
|
375
591
|
const toolCalls = (
|
|
@@ -398,9 +614,12 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
398
614
|
|
|
399
615
|
if (reasoningText) {
|
|
400
616
|
reasoningOpen = true
|
|
617
|
+
reasoningContent += reasoningText
|
|
401
618
|
yield {
|
|
402
619
|
id: chunk.id,
|
|
403
|
-
delta: {
|
|
620
|
+
delta: {
|
|
621
|
+
reasoning: { index: 0, type: 'thinking', text: reasoningText },
|
|
622
|
+
},
|
|
404
623
|
}
|
|
405
624
|
}
|
|
406
625
|
|
|
@@ -413,7 +632,10 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
413
632
|
(toolCalls !== undefined && toolCalls.length > 0)
|
|
414
633
|
if (reasoningOpen && contentStarting) {
|
|
415
634
|
reasoningOpen = false
|
|
416
|
-
yield {
|
|
635
|
+
yield {
|
|
636
|
+
id: chunk.id,
|
|
637
|
+
delta: { reasoning: { index: 0, done: true } },
|
|
638
|
+
}
|
|
417
639
|
}
|
|
418
640
|
|
|
419
641
|
const hasDelta =
|
|
@@ -421,8 +643,20 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
421
643
|
(toolCalls !== undefined && toolCalls.length > 0)
|
|
422
644
|
if (!hasDelta && !finishReason && !usage) continue
|
|
423
645
|
|
|
646
|
+
const replayState =
|
|
647
|
+
!replayStateEmitted && finishReason && reasoningContent.length > 0
|
|
648
|
+
? ({
|
|
649
|
+
kind: 'namzu-deepseek-reasoning',
|
|
650
|
+
version: 1,
|
|
651
|
+
route: providerRoute,
|
|
652
|
+
reasoningContent,
|
|
653
|
+
} satisfies DeepSeekReplayState)
|
|
654
|
+
: undefined
|
|
655
|
+
if (replayState !== undefined) replayStateEmitted = true
|
|
656
|
+
|
|
424
657
|
yield {
|
|
425
658
|
id: chunk.id,
|
|
659
|
+
...(replayState !== undefined ? { replayState } : {}),
|
|
426
660
|
delta: { content: delta?.content ?? undefined, toolCalls },
|
|
427
661
|
finishReason,
|
|
428
662
|
usage,
|
|
@@ -439,7 +673,10 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
439
673
|
// A turn that was all reasoning and no content — the model thought
|
|
440
674
|
// and then stopped — still has to close its block.
|
|
441
675
|
if (reasoningOpen) {
|
|
442
|
-
yield {
|
|
676
|
+
yield {
|
|
677
|
+
id: 'deepseek-reasoning-close',
|
|
678
|
+
delta: { reasoning: { index: 0, done: true } },
|
|
679
|
+
}
|
|
443
680
|
}
|
|
444
681
|
} catch (err) {
|
|
445
682
|
if (isCallerAbortError(err, params.signal)) throw params.signal?.reason ?? err
|
|
@@ -448,20 +685,35 @@ export class DeepSeekProvider implements LLMProvider {
|
|
|
448
685
|
}
|
|
449
686
|
}
|
|
450
687
|
|
|
451
|
-
async probeCredential(): Promise<void> {
|
|
452
|
-
|
|
688
|
+
async probeCredential(signal?: AbortSignal): Promise<void> {
|
|
689
|
+
signal?.throwIfAborted()
|
|
690
|
+
await this.client.models.list(signal ? { signal } : undefined)
|
|
691
|
+
signal?.throwIfAborted()
|
|
453
692
|
}
|
|
454
693
|
|
|
455
|
-
async listModels(): Promise<ModelInfo[]> {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
694
|
+
async listModels(signal?: AbortSignal): Promise<ModelInfo[]> {
|
|
695
|
+
signal?.throwIfAborted()
|
|
696
|
+
const page = await this.client.models.list(signal ? { signal } : undefined)
|
|
697
|
+
signal?.throwIfAborted()
|
|
698
|
+
const models = new Map<string, { id: string; name: string }>()
|
|
699
|
+
for (const model of page.data) models.set(model.id, { id: model.id, name: model.id })
|
|
700
|
+
// The preview model is part of the source adapter's verified default
|
|
701
|
+
// catalogue but may lag the account listing endpoint. A successful list
|
|
702
|
+
// therefore augments rather than erases the known selectable catalogue.
|
|
703
|
+
for (const model of KNOWN_MODELS) {
|
|
704
|
+
models.set(model.id, { id: model.id, name: model.name })
|
|
705
|
+
}
|
|
706
|
+
return [...models.values()].map((model) => {
|
|
707
|
+
const inputModalities = inputModalitiesFor(model.id)
|
|
708
|
+
return {
|
|
709
|
+
...model,
|
|
710
|
+
...(inputModalities !== undefined ? { inputModalities } : {}),
|
|
711
|
+
inputPrice: 0,
|
|
712
|
+
outputPrice: 0,
|
|
713
|
+
supportsToolUse: true,
|
|
714
|
+
supportsStreaming: true,
|
|
715
|
+
}
|
|
716
|
+
})
|
|
465
717
|
}
|
|
466
718
|
|
|
467
719
|
async healthCheck(): Promise<boolean> {
|