@molecule/api-ai-deepseek 1.0.12 → 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 +10 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +165 -80
- package/package.json +1 -1
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
|
|
@@ -93,6 +93,15 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
93
93
|
* @yields {ChatEvent} Chat events parsed from the response.
|
|
94
94
|
*/
|
|
95
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;
|
|
96
105
|
/**
|
|
97
106
|
* Parses a streaming SSE response from the DeepSeek API (OpenAI-compatible
|
|
98
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
|
|
@@ -191,7 +191,7 @@ class DeepseekAIProviderImpl {
|
|
|
191
191
|
// prose-only backstops, whereas a 400 produces nothing at all.
|
|
192
192
|
//
|
|
193
193
|
// LIVE for an entry that exposes opt-in reasoning (deepseek-flash, whose
|
|
194
|
-
// effort levels include
|
|
194
|
+
// effort levels include a local `off` switch): a project that uses
|
|
195
195
|
// low/high/max gets thinking mode on a discovery turn, and the forced
|
|
196
196
|
// nudge is degraded here instead of 400ing the first turn of the
|
|
197
197
|
// conversation.
|
|
@@ -215,9 +215,8 @@ class DeepseekAIProviderImpl {
|
|
|
215
215
|
// assistant turn's `reasoning_content` back — including a turn where the
|
|
216
216
|
// model made no tool call — or the API answers 400. `formatMessages` replays
|
|
217
217
|
// `ChatMessage.reasoning` for exactly that reason. The catalog decides
|
|
218
|
-
// whether a turn carries a thinking block
|
|
219
|
-
//
|
|
220
|
-
// only a project that opts into low/high/max sends one.
|
|
218
|
+
// whether a turn carries a thinking block. No block explicitly disables
|
|
219
|
+
// reasoning below; a low/high/max block enables that native effort.
|
|
221
220
|
if (params.thinking) {
|
|
222
221
|
body.thinking = { type: 'enabled' };
|
|
223
222
|
// Prefer a caller-resolved native value (`low` | `high` | `max`) from the
|
|
@@ -352,9 +351,8 @@ class DeepseekAIProviderImpl {
|
|
|
352
351
|
return;
|
|
353
352
|
}
|
|
354
353
|
const state = {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
cachedTokens: 0,
|
|
354
|
+
finishReason: null,
|
|
355
|
+
usage: {},
|
|
358
356
|
keepAlives: 0,
|
|
359
357
|
streamedChars: 0,
|
|
360
358
|
contentSeen: false,
|
|
@@ -648,48 +646,90 @@ class DeepseekAIProviderImpl {
|
|
|
648
646
|
*/
|
|
649
647
|
*parseNonStreamingResponse(data) {
|
|
650
648
|
const choices = data.choices;
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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');
|
|
678
698
|
}
|
|
679
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
|
+
});
|
|
680
711
|
}
|
|
712
|
+
yield* completedTools;
|
|
681
713
|
}
|
|
682
|
-
const usage = data.usage;
|
|
683
|
-
const cached = usage?.prompt_tokens_details?.cached_tokens ?? 0;
|
|
684
714
|
yield {
|
|
685
715
|
type: 'done',
|
|
686
|
-
usage: {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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,
|
|
693
733
|
};
|
|
694
734
|
}
|
|
695
735
|
/**
|
|
@@ -774,19 +814,22 @@ class DeepseekAIProviderImpl {
|
|
|
774
814
|
return;
|
|
775
815
|
}
|
|
776
816
|
}
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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;
|
|
790
833
|
}
|
|
791
834
|
}
|
|
792
835
|
finally {
|
|
@@ -794,11 +837,7 @@ class DeepseekAIProviderImpl {
|
|
|
794
837
|
}
|
|
795
838
|
yield {
|
|
796
839
|
type: 'done',
|
|
797
|
-
usage:
|
|
798
|
-
inputTokens: Math.max(0, state.inputTokens - state.cachedTokens),
|
|
799
|
-
outputTokens: state.outputTokens,
|
|
800
|
-
cacheReadInputTokens: state.cachedTokens,
|
|
801
|
-
},
|
|
840
|
+
usage: this.parseUsage(state.usage),
|
|
802
841
|
provider: call.meta,
|
|
803
842
|
};
|
|
804
843
|
}
|
|
@@ -898,20 +937,22 @@ class DeepseekAIProviderImpl {
|
|
|
898
937
|
// Usage info (may appear in the final chunk)
|
|
899
938
|
const usage = event.usage;
|
|
900
939
|
if (usage) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
state.
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
};
|
|
907
950
|
}
|
|
908
951
|
const choices = event.choices;
|
|
909
952
|
if (!choices?.length)
|
|
910
953
|
continue;
|
|
911
954
|
const choice = choices[0];
|
|
912
|
-
const delta = choice.delta;
|
|
913
|
-
if (!delta)
|
|
914
|
-
continue;
|
|
955
|
+
const delta = (choice.delta ?? {});
|
|
915
956
|
// Reasoning/thinking content
|
|
916
957
|
if (delta.reasoning_content && typeof delta.reasoning_content === 'string') {
|
|
917
958
|
yield { type: 'thinking', content: delta.reasoning_content };
|
|
@@ -955,30 +996,74 @@ class DeepseekAIProviderImpl {
|
|
|
955
996
|
}
|
|
956
997
|
}
|
|
957
998
|
}
|
|
958
|
-
//
|
|
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`.
|
|
959
1002
|
const finishReason = choice.finish_reason;
|
|
960
|
-
if (finishReason
|
|
961
|
-
|
|
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 = [];
|
|
962
1028
|
for (const [, pending] of pendingTools) {
|
|
963
|
-
let input
|
|
1029
|
+
let input;
|
|
964
1030
|
try {
|
|
965
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
|
+
}
|
|
966
1041
|
}
|
|
967
|
-
catch (
|
|
968
|
-
|
|
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;
|
|
969
1050
|
}
|
|
970
|
-
|
|
1051
|
+
completedTools.push({
|
|
971
1052
|
type: 'tool_use',
|
|
972
1053
|
id: pending.id,
|
|
973
1054
|
name: pending.name,
|
|
974
1055
|
input,
|
|
975
|
-
};
|
|
1056
|
+
});
|
|
976
1057
|
}
|
|
977
1058
|
pendingTools.clear();
|
|
1059
|
+
state.finishReason = finishReason;
|
|
1060
|
+
yield* completedTools;
|
|
978
1061
|
}
|
|
979
1062
|
}
|
|
980
1063
|
catch (error) {
|
|
981
|
-
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;
|
|
982
1067
|
}
|
|
983
1068
|
}
|
|
984
1069
|
}
|