@molecule/api-ai-deepseek 1.0.8 → 1.0.9
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 +4 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +74 -8
- package/package.json +2 -2
package/dist/provider.d.ts
CHANGED
|
@@ -90,6 +90,8 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
90
90
|
* and streamed content. Owned by the caller so a retry can read what the
|
|
91
91
|
* abandoned attempt produced.
|
|
92
92
|
* @param bounds - How to re-arm the stream bound after the reply starts.
|
|
93
|
+
* @param call - The request's own identity, filled in from the response —
|
|
94
|
+
* see {@link DeepseekCallContext}.
|
|
93
95
|
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
94
96
|
*/
|
|
95
97
|
private parseStreamingResponse;
|
|
@@ -100,6 +102,8 @@ declare class DeepseekAIProviderImpl implements AIProvider {
|
|
|
100
102
|
* @param lines - Raw SSE lines to process.
|
|
101
103
|
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
102
104
|
* @param state - Mutable token usage counters.
|
|
105
|
+
* @param call - The request's identity: the answer's own `id` and `model` are
|
|
106
|
+
* recorded on `call.meta`, and any echo of the request is verified.
|
|
103
107
|
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
104
108
|
*/
|
|
105
109
|
private processSSELines;
|
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;
|
|
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,EAGX,MAAM,kBAAkB,CAAA;AASzB,OAAO,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAsGhD,8DAA8D;AAC9D,eAAO,MAAM,8BAA8B,QAAS,CAAA;AAyFpD;;;;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;IAyOzD;;;;;;;;;OASG;YACW,2BAA2B;IA0EzC;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAgFtB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAWnB;;;;;OAKG;IACH,OAAO,CAAE,yBAAyB;IAuDlC;;;;;;;;;;;;OAYG;YACY,sBAAsB;IA2GrC;;;;;;;;;;OAUG;IACH,OAAO,CAAE,eAAe;CA2KzB;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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
11
12
|
import { getLogger } from '@molecule/api-bond';
|
|
12
13
|
import { t } from '@molecule/api-i18n';
|
|
13
14
|
const logger = getLogger();
|
|
@@ -205,9 +206,24 @@ class DeepseekAIProviderImpl {
|
|
|
205
206
|
body.temperature = params.temperature;
|
|
206
207
|
}
|
|
207
208
|
}
|
|
209
|
+
// Correlation for THIS call (see ChatParams.requestId). It rides a header
|
|
210
|
+
// rather than the body's `user`: `user` is the stable per-user
|
|
211
|
+
// abuse-attribution value the provider scopes enforcement by, and varying it
|
|
212
|
+
// per request would make one account look like thousands. The header is
|
|
213
|
+
// ignored by every OpenAI-compatible server and is here so the vendor can
|
|
214
|
+
// find our request when a reply turns out not to belong to it.
|
|
215
|
+
const requestId = params.requestId ?? randomUUID();
|
|
216
|
+
// What the provider tells us about the call, reported on `done` (or logged
|
|
217
|
+
// when the reply has to be dropped as foreign). `responseId` and `model`
|
|
218
|
+
// come off the stream itself; the trace id off the response headers.
|
|
219
|
+
const call = {
|
|
220
|
+
meta: { requestId, endpoint: `${this.baseUrl}${this.completionsPath}` },
|
|
221
|
+
endUserId: params.endUserId,
|
|
222
|
+
};
|
|
208
223
|
const headers = {
|
|
209
224
|
'Content-Type': 'application/json',
|
|
210
225
|
Authorization: `Bearer ${this.apiKey}`,
|
|
226
|
+
'X-Mol-Request-Id': requestId,
|
|
211
227
|
};
|
|
212
228
|
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
213
229
|
const firstTokenTimeoutMs = this.firstTokenTimeoutMs;
|
|
@@ -241,6 +257,16 @@ class DeepseekAIProviderImpl {
|
|
|
241
257
|
model,
|
|
242
258
|
signal,
|
|
243
259
|
});
|
|
260
|
+
// Whatever the transport exposes about this call. DeepSeek's edge
|
|
261
|
+
// answers with `x-ds-trace-id`; other OpenAI-compatible hosts use
|
|
262
|
+
// `x-request-id`. Recorded rather than read for control flow: it is the
|
|
263
|
+
// handle a vendor investigation needs.
|
|
264
|
+
const traceId = response.headers.get('x-ds-trace-id') ??
|
|
265
|
+
response.headers.get('x-request-id') ??
|
|
266
|
+
response.headers.get('request-id') ??
|
|
267
|
+
undefined;
|
|
268
|
+
if (traceId)
|
|
269
|
+
call.meta.traceId = traceId;
|
|
244
270
|
if (!response.ok) {
|
|
245
271
|
const errorBody = await response.text();
|
|
246
272
|
let detail = `HTTP ${response.status}`;
|
|
@@ -299,10 +325,7 @@ class DeepseekAIProviderImpl {
|
|
|
299
325
|
};
|
|
300
326
|
armBound(firstTokenTimeoutMs, 'before_content');
|
|
301
327
|
try {
|
|
302
|
-
yield* this.parseStreamingResponse(response, state, {
|
|
303
|
-
silenceMs: this.streamSilenceTimeoutMs,
|
|
304
|
-
arm: armBound,
|
|
305
|
-
});
|
|
328
|
+
yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
|
|
306
329
|
}
|
|
307
330
|
catch (error) {
|
|
308
331
|
// Only a bound expiry is this provider's to absorb — anything else
|
|
@@ -608,9 +631,11 @@ class DeepseekAIProviderImpl {
|
|
|
608
631
|
* and streamed content. Owned by the caller so a retry can read what the
|
|
609
632
|
* abandoned attempt produced.
|
|
610
633
|
* @param bounds - How to re-arm the stream bound after the reply starts.
|
|
634
|
+
* @param call - The request's own identity, filled in from the response —
|
|
635
|
+
* see {@link DeepseekCallContext}.
|
|
611
636
|
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
612
637
|
*/
|
|
613
|
-
async *parseStreamingResponse(response, state, bounds) {
|
|
638
|
+
async *parseStreamingResponse(response, state, bounds, call) {
|
|
614
639
|
const reader = response.body?.getReader();
|
|
615
640
|
if (!reader) {
|
|
616
641
|
yield {
|
|
@@ -634,7 +659,7 @@ class DeepseekAIProviderImpl {
|
|
|
634
659
|
const lines = buffer.split('\n');
|
|
635
660
|
buffer = lines.pop() ?? '';
|
|
636
661
|
let yieldedContent = false;
|
|
637
|
-
for (const event of this.processSSELines(lines, pendingTools, state)) {
|
|
662
|
+
for (const event of this.processSSELines(lines, pendingTools, state, call)) {
|
|
638
663
|
yieldedContent = true;
|
|
639
664
|
// The reply has started. Counting this from the EVENTS (rather than
|
|
640
665
|
// from the byte stream) is what lets the first-token bound see a
|
|
@@ -673,7 +698,7 @@ class DeepseekAIProviderImpl {
|
|
|
673
698
|
}
|
|
674
699
|
// Flush any remaining data in the buffer after EOF
|
|
675
700
|
if (buffer.trim()) {
|
|
676
|
-
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
|
|
701
|
+
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state, call)) {
|
|
677
702
|
yield event;
|
|
678
703
|
if (event.type === 'error')
|
|
679
704
|
return;
|
|
@@ -704,6 +729,7 @@ class DeepseekAIProviderImpl {
|
|
|
704
729
|
outputTokens: state.outputTokens,
|
|
705
730
|
cacheReadInputTokens: state.cachedTokens,
|
|
706
731
|
},
|
|
732
|
+
provider: call.meta,
|
|
707
733
|
};
|
|
708
734
|
}
|
|
709
735
|
/**
|
|
@@ -713,9 +739,11 @@ class DeepseekAIProviderImpl {
|
|
|
713
739
|
* @param lines - Raw SSE lines to process.
|
|
714
740
|
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
715
741
|
* @param state - Mutable token usage counters.
|
|
742
|
+
* @param call - The request's identity: the answer's own `id` and `model` are
|
|
743
|
+
* recorded on `call.meta`, and any echo of the request is verified.
|
|
716
744
|
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
717
745
|
*/
|
|
718
|
-
*processSSELines(lines, pendingTools, state) {
|
|
746
|
+
*processSSELines(lines, pendingTools, state, call) {
|
|
719
747
|
for (const line of lines) {
|
|
720
748
|
if (!line.startsWith('data: '))
|
|
721
749
|
continue;
|
|
@@ -724,6 +752,44 @@ class DeepseekAIProviderImpl {
|
|
|
724
752
|
continue;
|
|
725
753
|
try {
|
|
726
754
|
const event = JSON.parse(json);
|
|
755
|
+
// The reply's OWN identity, recorded for the caller: the completion id
|
|
756
|
+
// (one per answer, repeated on every delta) and the model the provider
|
|
757
|
+
// says answered. Recorded rather than asserted — a re-host legitimately
|
|
758
|
+
// answers under its own dated snapshot id.
|
|
759
|
+
if (typeof event.id === 'string')
|
|
760
|
+
call.meta.responseId = event.id;
|
|
761
|
+
if (typeof event.model === 'string')
|
|
762
|
+
call.meta.model = event.model;
|
|
763
|
+
// If this host echoes an identity back, verify it. A reply carrying an
|
|
764
|
+
// identity that is not the one we sent is a reply that was produced for
|
|
765
|
+
// somebody else's request, and it must not be streamed on as ours —
|
|
766
|
+
// 2026-10-16 a conversation received a complete foreign completion on a
|
|
767
|
+
// stream whose usage matched its own context, and nothing could tell.
|
|
768
|
+
// DeepSeek's own API echoes nothing, so this arm is inert there; it is
|
|
769
|
+
// live for any OpenAI-compatible host that does, and it is the only
|
|
770
|
+
// check that runs BEFORE the text reaches the caller.
|
|
771
|
+
const echoedUser = typeof event.user === 'string' ? event.user : undefined;
|
|
772
|
+
const echoedRequest = typeof event.request_id === 'string'
|
|
773
|
+
? event.request_id
|
|
774
|
+
: typeof event.requestId === 'string'
|
|
775
|
+
? event.requestId
|
|
776
|
+
: undefined;
|
|
777
|
+
const wrongUser = echoedUser !== undefined && call.endUserId !== undefined && echoedUser !== call.endUserId;
|
|
778
|
+
const wrongRequest = echoedRequest !== undefined && echoedRequest !== call.meta.requestId;
|
|
779
|
+
if (wrongUser || wrongRequest) {
|
|
780
|
+
logger.error('Provider echoed a different identity — this reply is not ours', {
|
|
781
|
+
requestId: call.meta.requestId,
|
|
782
|
+
echoedUser: wrongUser ? echoedUser : undefined,
|
|
783
|
+
echoedRequestId: wrongRequest ? echoedRequest : undefined,
|
|
784
|
+
responseId: call.meta.responseId,
|
|
785
|
+
traceId: call.meta.traceId,
|
|
786
|
+
endpoint: call.meta.endpoint,
|
|
787
|
+
});
|
|
788
|
+
yield { type: 'error', message: t('ai.error.apiError'), errorKey: 'ai.error.apiError' };
|
|
789
|
+
// The whole reply is somebody else's: stop reading it. The caller's
|
|
790
|
+
// error ladder retries the request once.
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
727
793
|
// A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
|
|
728
794
|
// `choices`) — most importantly rate-limit/overload during high load.
|
|
729
795
|
// Without this it matches no branch below and is silently dropped, so the
|
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.9",
|
|
4
4
|
"description": "DeepSeek AI provider for molecule.dev",
|
|
5
5
|
"homepage": "https://www.molecule.dev/packages/api-ai-deepseek",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"directory": "packages/api/bonds/ai/deepseek"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@molecule/api-ai": "1.3.
|
|
36
|
+
"@molecule/api-ai": "1.3.3",
|
|
37
37
|
"@molecule/api-bond": "1.0.2",
|
|
38
38
|
"@types/node": "26.1.2",
|
|
39
39
|
"typescript": "6.0.3",
|