@molecule/api-ai-deepseek 1.0.11 → 1.0.13
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/dist/provider.d.ts +22 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +206 -94
- package/package.json +2 -2
package/dist/provider.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses the DeepSeek API (OpenAI-compatible) for streaming chat completions with
|
|
5
5
|
* tool use and reasoning. DeepSeek's Chat Completions endpoint mirrors OpenAI's
|
|
6
|
-
* wire format (tool_calls, reasoning_content,
|
|
6
|
+
* wire format (tool_calls, reasoning_content, native and nested cache usage),
|
|
7
7
|
* so this provider shares its shape with the xAI provider.
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
@@ -62,8 +62,20 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
62
62
|
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
63
63
|
* System prompt is prepended as a system role message.
|
|
64
64
|
*
|
|
65
|
+
* Thinking mode has one wire obligation that non-thinking mode does not:
|
|
66
|
+
* DeepSeek's thinking-mode guide requires the `reasoning_content` of every
|
|
67
|
+
* retained assistant turn to be sent back on a later request carrying `tools`,
|
|
68
|
+
* including turns where the model made no tool call — omitting it is a 400.
|
|
69
|
+
* So when `replayReasoning` is set (the caller passed a `thinking` block), each
|
|
70
|
+
* assistant turn carries `ChatMessage.reasoning` verbatim. That reasoning is
|
|
71
|
+
* never synthesized: a turn with no recorded reasoning sends `''`, which is
|
|
72
|
+
* the only representation of "this turn ran without thinking" the wire has.
|
|
73
|
+
* A non-thinking request keeps the historical shape (no `reasoning_content`);
|
|
74
|
+
* the guide says a non-thinking request ignores the field anyway.
|
|
75
|
+
*
|
|
65
76
|
* @param messages - The chat messages to format.
|
|
66
77
|
* @param system - Optional system prompt to prepend.
|
|
78
|
+
* @param replayReasoning - Whether thinking mode is on for this request.
|
|
67
79
|
* @returns The formatted messages array for the DeepSeek API.
|
|
68
80
|
*/
|
|
69
81
|
private formatMessages;
|
|
@@ -81,6 +93,15 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
81
93
|
* @yields {ChatEvent} Chat events parsed from the response.
|
|
82
94
|
*/
|
|
83
95
|
private parseNonStreamingResponse;
|
|
96
|
+
/**
|
|
97
|
+
* Normalize native usage once for streaming and JSON responses. Native flat
|
|
98
|
+
* counters take precedence, including zero; other compatible hosts may expose
|
|
99
|
+
* only the nested count. `inputTokens` excludes cache hits in the core contract.
|
|
100
|
+
*
|
|
101
|
+
* @param usage - The latest upstream counters.
|
|
102
|
+
* @returns Uncached input, output and cached input without double-counting.
|
|
103
|
+
*/
|
|
104
|
+
private parseUsage;
|
|
84
105
|
/**
|
|
85
106
|
* Parses a streaming SSE response from the DeepSeek API (OpenAI-compatible
|
|
86
107
|
* format), yielding `ChatEvent` objects as they arrive.
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,UAAU,EAIV,SAAS,EAET,UAAU,EAIX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgHhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAgGpD;;;;GAIG;AACH,cAAM,sBAAuB,YAAW,UAAU;IAChD,QAAQ,CAAC,IAAI,cAAa;IAC1B,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAwB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAqB;IACzC,OAAO,CAAC,aAAa,CAAC,CAAuB;IAC7C,OAAO,CAAC,mBAAmB,CAAQ;IACnC,OAAO,CAAC,sBAAsB,CAAQ;gBAE1B,MAAM,GAAE,cAAmB;IAwBvC;;;;;OAKG;IACI,IAAI,CAAC,MAAM,EAAE,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC;IAoRzD;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,cAAc;IA8FtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IA+ElC;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAUlB;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CAwOzB;AAED;;GAEG;AACH,eAAO,MAAM,kBAAkB,+BAAyB,CAAA;AAExD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,UAAU,CAElE"}
|
package/dist/provider.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses the DeepSeek API (OpenAI-compatible) for streaming chat completions with
|
|
5
5
|
* tool use and reasoning. DeepSeek's Chat Completions endpoint mirrors OpenAI's
|
|
6
|
-
* wire format (tool_calls, reasoning_content,
|
|
6
|
+
* wire format (tool_calls, reasoning_content, native and nested cache usage),
|
|
7
7
|
* so this provider shares its shape with the xAI provider.
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
@@ -17,9 +17,11 @@ const logger = getLogger();
|
|
|
17
17
|
// (not through the package barrel).
|
|
18
18
|
import './secrets.js';
|
|
19
19
|
/**
|
|
20
|
-
* Map thinking budget tokens to a DeepSeek reasoning_effort level
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* Map thinking budget tokens to a DeepSeek reasoning_effort level, used ONLY as
|
|
21
|
+
* a fallback when the caller supplies a budget without a resolved native level.
|
|
22
|
+
* The native API accepts `low`/`high`/`max`; a caller that resolved one of them
|
|
23
|
+
* (the catalog's `supportedEffortLevels`) sends it verbatim and never reaches
|
|
24
|
+
* this function, so the fallback stays the two coarse buckets.
|
|
23
25
|
*
|
|
24
26
|
* @param budgetTokens - Requested thinking budget in tokens.
|
|
25
27
|
* @returns `'high'` or `'max'` for the upstream effort hint.
|
|
@@ -149,7 +151,7 @@ class DeepseekAIProviderImpl {
|
|
|
149
151
|
const canonicalModel = params.model ?? this.defaultModel;
|
|
150
152
|
const model = this.modelMap[canonicalModel] ?? canonicalModel;
|
|
151
153
|
const maxTokens = params.maxTokens ?? this.maxTokens;
|
|
152
|
-
const messages = this.formatMessages(params.messages, params.system);
|
|
154
|
+
const messages = this.formatMessages(params.messages, params.system, Boolean(params.thinking));
|
|
153
155
|
// Stop sequences, capped at the API's own limit. Verified honoured by the
|
|
154
156
|
// native endpoint on 2026-10-17: with `stop: ['<analysis>']` a reply that
|
|
155
157
|
// would have opened with that tag streamed nothing at all, and one that
|
|
@@ -188,10 +190,11 @@ class DeepseekAIProviderImpl {
|
|
|
188
190
|
// weaker nudge still produces a turn, and the agent loop has its own
|
|
189
191
|
// prose-only backstops, whereas a 400 produces nothing at all.
|
|
190
192
|
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// is
|
|
193
|
+
// LIVE for an entry that exposes opt-in reasoning (deepseek-flash, whose
|
|
194
|
+
// effort levels include a local `off` switch): a project that uses
|
|
195
|
+
// low/high/max gets thinking mode on a discovery turn, and the forced
|
|
196
|
+
// nudge is degraded here instead of 400ing the first turn of the
|
|
197
|
+
// conversation.
|
|
195
198
|
const forcedChoice = params.toolChoice === 'required' ||
|
|
196
199
|
(typeof params.toolChoice === 'object' && params.toolChoice?.type === 'tool');
|
|
197
200
|
if (forcedChoice && params.thinking) {
|
|
@@ -206,15 +209,18 @@ class DeepseekAIProviderImpl {
|
|
|
206
209
|
body.tool_choice = { type: 'function', function: { name: params.toolChoice.name } };
|
|
207
210
|
}
|
|
208
211
|
}
|
|
209
|
-
// DeepSeek V4 defaults `thinking` to ENABLED.
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
212
|
+
// DeepSeek V4 defaults `thinking` to ENABLED. Per DeepSeek's thinking-mode
|
|
213
|
+
// guide (https://api-docs.deepseek.com/guides/thinking_mode/), a
|
|
214
|
+
// thinking-mode request that carries tools must send every retained
|
|
215
|
+
// assistant turn's `reasoning_content` back — including a turn where the
|
|
216
|
+
// model made no tool call — or the API answers 400. `formatMessages` replays
|
|
217
|
+
// `ChatMessage.reasoning` for exactly that reason. The catalog decides
|
|
218
|
+
// whether a turn carries a thinking block. No block explicitly disables
|
|
219
|
+
// reasoning below; a low/high/max block enables that native effort.
|
|
214
220
|
if (params.thinking) {
|
|
215
221
|
body.thinking = { type: 'enabled' };
|
|
216
|
-
// Prefer a caller-resolved native value (
|
|
217
|
-
// catalog; fall back to the budget threshold.
|
|
222
|
+
// Prefer a caller-resolved native value (`low` | `high` | `max`) from the
|
|
223
|
+
// model catalog; fall back to the budget threshold.
|
|
218
224
|
body.reasoning_effort = params.thinking.effort ?? budgetToEffort(params.thinking.budgetTokens);
|
|
219
225
|
}
|
|
220
226
|
else {
|
|
@@ -345,9 +351,8 @@ class DeepseekAIProviderImpl {
|
|
|
345
351
|
return;
|
|
346
352
|
}
|
|
347
353
|
const state = {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
cachedTokens: 0,
|
|
354
|
+
finishReason: null,
|
|
355
|
+
usage: {},
|
|
351
356
|
keepAlives: 0,
|
|
352
357
|
streamedChars: 0,
|
|
353
358
|
contentSeen: false,
|
|
@@ -518,11 +523,23 @@ class DeepseekAIProviderImpl {
|
|
|
518
523
|
* Converts internal `ChatMessage` objects to the OpenAI-compatible message format.
|
|
519
524
|
* System prompt is prepended as a system role message.
|
|
520
525
|
*
|
|
526
|
+
* Thinking mode has one wire obligation that non-thinking mode does not:
|
|
527
|
+
* DeepSeek's thinking-mode guide requires the `reasoning_content` of every
|
|
528
|
+
* retained assistant turn to be sent back on a later request carrying `tools`,
|
|
529
|
+
* including turns where the model made no tool call — omitting it is a 400.
|
|
530
|
+
* So when `replayReasoning` is set (the caller passed a `thinking` block), each
|
|
531
|
+
* assistant turn carries `ChatMessage.reasoning` verbatim. That reasoning is
|
|
532
|
+
* never synthesized: a turn with no recorded reasoning sends `''`, which is
|
|
533
|
+
* the only representation of "this turn ran without thinking" the wire has.
|
|
534
|
+
* A non-thinking request keeps the historical shape (no `reasoning_content`);
|
|
535
|
+
* the guide says a non-thinking request ignores the field anyway.
|
|
536
|
+
*
|
|
521
537
|
* @param messages - The chat messages to format.
|
|
522
538
|
* @param system - Optional system prompt to prepend.
|
|
539
|
+
* @param replayReasoning - Whether thinking mode is on for this request.
|
|
523
540
|
* @returns The formatted messages array for the DeepSeek API.
|
|
524
541
|
*/
|
|
525
|
-
formatMessages(messages, system) {
|
|
542
|
+
formatMessages(messages, system, replayReasoning = false) {
|
|
526
543
|
const formatted = [];
|
|
527
544
|
if (system) {
|
|
528
545
|
formatted.push({ role: 'system', content: system });
|
|
@@ -530,8 +547,14 @@ class DeepseekAIProviderImpl {
|
|
|
530
547
|
for (const m of messages) {
|
|
531
548
|
if (m.role === 'system')
|
|
532
549
|
continue; // handled above
|
|
550
|
+
// Only assistant turns have a reasoning channel. `reasoning_content: ''`
|
|
551
|
+
// is deliberate for a retained turn with no recorded reasoning (a
|
|
552
|
+
// non-thinking turn inside a thinking-mode history): the vendor requires
|
|
553
|
+
// the field on every retained assistant turn, and an empty string is the
|
|
554
|
+
// only way to say "nothing was recorded" without inventing content.
|
|
555
|
+
const reasoningField = replayReasoning && m.role === 'assistant' ? { reasoning_content: m.reasoning ?? '' } : {};
|
|
533
556
|
if (typeof m.content === 'string') {
|
|
534
|
-
formatted.push({ role: m.role, content: m.content });
|
|
557
|
+
formatted.push({ role: m.role, content: m.content, ...reasoningField });
|
|
535
558
|
continue;
|
|
536
559
|
}
|
|
537
560
|
// Content blocks — convert to OpenAI format
|
|
@@ -578,13 +601,14 @@ class DeepseekAIProviderImpl {
|
|
|
578
601
|
// screenshot tool) must not be dropped: Chat Completions accepts a user
|
|
579
602
|
// message after the tool messages, so emit them instead of losing them.
|
|
580
603
|
if (parts.length > 0)
|
|
581
|
-
formatted.push({ role: m.role, content: parts });
|
|
604
|
+
formatted.push({ role: m.role, content: parts, ...reasoningField });
|
|
582
605
|
}
|
|
583
606
|
else if (toolCalls.length > 0) {
|
|
584
607
|
// Assistant message with tool calls
|
|
585
608
|
formatted.push({
|
|
586
609
|
role: 'assistant',
|
|
587
610
|
content: parts.length > 0 ? parts : null,
|
|
611
|
+
...reasoningField,
|
|
588
612
|
tool_calls: toolCalls,
|
|
589
613
|
});
|
|
590
614
|
}
|
|
@@ -592,6 +616,7 @@ class DeepseekAIProviderImpl {
|
|
|
592
616
|
formatted.push({
|
|
593
617
|
role: m.role,
|
|
594
618
|
content: parts.length === 1 && parts[0].type === 'text' ? parts[0].text : parts,
|
|
619
|
+
...reasoningField,
|
|
595
620
|
});
|
|
596
621
|
}
|
|
597
622
|
}
|
|
@@ -621,48 +646,90 @@ class DeepseekAIProviderImpl {
|
|
|
621
646
|
*/
|
|
622
647
|
*parseNonStreamingResponse(data) {
|
|
623
648
|
const choices = data.choices;
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
649
|
+
const choice = choices?.[0];
|
|
650
|
+
const message = choice?.message;
|
|
651
|
+
if (!message) {
|
|
652
|
+
logger.warn('DeepSeek response has no assistant message');
|
|
653
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// Reasoning/thinking content
|
|
657
|
+
if (message.reasoning_content && typeof message.reasoning_content === 'string') {
|
|
658
|
+
yield { type: 'thinking', content: message.reasoning_content };
|
|
659
|
+
}
|
|
660
|
+
if (message.content && typeof message.content === 'string') {
|
|
661
|
+
yield { type: 'text', content: message.content };
|
|
662
|
+
}
|
|
663
|
+
const finishReason = choice?.finish_reason;
|
|
664
|
+
if (finishReason !== 'stop' && finishReason !== 'tool_calls') {
|
|
665
|
+
logger.warn('DeepSeek response did not complete', { finishReason });
|
|
666
|
+
yield {
|
|
667
|
+
type: 'error',
|
|
668
|
+
message: t('ai.error.apiError', undefined, {
|
|
669
|
+
defaultValue: finishReason === 'length'
|
|
670
|
+
? 'AI response reached the output token limit before completion.'
|
|
671
|
+
: 'AI response ended before completion. Please try again.',
|
|
672
|
+
}),
|
|
673
|
+
errorKey: 'ai.error.apiError',
|
|
674
|
+
};
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const toolCalls = message.tool_calls;
|
|
678
|
+
if (finishReason === 'tool_calls' && !toolCalls?.length) {
|
|
679
|
+
logger.warn('DeepSeek finished tool use without a tool call');
|
|
680
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (toolCalls) {
|
|
684
|
+
const completedTools = [];
|
|
685
|
+
for (const tc of toolCalls) {
|
|
686
|
+
const fn = tc.function;
|
|
687
|
+
let input;
|
|
688
|
+
try {
|
|
689
|
+
input = JSON.parse(fn.arguments);
|
|
690
|
+
if (typeof tc.id !== 'string' ||
|
|
691
|
+
!tc.id ||
|
|
692
|
+
typeof fn.name !== 'string' ||
|
|
693
|
+
!fn.name ||
|
|
694
|
+
!input ||
|
|
695
|
+
typeof input !== 'object' ||
|
|
696
|
+
Array.isArray(input)) {
|
|
697
|
+
throw new Error('Tool call must have an identity and JSON object arguments');
|
|
651
698
|
}
|
|
652
699
|
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
logger.warn('DeepSeek completed a tool call with invalid arguments', { error });
|
|
702
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
completedTools.push({
|
|
706
|
+
type: 'tool_use',
|
|
707
|
+
id: tc.id,
|
|
708
|
+
name: fn.name,
|
|
709
|
+
input,
|
|
710
|
+
});
|
|
653
711
|
}
|
|
712
|
+
yield* completedTools;
|
|
654
713
|
}
|
|
655
|
-
const usage = data.usage;
|
|
656
|
-
const cached = usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
657
714
|
yield {
|
|
658
715
|
type: 'done',
|
|
659
|
-
usage: {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
716
|
+
usage: this.parseUsage((data.usage ?? {})),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Normalize native usage once for streaming and JSON responses. Native flat
|
|
721
|
+
* counters take precedence, including zero; other compatible hosts may expose
|
|
722
|
+
* only the nested count. `inputTokens` excludes cache hits in the core contract.
|
|
723
|
+
*
|
|
724
|
+
* @param usage - The latest upstream counters.
|
|
725
|
+
* @returns Uncached input, output and cached input without double-counting.
|
|
726
|
+
*/
|
|
727
|
+
parseUsage(usage) {
|
|
728
|
+
const cached = usage.prompt_cache_hit_tokens ?? usage.prompt_tokens_details?.cached_tokens ?? 0;
|
|
729
|
+
return {
|
|
730
|
+
inputTokens: usage.prompt_cache_miss_tokens ?? Math.max(0, (usage.prompt_tokens ?? 0) - cached),
|
|
731
|
+
outputTokens: usage.completion_tokens ?? 0,
|
|
732
|
+
cacheReadInputTokens: cached,
|
|
666
733
|
};
|
|
667
734
|
}
|
|
668
735
|
/**
|
|
@@ -747,19 +814,22 @@ class DeepseekAIProviderImpl {
|
|
|
747
814
|
return;
|
|
748
815
|
}
|
|
749
816
|
}
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
817
|
+
// A clean socket EOF can still cut off a response or tool arguments. Never
|
|
818
|
+
// turn that into executable `{}` arguments or a successful completion.
|
|
819
|
+
if (!state.finishReason || pendingTools.size > 0) {
|
|
820
|
+
logger.warn('DeepSeek stream ended before an explicit completion', {
|
|
821
|
+
finishReason: state.finishReason,
|
|
822
|
+
pendingTools: pendingTools.size,
|
|
823
|
+
requestId: call.meta.requestId,
|
|
824
|
+
});
|
|
825
|
+
yield {
|
|
826
|
+
type: 'error',
|
|
827
|
+
message: t('ai.error.apiError', undefined, {
|
|
828
|
+
defaultValue: 'AI response ended before completion. Please try again.',
|
|
829
|
+
}),
|
|
830
|
+
errorKey: 'ai.error.apiError',
|
|
831
|
+
};
|
|
832
|
+
return;
|
|
763
833
|
}
|
|
764
834
|
}
|
|
765
835
|
finally {
|
|
@@ -767,11 +837,7 @@ class DeepseekAIProviderImpl {
|
|
|
767
837
|
}
|
|
768
838
|
yield {
|
|
769
839
|
type: 'done',
|
|
770
|
-
usage:
|
|
771
|
-
inputTokens: Math.max(0, state.inputTokens - state.cachedTokens),
|
|
772
|
-
outputTokens: state.outputTokens,
|
|
773
|
-
cacheReadInputTokens: state.cachedTokens,
|
|
774
|
-
},
|
|
840
|
+
usage: this.parseUsage(state.usage),
|
|
775
841
|
provider: call.meta,
|
|
776
842
|
};
|
|
777
843
|
}
|
|
@@ -871,20 +937,22 @@ class DeepseekAIProviderImpl {
|
|
|
871
937
|
// Usage info (may appear in the final chunk)
|
|
872
938
|
const usage = event.usage;
|
|
873
939
|
if (usage) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
state.
|
|
878
|
-
|
|
879
|
-
|
|
940
|
+
// These are snapshots, never increments. Preserve partial counters
|
|
941
|
+
// across frames while allowing a later explicit zero to reset them.
|
|
942
|
+
state.usage = {
|
|
943
|
+
...state.usage,
|
|
944
|
+
...usage,
|
|
945
|
+
prompt_tokens_details: {
|
|
946
|
+
...state.usage.prompt_tokens_details,
|
|
947
|
+
...usage.prompt_tokens_details,
|
|
948
|
+
},
|
|
949
|
+
};
|
|
880
950
|
}
|
|
881
951
|
const choices = event.choices;
|
|
882
952
|
if (!choices?.length)
|
|
883
953
|
continue;
|
|
884
954
|
const choice = choices[0];
|
|
885
|
-
const delta = choice.delta;
|
|
886
|
-
if (!delta)
|
|
887
|
-
continue;
|
|
955
|
+
const delta = (choice.delta ?? {});
|
|
888
956
|
// Reasoning/thinking content
|
|
889
957
|
if (delta.reasoning_content && typeof delta.reasoning_content === 'string') {
|
|
890
958
|
yield { type: 'thinking', content: delta.reasoning_content };
|
|
@@ -928,30 +996,74 @@ class DeepseekAIProviderImpl {
|
|
|
928
996
|
}
|
|
929
997
|
}
|
|
930
998
|
}
|
|
931
|
-
//
|
|
999
|
+
// Only these upstream reasons denote a completed response. `length`
|
|
1000
|
+
// is the output-token cap; filters/resource failures/aborts also retain
|
|
1001
|
+
// partial text but must not release pending tools or emit `done`.
|
|
932
1002
|
const finishReason = choice.finish_reason;
|
|
933
|
-
if (finishReason
|
|
934
|
-
|
|
1003
|
+
if (finishReason !== undefined && finishReason !== null) {
|
|
1004
|
+
if (finishReason !== 'tool_calls' && finishReason !== 'stop') {
|
|
1005
|
+
logger.warn('DeepSeek response did not complete', {
|
|
1006
|
+
finishReason,
|
|
1007
|
+
requestId: call.meta.requestId,
|
|
1008
|
+
});
|
|
1009
|
+
yield {
|
|
1010
|
+
type: 'error',
|
|
1011
|
+
message: t('ai.error.apiError', undefined, {
|
|
1012
|
+
defaultValue: finishReason === 'length'
|
|
1013
|
+
? 'AI response reached the output token limit before completion.'
|
|
1014
|
+
: 'AI response was interrupted before completion. Please try again.',
|
|
1015
|
+
}),
|
|
1016
|
+
errorKey: 'ai.error.apiError',
|
|
1017
|
+
};
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
// Validate ALL calls before releasing any: a malformed parallel call
|
|
1021
|
+
// must not execute alongside an apparently successful partial batch.
|
|
1022
|
+
if (finishReason === 'tool_calls' && pendingTools.size === 0) {
|
|
1023
|
+
logger.warn('DeepSeek finished tool use without a tool call');
|
|
1024
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const completedTools = [];
|
|
935
1028
|
for (const [, pending] of pendingTools) {
|
|
936
|
-
let input
|
|
1029
|
+
let input;
|
|
937
1030
|
try {
|
|
938
1031
|
input = JSON.parse(pending.args);
|
|
1032
|
+
if (typeof pending.id !== 'string' ||
|
|
1033
|
+
!pending.id ||
|
|
1034
|
+
typeof pending.name !== 'string' ||
|
|
1035
|
+
!pending.name ||
|
|
1036
|
+
!input ||
|
|
1037
|
+
typeof input !== 'object' ||
|
|
1038
|
+
Array.isArray(input)) {
|
|
1039
|
+
throw new Error('Tool call must have an identity and JSON object arguments');
|
|
1040
|
+
}
|
|
939
1041
|
}
|
|
940
|
-
catch (
|
|
941
|
-
|
|
1042
|
+
catch (error) {
|
|
1043
|
+
logger.warn('DeepSeek completed a tool call with invalid arguments', { error });
|
|
1044
|
+
yield {
|
|
1045
|
+
type: 'error',
|
|
1046
|
+
message: t('ai.error.apiError'),
|
|
1047
|
+
errorKey: 'ai.error.apiError',
|
|
1048
|
+
};
|
|
1049
|
+
return;
|
|
942
1050
|
}
|
|
943
|
-
|
|
1051
|
+
completedTools.push({
|
|
944
1052
|
type: 'tool_use',
|
|
945
1053
|
id: pending.id,
|
|
946
1054
|
name: pending.name,
|
|
947
1055
|
input,
|
|
948
|
-
};
|
|
1056
|
+
});
|
|
949
1057
|
}
|
|
950
1058
|
pendingTools.clear();
|
|
1059
|
+
state.finishReason = finishReason;
|
|
1060
|
+
yield* completedTools;
|
|
951
1061
|
}
|
|
952
1062
|
}
|
|
953
1063
|
catch (error) {
|
|
954
|
-
logger.
|
|
1064
|
+
logger.warn('DeepSeek returned malformed SSE JSON', { error });
|
|
1065
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
1066
|
+
return;
|
|
955
1067
|
}
|
|
956
1068
|
}
|
|
957
1069
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-ai-deepseek",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@molecule/api-bond": "1.0.2",
|
|
38
38
|
"@types/node": "26.1.2",
|
|
39
39
|
"typescript": "6.0.3",
|
|
40
|
-
"vitest": "4.1.
|
|
40
|
+
"vitest": "4.1.12"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@molecule/api-ai": "^1.0.1",
|