@molecule/api-ai-deepseek 1.0.7 → 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 +79 -12
- 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;
|
|
@@ -217,10 +233,11 @@ class DeepseekAIProviderImpl {
|
|
|
217
233
|
// wrong, and DeepSeek keeps a stalled prefill's connection alive with
|
|
218
234
|
// `: keep-alive` comments — so a byte-level idle timeout stays quiet too.
|
|
219
235
|
// Left alone, the whole first-token window is spent before anything notices.
|
|
220
|
-
// Measured in production
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
236
|
+
// Measured in production: on 2026-10-01 nine turns died this way in one
|
|
237
|
+
// project, each spending 540-582 s — three 180 s windows of the chat
|
|
238
|
+
// handler's progress watchdog, which was the ONLY bound on the stream, and
|
|
239
|
+
// their own record named no provider time at all. Bound it here instead,
|
|
240
|
+
// where the same request can simply be asked again.
|
|
224
241
|
for (let stalledAttempt = 0;; stalledAttempt++) {
|
|
225
242
|
const controller = new AbortController();
|
|
226
243
|
const forwardAbort = () => controller.abort();
|
|
@@ -240,6 +257,16 @@ class DeepseekAIProviderImpl {
|
|
|
240
257
|
model,
|
|
241
258
|
signal,
|
|
242
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;
|
|
243
270
|
if (!response.ok) {
|
|
244
271
|
const errorBody = await response.text();
|
|
245
272
|
let detail = `HTTP ${response.status}`;
|
|
@@ -298,10 +325,7 @@ class DeepseekAIProviderImpl {
|
|
|
298
325
|
};
|
|
299
326
|
armBound(firstTokenTimeoutMs, 'before_content');
|
|
300
327
|
try {
|
|
301
|
-
yield* this.parseStreamingResponse(response, state, {
|
|
302
|
-
silenceMs: this.streamSilenceTimeoutMs,
|
|
303
|
-
arm: armBound,
|
|
304
|
-
});
|
|
328
|
+
yield* this.parseStreamingResponse(response, state, { silenceMs: this.streamSilenceTimeoutMs, arm: armBound }, call);
|
|
305
329
|
}
|
|
306
330
|
catch (error) {
|
|
307
331
|
// Only a bound expiry is this provider's to absorb — anything else
|
|
@@ -607,9 +631,11 @@ class DeepseekAIProviderImpl {
|
|
|
607
631
|
* and streamed content. Owned by the caller so a retry can read what the
|
|
608
632
|
* abandoned attempt produced.
|
|
609
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}.
|
|
610
636
|
* @yields {ChatEvent} Chat events as they arrive from the SSE stream.
|
|
611
637
|
*/
|
|
612
|
-
async *parseStreamingResponse(response, state, bounds) {
|
|
638
|
+
async *parseStreamingResponse(response, state, bounds, call) {
|
|
613
639
|
const reader = response.body?.getReader();
|
|
614
640
|
if (!reader) {
|
|
615
641
|
yield {
|
|
@@ -633,7 +659,7 @@ class DeepseekAIProviderImpl {
|
|
|
633
659
|
const lines = buffer.split('\n');
|
|
634
660
|
buffer = lines.pop() ?? '';
|
|
635
661
|
let yieldedContent = false;
|
|
636
|
-
for (const event of this.processSSELines(lines, pendingTools, state)) {
|
|
662
|
+
for (const event of this.processSSELines(lines, pendingTools, state, call)) {
|
|
637
663
|
yieldedContent = true;
|
|
638
664
|
// The reply has started. Counting this from the EVENTS (rather than
|
|
639
665
|
// from the byte stream) is what lets the first-token bound see a
|
|
@@ -672,7 +698,7 @@ class DeepseekAIProviderImpl {
|
|
|
672
698
|
}
|
|
673
699
|
// Flush any remaining data in the buffer after EOF
|
|
674
700
|
if (buffer.trim()) {
|
|
675
|
-
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state)) {
|
|
701
|
+
for (const event of this.processSSELines(buffer.split('\n'), pendingTools, state, call)) {
|
|
676
702
|
yield event;
|
|
677
703
|
if (event.type === 'error')
|
|
678
704
|
return;
|
|
@@ -703,6 +729,7 @@ class DeepseekAIProviderImpl {
|
|
|
703
729
|
outputTokens: state.outputTokens,
|
|
704
730
|
cacheReadInputTokens: state.cachedTokens,
|
|
705
731
|
},
|
|
732
|
+
provider: call.meta,
|
|
706
733
|
};
|
|
707
734
|
}
|
|
708
735
|
/**
|
|
@@ -712,9 +739,11 @@ class DeepseekAIProviderImpl {
|
|
|
712
739
|
* @param lines - Raw SSE lines to process.
|
|
713
740
|
* @param pendingTools - Mutable map tracking tool call deltas accumulated across chunks.
|
|
714
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.
|
|
715
744
|
* @yields {ChatEvent} Chat events extracted from the SSE lines.
|
|
716
745
|
*/
|
|
717
|
-
*processSSELines(lines, pendingTools, state) {
|
|
746
|
+
*processSSELines(lines, pendingTools, state, call) {
|
|
718
747
|
for (const line of lines) {
|
|
719
748
|
if (!line.startsWith('data: '))
|
|
720
749
|
continue;
|
|
@@ -723,6 +752,44 @@ class DeepseekAIProviderImpl {
|
|
|
723
752
|
continue;
|
|
724
753
|
try {
|
|
725
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
|
+
}
|
|
726
793
|
// A mid-stream error arrives as a `data: {"error": {...}}` chunk (no
|
|
727
794
|
// `choices`) — most importantly rate-limit/overload during high load.
|
|
728
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",
|